快速排序
一、原理
☆思想:快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists),又是一种分而治之思想在排序算法上的典型应用;通过一趟排序将要排序的数据分割成独立的两部分,调整后其中一部分的所有数据比另外一部分的所有数据都要小,再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,从而达到整个数据变成有序序列;
☆过程:以递增为例,用数组表示,长度为n,有如下几个步骤;
- 设定一个分界值,通过该分界值将数组分成左右两部分;
- 将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边,这个称为分区(partition)操作;
- 重复上述过程,可以使用递归;通过递归再对左右区间重复第二步,直到各区间只有一个数。 。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。
二、实现代码
JavaScript 代码实现挖坑法
function partition(arr, start, end) { var left = start, right = end, pivot = arr[left]; //基准值,在首端挖坑 while (left < right) { //基准值在首端,从尾端开始遍历,寻找小于基准值元素 while (right > left && arr[right] >= pivot) { right--; } //挖走右端元素填左坑 arr[left] = arr[right]; //从首端开始遍历,寻找大于基准值元素 while (left < right && arr[left] <= pivot) { left++; } //挖走左端元素填右坑 arr[right] = arr[left]; } //基准值就位 arr[right] = pivot; return right; } function qsort(arr, start, end) { if (start < end) { let pivot = partition(arr, start, end); qsort(arr, start, pivot - 1); qsort(arr, pivot + 1, end); } }
JavaScript 代码实现双指针法
function partition(arr, start, end) { var left = start, right = end, pivot = arr[left]; while (left < right) { //基准值在首端,从尾端开始遍历,寻找小于基准值元素 while (right > left && arr[right] >= pivot) { right--; } //从首端开始遍历,寻找大于基准值元素 while (left < right && arr[left] <= pivot) { left++; } //交换左右元素 swap(arr, left, right); } //如果指针落在比基准值小的元素,移到首端 if (arr[right] < pivot) { arr[start] = arr[right]; } //基准值就位 arr[right] = pivot; return right; } function qsort(arr, start, end) { if (start < end) { let pivot = partition(arr, start, end); qsort(arr, start, pivot - 1); qsort(arr, pivot + 1, end); } } function swap(arr, i, j) { var temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
JavaScript 代码实现单向遍历法,适用于单向链表
function partition(arr, start, end) { var base = start, bigger = base + 1; for (var i = bigger; i <= end; i++) { //从首端开始遍历,bigger指示比基准值大的元素索引 //遍历i找到比基准值小的元素索引,和bigger交换元素,同时bigger后移一位 //如果i和bigger重合,只把bigger后移一位 if (arr[i] < arr[base]) { if (i != bigger) { swap(arr, i, bigger); } bigger++; } } //bigger越界减1,和基准值交换元素,bigger-1为分区索引 swap(arr, base, bigger - 1); return bigger - 1; } function qsort(arr, start, end) { if (start < end) { let pivot = partition(arr, start, end); qsort(arr, start, pivot - 1); qsort(arr, pivot + 1, end); } } function swap(arr, i, j) { var temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
JavaScript 代码实现迭代法
function qsort(arr, start, end) { var base, bigger; var stack = [], top = 0; var sjson; stack[top++] = { start: start, end: end }; while (top) { //出栈,以备分区 sjson = stack[--top]; jstart = sjson.start; jend = sjson.end; base = jstart; bigger = base + 1; for (var i = bigger; i <= jend; i++) { //从首端开始遍历,bigger指示比基准值大的元素索引 //遍历i找到比基准值小的元素索引,和bigger交换元素,同时bigger后移一位 //如果i和bigger重合,只把bigger后移一位 if (arr[i] < arr[base]) { if (i != bigger) { swap(arr, i, bigger); } bigger++; } } //bigger越界减1,和基准值交换元素,bigger-1为分区索引 swap(arr, base, bigger - 1); //左边部分长度大于1,入栈以备循环 if (jstart < bigger - 1) { stack[top++] = { start: jstart, end: bigger - 1 }; } //右边部分长度大于1,入栈以备循环 if (bigger < jend) { stack[top++] = { start: bigger, end: jend }; } } } function swap(arr, i, j) { var temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
三、优化
- 优化选取基准值;
- 三数取中法,解决数据基本有序的(就是找到数组中最小下标,最大下标,中间下标的数值,进行比较,把中间值和最左元素交换);
- 随机选取基准,在待排序列是部分有序时,固定选取枢轴使快排效率底下,就引入了随机选取枢轴,把其和最左元素交换;
- 去掉不必要的交换;
- 数组长度大于阈值的,使用归并排序策略,数组长度小于阈值的,使用直接插入排序;
- 优化递归;
- 三路划分;
四、复杂度
| 名称 | 时间复杂度 | 空间复杂度 | 稳定性 | ||
| 平均 | 最坏 | 最优 | |||
| 快速排序 | O(nlogn) | O(n²) | O(nlogn) | O(logn) | X |
- 快速排序的时间主要耗费在划分操作上,对长度为k的区间进行划分,共需k-1次关键字的比较;
- 最坏情况是每次划分选取的基准都是当前无序区中关键字最小(或最大)的记录,划分的结果是基准左边的子区间为空(或右边的子区间为空),而划分所得的另一个非空的子区间中记录数目,仅仅比划分前的无序区中记录个数减少一个。时间复杂度为O(n²);
- 最优情况,每次划分所取的基准都是当前无序区的"中值"记录,划分的结果是基准的左、右两个无序子区间的长度大致相等。总的关键字比较次数:O(nlogn);
- 尽管快速排序的最坏时间为O(n²),但就平均性能而言,它是基于关键字比较的内部排序算法中速度最快者,快速排序亦因此而得名。它的平均时间复杂度为O(nlogn)。
- 本排序每次划分需要为基准值申请一个额外空间,大约需要logn次划分,空间复杂度为O(logn);
- 跳跃式的交换可能会造成元素的相对位置的改变。,本排序不稳定。
参考资料:
- 菜鸟教程
- https://www.cnblogs.com/BaoZiY/p/10931305.html
- https://www.cnblogs.com/jing-an-feng-shao/p/9865644.html
- 《大话数据结构》2011年清华大学出版社 作者程杰

浙公网安备 33010602011771号