JavaScript--数据结构与算法之排序
排序总结————常见的排序
常见的9中排序(冒泡,选择,插入(二分插入,希尔),归并,快速,堆,计数,基数,桶排序)可分为两类
比较排序:冒泡,选择,插入(二分插入,希尔),归并,堆,快速
非比较排序:计数,基数,桶排序
var arr = [298,113,138,96,78,203,56,11]; function testMap(arr) { for(var i = 0; i < arr.length;i++) { $(".testBoxList").eq(i).css({"height":arr[i]}); } } testMap(arr);//初始化 //BubbleSort(arr); //CocktailSort(arr); //SelectSort(arr); //InsertSort(arr); //BinaryInsertSort(arr); //ShellSort(arr); //mergeSort(arr); quickSort(arr,0,arr.length-1); setTimeout(function() { testMap(arr); console.log(arr); },1500);
1、冒泡排序:(Bubble sort)
冒泡排序算法的运作如下:
1、比较相邻的元素,如果前一个比后一个大,就把它们两个调换位置。
2、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
3、针对所有的元素重复以上的步骤,除了最后一个。
4、持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。*/
分类 -------------- 内部比较排序
数据结构 ---------- 数组
最差时间复杂度 ---- O(n^2)
最优时间复杂度 ---- 如果能在内部循环第一次运行时,使用一个旗标来表示有无需要交换的可能,可以把最优时间复杂度降低到O(n)
平均时间复杂度 ---- O(n^2)
所需辅助空间 ------ O(1)
稳定性 ------------ 稳定*/
function BubbleSort(arr) { for(var i = 0;i < arr.length-1; i++) {//控制次数 for(var j = 0;j < arr.length-1-i; j++) {//两两比较找出最大的,-i比较过的不用在比较 if(arr[j] > arr[j+1]) { var temp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = temp; } } } }
2 鸡尾酒排序(定向冒泡排序)从低到高然后从高到低来回排序,性能略好于冒泡排序。(与折半查找类似)
分类 -------------- 内部比较排序
数据结构 ---------- 数组
最差时间复杂度 ---- O(n^2)
最优时间复杂度 ---- 如果序列在一开始已经大部分排序过的话,会接近O(n)
平均时间复杂度 ---- O(n^2)
所需辅助空间 ------ O(1)
稳定性 ------------ 稳定
function CocktailSort(arr) { var left = 0; var right = arr.length - 1; while(left < right) { for(var i = left;i < right;i++) { if(arr[i] > arr[i+1]) { var temp = arr[i]; arr[i] = arr[i+1]; arr[i+1] = temp; } } right--; for(var j = right; j > left;j--) { if(arr[j-1] > arr[j]) { var temp1 = arr[j-1]; arr[j-1] = arr[j]; arr[j] = temp1; } } left++; } }
3 选择排序:排序原理:开始时在序列中找到一个最小的元素作为有序的序列的第一个元素,一次找次最小元素放在有序队列的末尾,以此类推知道所有元素有序为止。
与冒泡排序的区别:冒泡在每一次中两个相邻的元素对比找出大的或者小的元素,经过往复就可以达到有序。选择排序是在遍历的过程中利用当前最小元素和所有元素进行比较。
分类 -------------- 内部比较排序
数据结构 ---------- 数组
最差时间复杂度 ---- O(n^2)
最优时间复杂度 ---- O(n^2)
平均时间复杂度 ---- O(n^2)
所需辅助空间 ------ O(1)
稳定性 ------------ 不稳定
function SelectSort(arr) { for(var i = 0;i < arr.length-1;i++) { var min = i;//第一个元素 for(var j = i + 1; j < arr.length; j++) {//从第二个元素开始遍历 if(arr[j] < arr[min]) {//依次找到最小的元素 min = j; } } if(min != i) {//放到已排序序列的末尾,该操作很有可能把稳定性打乱,选择排序是不稳定的排序算法 var temp = arr[min]; arr[min] = arr[i]; arr[i] = temp; } } }
4 插入排序(直接插入排序):
算法描述如下:(类似一玩扑克牌的插入)
1、从第一个元素开始,该元素可以认为已经被排序
2、取出下一个元素,在已经排序的元素序列中从后向前扫描
3、如果该元素(已排序)大于新元素,将该元素移到下一位置
4、重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
5、将新元素插入到该位置后
6、重复步骤2~5
function InsertSort(arr) { for(var i = 1;i < arr.length;i++) { var first= arr[i];//抓取一张扑克牌 var j = i-1;//另一部分牌存放总是排序好的 while(j >= 0 && arr[j] > first) {//将抓到的牌与有序的牌从右向左进行比较 arr[j + 1] = arr[j]; //如果该有序的牌比抓到的牌大,就将其右移 j--; } arr[j + 1] = first; //直到该手牌比抓到的牌小(或二者相等),将抓到的牌插入到该手牌右边(相等元素的相对次序未变,所以插入排序是稳定的)//此处的j+1=原来的j,在while中j--操作 } }
插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,比如量级小于千,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。
5 插入排序的改进:二分插入排序(折半插入)
function BinaryInsertSort(arr) { for(var i = 1; i < arr.length;i++) { var first = arr[i];//抓取第一下张牌 var left = 0; var right = i-1;//两个游标 while(left <= right) {//满足游标的不出界条件,确定抓到的新牌的位置 var mind = Math.ceil( (left + right)/2 ); //折半对比必须转化成整数,parseInt()也可以 if(arr[mind] > first) {//如果抓取的牌小于中间的则把中间的左边的位置换成最右侧的,一次这样判断 right = mind-1; }else{ left = mind+1; } } for (var j = i-1; j >= left; j--) { // 将欲插入新牌位置右边的牌整体向右移动一个单位 arr[j + 1] = arr[j]; } arr[left] = first;//位置移动后将插入的牌插入对应的位置 } }
当n较大时,二分插入排序的比较次数比直接插入排序的最差情况好得多,但比直接插入排序的最好情况要差,所当以元素初始序列已经接近升序时,直接插入排序比二分插入排序比较次数少。二分插入排序元素移动次数与直接插入排序相同,依赖于元素初始序列。*/
6 插入排序的更高效改进:希尔排序(Shell Sort) (递减增量排序插入排序的一种更高效的改进版本。希尔排序是不稳定的排序算法。)
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
1 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
2 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。
假设有一个很小的数据在一个已按升序排好序的数组的末端。如果用复杂度为O(n^2)的排序(冒泡排序或直接插入排序),可能会进行n次的比较和交换才能将该数据移至正确位置。而希尔排序会用较大的步长移动数据,所以小数据只需进行少数比较和交换即可到正确位置。
分类 -------------- 内部比较排序
数据结构 ---------- 数组
最差时间复杂度 ---- 根据步长序列的不同而不同。已知最好的为O(n(logn)^2)
最优时间复杂度 ---- O(n)
平均时间复杂度 ---- 根据步长序列的不同而不同。
所需辅助空间 ------ O(1)
稳定性 ------------ 不稳定
function ShellSort(arr) { //var arr = [298,113,138,96,78,203,56,11];----1 //var arr = [113,298,138,96,78,203,56,11];----2 //var arr = [113,138,298,96,78,203,56,11];----2 var n = 0; while(n <= arr.length) { //生成初始增量 n = 3*n + 1; }//n = 1 while ( n >= 1) { for(var i = n; i < arr.length; i++) {//i=1,i=2,i=3 var j = i - n;//j=0,j=1,j=2 var first = arr[i];//first=arr[1]=113,first=arr[2]=138,first=arr[3]=96 while (j >= 0 && arr[j] > first) {//arr[0] 与 first比较。抓到的一张牌和第一张对比,298-113, 298-138 arr[0]=113--first=138(不成立),arr[2]=298--first=96 arr[j + n] = arr[j];//大的往后移动n个位置 arr[1] =113--arr[0]=298, arr[1+1]=138--arr[1]=298,arr[2+1]=96--arr[2]=298 j = j - n;//j-n=-1;移动之后确定退出的条件 j= 0-1=-1,j=1-1=0,j=2-1 } arr[j + n] = first;//j=1,小的做一次插入排序 arr[0+1]=first=113 arr[0+1]=138 } n = (n - 1) / 3; // 递减增量 } }
希尔排序是不稳定的排序算法,虽然一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱。
ShellSort2(); function ShellSort2() { var arr=[49,38,65,97,76,13,27,49,55,04],len=arr.length; for(var fraction=Math.floor(len/2);fraction>0;fraction=Math.floor(fraction/2)){ for(var i=fraction;i<len;i++){ for(var j=i-fraction;j>=0&&arr[j]>arr[fraction+j];j-=fraction){ var temp=arr[j]; arr[j]=arr[fraction+j]; arr[fraction+j]=temp; } } } //console.log(arr); }
7 归并排序 由计算机之父冯·诺伊曼提出
归并排序的实现分为递归实现与非递归(迭代)实现。递归实现的归并排序是算法设计中分治策略的典型应用,我们将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决整个大问题。非递归(迭代)实现的归并排序首先进行是两两归并,然后四四归并,然后是八八归并,一直下去直到归并了整个数组。
归并排序算法主要依赖归并(Merge)操作。归并操作指的是将两个已经排序的序列合并成一个序列的操作,归并操作步骤如下:
1、申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
2、设定两个指针,最初位置分别为两个已经排序序列的起始位置
3、比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
4、重复步骤3直到某一指针到达序列尾
5、将另一序列剩下的所有元素直接复制到合并序列尾
function mergeSort(arr) { var len = arr.length; if(len > 1) { var index = Math.floor(len / 2); left = arr.slice(0,index); //得到下标从0~index-1的数组 right = arr.slice(index); //得到下标从index开始到末尾的数组 return merge(mergeSort(left) , mergeSort(right)); //里面采用递归 }else { return arr; } } function merge(left , right) { //该函数与快排类似,但是仔细发现,每次left或者right都是要shift掉第一个元素,表示left或者right是会变化的,最后arr.concat,因为不知道left或者right其中一个哪个剩下元素,所以要将剩下的元素给加上 var arr = []; while(left.length && right.length) { if(left[0] < right[0]) { arr.push(left.shift()); }else { arr.push(right.shift()) } } return arr.concat(left , right); } console.log(mergeSort([1, 3, 4, 2, 5, 0, 8, 10, 4])); function merge(left, right) { var result = []; while (left.length && right.length) { if(left[0] < right[0]) { result.push(left.shift()); }else { result.push(right.shift()); } } return result.concat(left, right); } function mergeSort(a) { if (a.length === 1) { return a; } var work = []; for (var i = 0, len = a.length; i < len; i++) { work.push([a[i]]); } work.push([]); // 如果数组长度为奇数 for (var lim = len; lim > 1; lim = (lim + 1) / 2) { for (var j = 0, k = 0; k < lim; j++, k += 2) { work[j] = merge(work[k], work[k + 1]); } work[j] = []; // 如果数组长度为奇数 } return work[0]; }
就是JavaScript没有对递归进行优化。运用递归函数不仅没有运行速度上的优势,还可能造成程序运行失败。因此不建议使用递归。
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n log n)的时间复杂度。代价是需要额外的内存空间。
function mergeSort(arr) { //采用自上而下的递归方法 var len = arr.length; if(len < 2) { return arr; } var middle = Math.floor(len / 2),//进行分割 left = arr.slice(0, middle), right = arr.slice(middle); return merge(mergeSort(left), mergeSort(right)); } function merge(left, right) {// var result = []; while (left.length>0 && right.length>0) { if(left[0] <= right[0]) { result.push(left.shift()); } else { result.push(right.shift()); } } while (left.length) { result.push(left.shift()); } while (right.length) { result.push(right.shift()); } return result; } console.log(mergeSort([1, 3, 4, 2, 5, 0, 8, 10, 4])); //var arr1 = mergeSort(arr); console.log( mergeSort([298,113,138,96,78,203,56,11]) ); console.log( mergeSort([98,113,138,96,78,23,56,11]) ); console.log( mergeSort([908,1130,138,196,78,203,56,11]) ); console.log( mergeSort([8,203,13,16,78,201,56,11]) ); console.log( mergeSort([8,203,13,16,78,79,156,199]) ); /*setTimeout(function() { testMap(arr1); console.log(arr1); },1500);*/
8 快速排序(Quick Sort)
又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高! 它是处理大数据最快的排序算法之一了。虽然Worst Case的时间复杂度达到了O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为O(n log n) 的排序算法表现要更好,快速排序的最坏运行情况是O(n²),比如说顺序数列的快排。但它的平摊期望时间是O(n log n) ,且O(n log n)记号中隐含的常数因子很小,比复杂度稳定等于O(n log n)的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
快速排序的内循环比大多数排序算法都要短小,这意味着它无论是在理论上还是在实际中都要更快。它的主要缺点是非常脆弱,在实现时要非常小心才能避免低劣的性能。
快速排序使用分治策略(Divide and Conquer)来把一个序列分为两个子序列。步骤为:
1、从序列中挑出一个元素,作为"基准"(pivot).
2、把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。
对每个分区递归地进行步骤1~2,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
一趟快速排序的算法是:
1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;
2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];
3)从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值A[j],将A[j]和A[i]互换;
4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]互换;
5)重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。
/*function quickSort(arr, left, right) { var len = arr.length, partitionIndex, left = typeof left != 'number' ? 0 : left, right = typeof right != 'number' ? len - 1 : right; if (left < right) { partitionIndex = partition(arr, left, right); quickSort(arr, left, partitionIndex-1); quickSort(arr, partitionIndex+1, right); } return arr; } function partition(arr, left ,right) { //分区操作 var pivot = left, //设定基准值(pivot) index = pivot + 1; for (var i = index; i <= right; i++) { if (arr[i] < arr[pivot]) { swap(arr, i, index); index++; } } swap(arr, pivot, index - 1); return index-1; } function swap(arr, i, j) { var temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } console.log( quickSort(arr,0,arr.length-1) );*/ function swap(arr, firstIndex, secondIndex){ var temp = arr[firstIndex]; arr[firstIndex] = arr[secondIndex]; arr[secondIndex] = temp; } //分区操作 function partition(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) {//经过上述的比较以确定arr[i]>pivot且arr[j]<pivot,只需判断i<=j(没有出界)是交换值。 swap(arr, i, j); i++;//交换之后同时修改左右指正 j--; } } return i;//最后返回左指针最后的一个位置,用于确定从哪里开始隔间的下一次 } function quickSort(arr, left, right) { var index; if (arr.length > 1) { index = partition(arr, left, right);//第一次返回左指针的值 //alert(index); if (left < index - 1) { quickSort(arr, left, index - 1); } if (index < right) { quickSort(arr, index, right); } } return arr; } //var result = quickSort(arr, 0, arr.length - 1); console.log( quickSort(arr,0,arr.length-1) ); //快速排序2 function Partition(arr,left,right) { //取正时注意parseInt和Math.floor可以,其他的不可以,会出现调用栈溢出。 var pivot = arr[Math.floor( (left+right)/2 )],_left = left,_right = right; /*Math.floor() 方法可对一个数进行下舍入。 parseInt() 函数可解析一个字符串,并返回一个整数。parseInt(string, radix),string为字符类型的数字,radix为要解析的数字的基数,取值2~36,默认为10,表示10进制。负数的时候有差异:Math.floor(-1.5)//-2 parseInt(-1.5)//-1*/ while(_left <= _right) { while(arr[_left] < pivot) { _left++; } /*if(arr[_left] < pivot) {_left++;}只是个判断语句,条件满足改变_left一次的值,while是个循环,满足条件不断的改变_left的值。 */ while(arr[_right] > pivot) { _right--; } if(_left <= _right) { var temp = _left; _left = _right; _right = temp; _left++; _right--; } } return _left; } function FastSort(arr,left,right) { var index; if(arr.length > 1) { index = Partition(arr,left,right);//4 //alert(index); if(left < index-1) {//只要满足返回来的index,没有出界继续递归传参分割。 FastSort(arr,left,index-1); } if(index < right) { FastSort(arr,index,right); } } return arr; } console.log( FastSort(arr,0,arr.length-1) );
9 堆排序
1.大顶堆:最大堆中的最大元素值出现在根结点(堆顶)
堆中每个父节点的元素值都大于等于其孩子结点(如果存在)
2.小顶堆:最小堆中的最小元素值出现在根结点(堆顶)
堆中每个父节点的元素值都小于等于其孩子结点(如果存在)
3.堆排序原理
堆排序就是把最大堆堆顶的最大数取出,将剩余的堆继续调整为最大堆,再次将堆顶的最大数取出,这个过程持续到剩余数只有一个时结束。在堆中定义以下几种操作:
1)、最大堆调整(Max-Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
2)、创建最大堆(Build-Max-Heap):将堆所有数据重新排序,使其成为最大堆
3)、堆排序(Heap-Sort):移除位在第一个数据的根节点,并做最大堆调整的递归运算
继续进行下面的讨论前,需要注意的一个问题是:数组都是 Zero-Based,这就意味着我们的堆数据结构模型要发生改变
(Zero-Based)
相应的,几个计算公式也要作出相应调整:
Parent(i) = floor((i-1)/2),i 的父节点下标
Left(i) = 2i + 1,i 的左子节点下标
Right(i) = 2(i + 1),i 的右子节点下标
最大堆调整(MAX‐HEAPIFY)的作用是保持最大堆的性质,是创建最大堆的核心子程序
console.log(heapSort(arr)); function heapSort(array) { function swap(array, i, j) { var temp = array[i]; array[i] = array[j]; array[j] = temp; } function maxHeapify(array, index, heapSize) { var iMax, iLeft, iRight; while (true) { iMax = index; iLeft = 2 * index + 1; iRight = 2 * (index + 1); if (iLeft < heapSize && array[index] < array[iLeft]) { iMax = iLeft; } if (iRight < heapSize && array[iMax] < array[iRight]) { iMax = iRight; } if (iMax != index) { swap(array, iMax, index); index = iMax; } else { break; } } } function buildMaxHeap(array) { var i, iParent = Math.floor(array.length / 2) - 1; for (i = iParent; i >= 0; i--) { maxHeapify(array, i, array.length); } } function sort(array) { buildMaxHeap(array); for (var i = array.length - 1; i > 0; i--) { swap(array, 0, i); maxHeapify(array, 0, i); } return array; } return sort(array); }