基于比较排序算法时间下限为O(nlogn),计数排序时间复杂度O(n)。
在待排序列基本有序的情况下,直接插入排序是最佳排序算法;快速排序的效率一般情况下都比较高,但在待排序列基本有序的情况下,时间复杂度接近 O(n2);归并排序效率仅次于快速排序,是稳定排序,经常用于多个有序的数据文件归并成一个有序的数据文件以及求解逆序对数,最好、最坏、平均时间复杂度均为O(nlogn),空间复杂度为O(n);桶排序在数据分布均匀且分区粒度够精细的情况下,其时间复杂度接近于O(n),但空间复杂度将非常大。
一. 选择排序
选择排序包括堆排序(Heapsort)是不稳定。
- 算法描述:每一次从无序区中选出最小(或最大)的一个元素,然后与无序区的首元素(或最后一个元素)进行交换,直到全部排序完成。比如[31,5,8,1,9,2,4,26,7]这个数组包含9个元素,在第一轮对无序区排序中需要8次比较才能找到最小值1,但是在这8次比较中都只记录较小值的索引,值交换是在无序区最值找到之后才发生的且仅一次,最后得到有序区为[1];然后在第二轮对无序区排序中需要7次比较才能找到最小值2,记录较小值的索引,最值交换,最后得到有序区为[1, 2];以此类推,直至排序完成。
- Javascript实现算法:
/** * 示例: s = [2,5,8,1,9,31,4,26,7] * SelectionSort(s) * [1,2,4,5,7,8,9,26,31] * * @param array s * @return array */ function SelectionSort(s) { let minIndex;//当前最小值index for (let i = 0; i < s.length; i++) { minIndex = i; for (let j = i + 1; j < s.length; j++) { if (s[minIndex] > s[j]) { minIndex = j; } } swapValue(minIndex, i); } return s;//返回排序后的数组 function swapValue(minIndex, i) {//交换值 if (minIndex != i) { let temp = s[i]; s[i] = s[minIndex]; s[minIndex] = temp; } } }
二. 快速排序
快速排序(Quicksort)通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列;快速排序最坏时间复杂度O(n2),平均时间复杂度O(nlogn);快速排序是一种不稳定的排序算法。
- 算法描述:
1. 初始化两个变量:i = 0,j = n - 1
2. 以第一个数组元素作为关键数据:key=A[0]
3. j由后向前搜索,找到第一个小于key的值A[j],将A[j]和A[i]互换
4. i由前向后搜索,找到第一个大于key的A[i],将A[i]和A[j]互换
5. 重复第3、4步,直到i == j - Javascript实现算法:
/** * 示例: quickSort.init([2, 5, 8, 1, 9, 31, 4, 26, 7]) * quickSort.do() * [1,2,4,5,7,8,9,26,31] */ quickSort = { s: [],//源数组 init: function (s) {//源数组初始化 this.s = s; }, swap: function (i, j) {//交换this.s[i]和是this.s[j] let temp = this.s[j]; this.s[j] = this.s[i]; this.s[i] = temp; }, do: function (i = 0, j = this.s.length - 1) { if (i >= j) {//当前子集排序已完成,无需继续处理 return; } let i_0 = i,//当前子集最小索引i_0初始化 j_0 = j;//当前子集最大索引j_0初始化 for (; j > i; j--) {//循环结束时是this.s[i] = this.s[j] = key,这里未单独声明一个key if (this.s[j] < this.s[i]) { this.swap(i, j); for (i++; i < j; i++) { if (this.s[i] > this.s[j]) { this.swap(i, j); break;//至此完成一次循环 } } } } this.do(i_0, i - 1);//比key小的部分继续排序 this.do(j + 1, j_0);//比key大的部分继续排序 return this.s;//返回排序后的数组 } };
-
算法优化:
1. 中间值(平衡)分区:以待排数组的首部、尾部和最中间的三个元素的中间值作为中轴key进行比较
2. 多算法混合:根据分区大小选择适用的算法混合使用。比如前期排序采用快速排序,当分区逐渐变小后,再使用堆排序或者插入排序
3. 三分区:一块是小于中轴key的所有元素,一块是等于中轴key的所有元素,另一块是大于中轴key的所有元素
4. 并行:在多台处理机上并行处理
5. 随机主元:随机选取一个元素作为主元key
6. 外部快排:关键数据是一段buffer,将首尾各M/2个元素读入buffer,并对buffer中的这些元素进行排序。然后从待排数组的开头(或者结尾)读入下一个元素,假如这个元素小于buffer中最小的元素,把它写到最开头的空位上;假如这个元素大于buffer中最大的元素,则写到最后的空位上;否则把buffer中最大或者最小的元素写入数组,而把这个元素放在buffer里,并保持buffer始终有序。所有元素都比较并移位完成之后,中间将空出M个位置,以buffer覆盖它们;两端未排部分同理递代,直至最终完成
7. 三路基数(Three-way Radix,也称Multi-key):结合了基数排序和快排的特点,是字符串排序中比较高效的算法。待排数组的元素具有Multi-key特点,如一个字符串,每个字母可以看作是一个key。
三. 插入排序
把待排的项插入到已排好序列的合适位置,因此必须对某些已排序的项依次进行移位后才能插入新项。
- 算法描述:比如[31,5,8,1,9,2,4,26,7]这个数组,在第一轮排序中先将31后移到5的位置(1),然后5插入到31的位置(0);第二轮31继续后移到8的位置(2),然后8移到31的位置(1);以此类推
- Javascript实现算法:
/** * 示例: s = [2,5,8,1,9,31,4,26,7] * InsertionSort(s) * [1,2,4,5,7,8,9,26,31] * * @param array s * @return array */ function InsertionSort(s) { let target,//待排的项 j; for (let i = 1; i < s.length; i++) { target = s[i]; j = i - 1; for ( ; j >= 0 && s[j] > target; j--) { s[j + 1] = s[j]; } s[j + 1] = target; } return s;//返回排序后的数组 }
四. 基数排序
基数排序(Radix Sort)属于分配式排序(Distribution Sort),又称桶子法(Bucket Sort)。它是将要排序的元素分配至某些“桶”中,藉以达到排序的作用。基数排序是稳定排序,在某些时候,基数排序法的效率高于其它的稳定排序。
- 算法描述: 比如[34, 6, 77, 81, 13, 100]这个数组,每个元素每个位上可能的数字基数为0~9,因此设置0~9这10个桶;然后逐次从低位开始每个元素按照当前位基数归入相应的(基数)桶中,最后所有元素将按桶顺序排列
- Javascript实现算法:
/** * 示例: RadixSort([31,5,8,1,9,2,4,26,7]) * [1, 2, 4, 5, 7, 8, 9, 26, 31] * * @param s array * @return array */ function RadixSort(s) { let maxNum = s[0];//跟踪数组中最大的元素,初始时为第一个元素 for (let i = 0; i < s.length; i++) { if (maxNum < s[i]) { maxNum = s[i]; } } let round = maxNum.toString().length,//最大元素决定排序总轮次 fillZeroLeft = '00000000000000000000'.slice(0, round - 1); for (let i = 0; i < s.length; i++) { s[i] = fillZeroLeft + s[i];//将所有元素转换为字串,并左填充round - 1个0 } let digit, buckets; for (let i = 1; i <= round; i++) {//执行round次排序 buckets = [];//初始化桶 for (let j = 0; j < s.length; j++) { digit = s[j].substr(-i, 1);//跟踪元素当前位数字 if (!buckets[digit]) { buckets[digit] = []; } buckets[digit].push(s[j]);//元素压入对应桶中 } s = []; for (let j = 0; j < buckets.length; j++) {//在每一轮排序结束后,用s来收集排序结果 if (buckets[j] && buckets[j].length) { s = s.concat(buckets[j]); } } } for (let i = 0; i < s.length; i++) {//数组从字串还原为数值形式 s[i] = parseInt(s[i]); } return s; }
- 其他非比较排序:
1. 计数排序:它的优势在于在对一定范围内的整数排序时,复杂度为Ο(n) 2. 桶排序 (Bucket Sort):或称箱排序,工作的原理是将数组元素通过桶映射函数分配到有限数量的桶里,每个桶再个分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)
五. 归并排序
归并排序(Merge Sort) 是采用分治法(Divide and Conquer)的一个非常典型的应用,是将两个或两个以上有序的数列(或有序表),合并成一个仍然有序的数列(或有序表)。
- 算法描述:比如[49, 38, 65, 97, 76, 13, 27]这个数组,第一轮两两归并:{38, 49},{65, 97},{13, 76};第二轮对上一轮产生的多个子序列再一次执行两两归并,以此类推,直至最后只剩下单一序列完成。一轮归并过程如下:
1. 声明数组m,m的长度为两个已经排序序列a、b长度之和
2. 初始化i = 0、j = 0、k = 0,此时的i、j、k分别a、b、m的首元素位置
3. m[k] = min(a[i], b[j]),发生该赋值的两个数组索引指向下一单元
4. 重复3,直到a或b遍历完(先遍历完的数组元素此时已全部归并到m中)
5. 将另一序列剩下的所有元素直接合并到m的尾部 - Javascript实现算法:
/** * 示例: mergeSort.do([31,5,8,1,9,2,4,26,7]) * [1, 2, 4, 5, 7, 8, 9, 26, 31] * */ let mergeSort = { merge: function (left = [], right = []) { let i = 0, j = 0, k = 0, m = []; while (i < left.length && j < right.length) { if (left[i] <= right[j]) { m[k] = left[i]; i++; } else { m[k] = right[j]; j++; } k++; } //合并尾部 if (i >= left.length) { m = m.concat(right.slice(j)); } else { m = m.concat(left.slice(i));//当right.length == 0时也是满足的 } return m;//返回归并后的数组 }, do: function (s, round = 0) { let temp = [], step = Math.pow(2,round); if (step >= s.length) { return s; } for (let i = 0; i < s.length - 1; i = i + 2 * step) { temp = temp.concat(this.merge( s.slice(i, i + step), s.slice(i + step, i + 2 * step) )); } return this.do(temp, round + 1); } }
六. 堆排序
n个关键字序列K1,K2,…,Kn称为堆(Heap),可分为大根堆和小根堆,是一棵完全二叉树,k(i)为二叉树的非叶子结点时,则K(2i)是左子节点,k(2i + 1)是右子节点;对于小根堆k(i) <= k(2i) 、 k(i) <= k(2i + 1) (1 ≤ i ≤ n/2),大根堆刚好相反;堆排序的时间复杂度O(nlogn)
- 算法描述:以大根堆排序为例
1. 将初始无序序列R[1..n]调整成一个大根堆
2. 将最大的记录R[1](即堆顶)和最后一个记录R[n]交换,由此得到新的无序区R[1..n-1]和有序区R[n]
3. 将无序区R[1..n-1]重新调整为一个大根堆
4. n--,重复2和3,直至无序区只剩下一个记录R[1] - Javascript实现算法:
/** * 示例: heapSort.do([31,5,8,1,9,2,4,26,7]) * [1, 2, 4, 5, 7, 8, 9, 26, 31] * */ let heapSort = { s: [], do: function (s) { this.init(s); return this.sort(); }, init: function (s) { this.s = s; for (let i = Math.floor(this.s.length / 2) - 1; i >= 0; i--) { this.adjust(i); } }, sort: function () { for (let i = this.s.length - 1; i > 0; i--) { this.swap(0, i); this.adjust(0, i - 1); } return this.s; }, swap: function (i, j) {//交换索引为i、j的元素值 let temp = this.s[i]; this.s[i] = this.s[j]; this.s[j] = temp; }, adjust: function (root = 0, tail = this.s.length - 1) {//调整堆,root代表堆的根,tail代表堆最后一个可能的元素。这里有一个前提是除了root,堆中其余结点均满足堆性质 let left = root * 2 + 1, right = left + 1, target = null; if (left > tail) {//无子 return; } else if (right > tail) {//左子存在,无右子 target = left; } else if (this.s[left] < this.s[right]) {//右子大于左子 target = right; } else {//左子大于等于右子 target = left; } if (this.s[root] < this.s[target]){ this.swap(root, target); this.adjust(target, tail); } } }
七. 希尔排序
希尔排序(Shell Sort,也称缩小增量排序)是对直接插入排序的一种改进,减少了复制的次数,速度要快很多。 希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差。专家们提倡,几乎任何排序工作在开始时都可以用希尔排序,若在实际使用中证明它不够快,再改成快速排序这样更高级的排序算法。
- 算法描述:每一轮分组的关键是如何控制好增量递减
1. 按增量分组分别进行插入排序: Increment = Step - turn * C。比如上图中初始时Step = 5、turn = 0、C = 2,最后待排序列分为Step个组(每组包含两项,最后一项i < n) 2. turn ++ 3. 如果 Increment <= 1,则对整个序列进行最后一次插入排序;否则循环1和2两步