JS实现常用排序算法—经典的轮子值得再造
关于排序算法的博客何止千千万了,也不多一个轮子,那我就斗胆粗制滥造个轮子吧!下面的排序算法未作说明默认是从小到大排序。
1.快速排序
为什么把快排放在最前面呢,因为传说Chrome中数组的sort方法默认采用的就是快排。
算法思想:
(1)在数据集之中,选择一个元素作为"基准"(pivot)。
(2)所有小于"基准"的元素,都移到"基准"的左边;所有大于"基准"的元素,都移到"基准"的右边。
(3)对"基准"左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个元素为止。
毋庸置疑,快排的实现离不开递归了。下面奉上阮大神关于快排的博客一篇。
快速排序(Quicksort)的Javascript实现
在阮大神的博客里有一个真心一目了然的实现方法,下面直接粘上算法实现并添加注释,如果不能理解可以点击上面的链接,直接查看阮大神的博客,如果阮大神的博客都看不懂,请关闭浏览器回炉再造!!!!
var quickSort=function(arr){
if (arr.length<=1) {//如果数组长度小于等于1,直接返回给数组
return arr;
}
/*选择"基准"(pivot),并将其与原数组分离,再定义两个空数组,用来存放一左一右的两个子集。*/
var pivotIndex=Math.floor(arr.length/2);
//基准值可以任意选择,但是选择中间的值比较容易理解
var pivot=arr.splice(pivotIndex,1)[0];
var left=[];
var right=[];
/*然后,开始遍历数组,小于"基准"的元素放入左边的子集,大于基准的元素放入右边的子集。*/
for (var i = 0; i < arr.length; i++) {
if (arr[i]<pivot) {
left.push(arr[i]);
}else{
right.push(arr[i]);
}
}
//最后,将各个部分连接成整体
return quickSort(left).concat([pivot],quickSort(right));
};
var arr=[85,24,63,45,17,31,96,50];
console.log(quickSort(arr));
简直没有比这段代码更能直接明了的表达快排的思想了。但是阮大神的方法牺牲空间换时间方法,查看了很多的算法书,里面的实现方法都是直接在原来的数组上进行交换操作的。下面给出了《学习JavaScript数据结构与算法》中的实现方法,国内比较出名的算法书《大话数据结构》《算法-第四版》中给出的示例也是大同小异。
var quickSort=function (arr) {
quick(arr,0,arr.length-1);//传索引0和及其最末位置,因为要排整个数组
return arr;
}
/*实施快排*/
var quick=function(arr,left,right){
var index;
if (arr.length>1) {
index=partition(arr,left,right);//获取划分后的基准位置
if (left<index-1) {
quick(arr,left,index-1);//对左部分再进行快排
}
if (index<right) {//对右部分再进行快排
quick(arr,index,right);
}
}
}
/*实施划分,并返回划分后的基准位置*/
var partition=function(arr,left,right){
var pivot=arr[Math.floor((right+left)/2)],
i=left,j=right;
while(i<j){
while(arr[i]<pivot){
i++;
}
while(arr[j]>pivot){
j--;
}
if(i<=j){
swapQuickSort(arr,i,j);
i++;j--;
}
}
return i;
}
/*交换数组的两个元素*/
var swapQuickSort=function(array,index1,index2){
var temp=array[index1];
array[index1]=array[index2];
array[index2]=temp;
};
var arr=[85,24,63,45,17,31,96,50];
console.log(quickSort(arr));
快速排序有两个方向,当arr[i] <= pivot,左边的i下标一直往右走,当arr[j] > pivot,右边的j下标一直往左走,。如果i和j都走不动了,i <= j, 交换arr[i]和arr[j],重复上面的过程,直到i>=j,完成一趟快速排序。交换的时候,很有可能把元素的稳定性打乱,比如序列为 5 3 3 4, 现在基准是第二个元素3,就会把前一个3和后一个3交换,元素3的稳定性被打乱,所以快速排序是一个不稳定的排序算法。
2.归并排序
算法思想:
归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
归并排序其实要做两件事:
(1)“分解”——将序列每次折半划分。
(2)“合并”——将划分后的序列段两两合并后排序。
var mergeSort=function (arr) {
if (arr.length<=1) {
return arr;
}
var mid=Math.floor(arr.length/2),
//分解序列
left=arr.slice(0,mid),
right=arr.slice(mid,arr.length);
return merge(mergeSort(left),mergeSort(right));
}
var merge=function(left,right){//合并有序序列
var result=[],i=0,j=0;
while(i<left.length&&j<right.length){
if (left[i]<right[j]) {
result.push(left[i++]);
}else{
result.push(right[j++]);
}
}
while(i<left.length){
result.push(left[i++]);
}
while(j<right.length){
result.push(right[j++]);
}
return result;
}
var arr=[85,24,63,45,17,31,96,50];
console.log(mergeSort(arr));
归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有 序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定 性。那么,在短的有序序列合并的过程中,稳定是是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结 果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。
前面介绍的快速排序和归并排序被大多数语言作为排序的默认实现。下面介绍几种经典的排序算法。
3.冒泡排序
基本思想:
从左到右依次比较相邻的两项,如果前一项比后一项大就交换,这样一趟冒泡下来,最大项就会移动到最右端,重复前面的步骤,依次得到次大项…,
实现要点:双层循环,内层交换
var bubbleSort=function (arr) {
var length=arr.length;
for (var i = 0; i < length; i++) {
/*因为每冒泡一次就会将本次最大值放在最后,所以后面的就不需要再比较了,
所以可以从内循环减去外循环已经跑过的轮数*/
for (var j = 0; j <length-1-i; j++) {
if (arr[j]>arr[j+1]) {
var temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
}
return arr;
}
var arr=[85,24,63,45,17,31,96,50];
console.log(bubbleSort(arr));
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无 聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改 变,所以冒泡排序是一种稳定排序算法。
4.选择排序(简单选择排序)
算法思想:
简单选择排序是一种原址比较排序算法,选择排序大致的思路是找到序列中最小值与第一个元素交换,接着找到次小值与第二个元素交换,以此类推。
实现要点:双层循环,使用一个变量存储最小值下标
var selectionSort=function (arr) {
var length=arr.length;
for (var i = 0; i < length; i++) {
var min=i;//用一个变量存储最小值下标,初始值为仍未排序的第一个元素的下标
for (var j = i+1; j <length; j++) {
//与当前最小值比较,如果小于当前最小值,最小值下标就要移动了
if (arr[j]<arr[min]) {
min=j;
}
}
if(min!=i){//如果最小值下标移动了,就交换元素
var temp=arr[i];
arr[i]=arr[min];
arr[min]=temp;
}
}
return arr;
}
var arr=[85,24,63,45,17,31,96,50];
console.log(selectionSort(arr));
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个 元素不用选择了,因为只剩下它一个最大的元素了。举个例子,序列5 8 5 2 9, 我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。
5.插入排序(直接插入排序)
算法思想:
将一个记录插入到已排序好的有序表中,从而得到一个新,记录数增1的有序表。即:先将序列的第1个记录看成是一个有序的子序列,然后从第2个记录逐个进行插入,直至整个序列有序为止。
var insertionSort=function (arr) {
var length=arr.length;
for (var i = 1; i < length; i++) {
var temp=arr[i];//待插入元素
var j=i;
while (j>0&&temp<arr[j-1]) {//从已有序的序列最后一个元素开始往前比较
arr[j]=arr[j-1];//如果不符合插入要求,那么与之比较的元素就要后移
j--;
}
arr[j]=temp;//插入元素到合适位置
}
return arr;
}
var arr=[85,24,63,45,17,31,96,50];
console.log(insertionSort(arr));
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开 始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相 等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳 定的。
6.希尔排序
前面介绍的直接插入排序在待排序元素少或者序列基本有序时很高效,但是一般情况下,上面两个条件很难满足,但是通过将序列分组可以减少每次参与排序的元素数量。希尔排序就是这样一种改进算法。
算法思想:
对待排记录序列先作“宏观”调整,再作“微观”调整。所谓“宏观”调整,指的是“跳跃式”的插入排序。即:将记录序列分成若干子序列,每个子序列分别进行直接插入排序。关键是这种子序列不是由相邻的记录构成的。假设将 n 个记录分成 d 个子序列,则这 d 个子序列分别为:
{ R[1],R[1+d],R[1+2d],…,R[1+kd] }
{ R[2],R[2+d],R[2+2d],…,R[2+kd] }… { R[d],R[2d],R[3d],…,R[kd],R[(k+1)d] }
var shellSort=function (arr) {
var length=arr.length;
var gap=Math.floor(length/2);
while(gap>=1){
for (var i = gap; i < length; i++) {
var j=i;
var temp=arr[i];
/*对距离为gap的元素组内进行直接插入排序
下面这段是不是似曾相识,和直接排序算法如出一辙。
这不过直接排序算法的gap为1*/
while(j >=0&&temp<arr[j-gap]) {
arr[j]=arr[j-gap];//元素在本组内后移
j-=gap;
}
arr[j]=temp;
}
gap=Math.floor(gap/2);
}
return arr;
}
var arr=[85,24,63,45,17,31,96,50,23];
console.log(shellSort(arr));
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小, 插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元 素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的。
八大排序算法还有两种:基数排序和堆排序,平时用的比较少,这里就没介绍了·····好吧,我承认是我偷懒。
下面总结一下其他相关知识点:
- 不稳定的排序算法:选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法,
- 稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法。
Algorithm | Average | Best | Worst | extra space | stable |
---|---|---|---|---|---|
冒泡排序 | O(N^2) | O(N) | O(N^2) | O(1) | 稳定 |
直接插入排序 | O(N^2) | O(N) | O(N^2) | O(1) | 稳定 |
希尔排序 | O(NlogN)~O(N^2) | O(N^13) | O(N^2) | O(1) | 不稳定 |
简单选择排序 | O(N^2) | O(N^2) | O(N^2) | O(1) | 不稳定 |
快速排序 | O(NlogN) | O(NlogN) | O(N^2) | O(logN)~O(N^2) | 不稳定 |
归并排序 | O(NlogN) | O(NlogN) | O(NlogN) | O(N) | 稳定 |
堆排序 | O(NlogN) | O(NlogN) | O(NlogN) | O(1) | 不稳定 |
基数排序 | O(d*(N+K)) | O(d*(N+K)) | O(d*(N+K)) | O(N+K) | 稳定 |
为了证明其实我并不懒,结尾还是送一个彩蛋吧,既然已经介绍了排序,增能不顺带讲讲经典的二分查找算法实现。
二分查找
不要忘了二分查找的前提是序列有序。可以选用前面已经介绍的任何一种排序算法,这里就选择快速排序吧。
算法步骤:
(1)选择序列的中间值。
(2)如果选中值就是待搜索值,那么你已经点到秋香了
(3)如果待搜索值比选中值小,则返回步骤1并在选中值左边的子序列中寻找
(4)如果待搜索值比选中值大,则返回步骤1并在选中值右边的子序列中寻找
var binarySearch=function(arr,item){
arr=quickSort(arr);//先对序列排序
var low=0,
high=arr.length-1,
mid;
while(low<=high){
mid=Math.floor((high+low)/2);
if (arr[mid]<item) {
low=mid+1;
}else if (arr[mid]>item) {
high=mid-1;
}else{
return mid;
}
}
return -1;//没有找到就返回-1
};
var arr=[85,24,63,45,17,31,96,50,23];
console.log(binarySearch(arr,45));
参考:
- 程序员的内功-数据结构和算法系列
- 排序算法的稳定性和时间复杂度分析]
- 排序算法总结
- 《学习JavaScript数据结构与算法》