快速排序的思路是依据一个“中值”数据项来把数据表分成两半:小于中值的一半和大于中值的一半,然后每部分分别进行快速排序(递归)
如果希望这两半拥有相等数量的数据项,则应该找到数据表的“中位数”
但找中位数需要计算开销,要想没有开销,只能随意找一个数来充当“中值”
快速排序的递归算法“递归三要素如下”
- 基本结束条件:数据表只有一个数据项,因此不需要排序
- 缩小规模:根据“中值”,将数据表分为两半,最好情况是相等规模的两半
- 调用自身:将两半分别调用自身排序(排序的基本操作在分裂过程中)
分裂数据表的目标:找到“中值”的位置
分裂数据的手段
设置左右标(left / right)
右标向左移动,左标向右移动
- 右标一直向左移动,碰到比中值小的数就停止
- 将右标做指位置的数值覆盖掉左标所指向位置的数值
- 左标一直向右移动,碰到比中值打的数就停止
- 将左标做指位置的数值覆盖掉右标所指向位置的数值
继续移动,直到左标等于右标,停止移动
这时左右标所指的位置就是“中值”应处的位置,将中值覆盖到此处
分裂完成,此时的数据表左半部分比中值小,右半部分比中值大
快速排序:图示
快速排序:代码
package com.zyj.test;
/**
* 快速排序实现和分析
* @author 张永俊
*/
import java.util.Arrays;
public class DemoTest {
public static void main(String[] args) {
int[] arr = new int[]{9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
quick(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
public static void quick(int[] arr, int left, int right) {
/*排序之前首先判断高低位指针是否重合,如果重合就代表只有一个数值,不用继续执行*/
if (left < right) {
/*记录每次递归的中值(又称中间值)*/
int baseNumber = arr[left];
/*定义高位指针和低位指针,分别指向*/
int low = left;
int high = right;
/*
* 指针的遍历必须从右边开始,如果从左边开始遍历,会因为没有记录值而丢失数据
* */
/*循环判断,直达两指针位置重合*/
while (low < high) {
/*一旦低位指针找到比中值小的数值,循环停止*/
while (arr[high] >= baseNumber && low < high) {
high--;
}
/*将低位指针所指向的数值覆盖到高位指针所指向的位置*/
arr[low] = arr[high];
/*一旦高位指针找到比中值大的数值,循环停止*/
while (arr[low] <= baseNumber && low < high) {
low++;
}
/*将高位指针所指向的数值覆盖到低位指针所指向的位置*/
arr[high] = arr[low];
}
/*最后将中值保存到两指针重合的位置*/
arr[low] = baseNumber;
/*递归排序*/
quick(arr, left, low - 1);
quick(arr, low + 1, right);
}
}
}
输出结果:
排序算法:算法分析
快速排序过程分为两部分:分裂和移动
如果分裂总能把数据表分为相等的两部分,那么就是O(log n)的复杂度;
而移动需要将每项都与中值进行对比,还是O(n)
综合起来就是O(nlog n);
而且,算法运行过程中不需要而外的存储空间
但是,如果不那么幸运的话,中值所在的分裂点过于偏离中部,造成左右两部分数量不平衡
极端情况,有一部分始终没有数据,这样时间复杂度就退化到O(n^2)
还要加上递归调用的开销(比冒泡排序还要糟糕)
可以适当改进下中值的选取方法,让中值更具代表性
比如“三点取样”,从数据表的头、尾、中间选出中值
但是会产生额外计算开销,且仍然不能排除极端情况