最近在学习数据结构与算法,但是对于学习过的知识总是学了又忘,忘了又学,没有深入的去理解。现在我将我学过的在博客上总结分享出来,一方面加深自己的理解,一方面供大家参考交流学习。
排序算法之快速排序
基本思想
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
排序流程
a. 先从数组中取出一个基准数
b. 分区,将比基准数大的放在右边,小的放在左边
c. 在对左右部分进行重复第二步,直到只剩一个数
代码实现
public static void quickSort(int[] arr, int p, int r) {
if (p < r) {
//int q = partition1(arr, p, r);
int q = partition2(arr, p, r);
quickSort(arr, p, q - 1);
quickSort(arr, q + 1, r);
}
}
/**
* 方法一:单向扫描
* @param arr
* @param p
* @param r
* @return
*/
private static int partition1(int[] arr, int p, int r) {
int pivot = arr[p];//以数组最左侧的值为目标值
int sp = p + 1; //扫描指针
int bigger = r; //右侧指针
while (sp <= bigger) {
if (arr[sp] <= pivot) { //扫描元素小于主元,左指针右移
sp++;
} else { //扫描元素大于主元,二指针元素交换,右指针左移
swap(arr, sp, bigger);
bigger--;
}
}
swap(arr, p, bigger);
return bigger;
}
/**
* 方法二:双向扫描
* @param arr
* @param p
* @param r
* @return
*/
public static int partition2(int[] arr,int p,int r){
int pivot = arr[p];以数组最左侧的值为目标值
int left = p + 1; //扫描指针
int right = r; //右侧指针
while (left <= right){
//left不停的往右走,知道遇到大于主元的元素
while (left <= right&& arr[left] <= pivot) left++;//循环退出时,left一定指向第一个大于主元的元素
while (left <= right&& pivot < arr[right]) right--;//循环退出时,right一定指向第一个小于主元的元素
if(left<=right){
swap(arr,left,right);
}
}
//while退出时,两者交错,right一定指向第一个小于主元的元素
swap(arr, p, right);
return right;
}
优化快速排序,上述两种方法,每次只搞定一个数,而我们进行优化,优化后,可以将所有等于这个数的都搞定,时间复杂度虽然差不多,但是常数项肯定会变小。
public static void quickSort(int[] arr,int p,int r){
if(p < r){
int[] q = partition3(arr,p,r);
quickSort(arr,l,q[0] - 1);
quickSort(arr,q[1] + 1,r);
}
}
/**
* 方法三:优化快速排序
* 将所有等于目标值的独立出来,大于目标值的和小于目标值的继续递归
* @param arr
* @param p
* @param r
* @return
*/
private static int[] partition3(int[] arr, int p, int r) {
int pivot = arr[r]; //以数组最右侧的值为目标值
int less = p - 1; //小于区域的指针
int more = r; //大于区域的指针
int cur = p;
while (cur < more){
if(arr[cur] < pivot){ //当前值小于目标值
less ++; //小于区域指针右移
swap(arr,less,cur);
cur ++;
}else if (arr[cur] > pivot){ //当前值大于目标值
more --; //大于区域指针右移
swap(arr,more,cur);
}else {
cur ++;
}
}
swap(arr,more,r); //more此时为第一个大于目标值的坐标
// 返回存储等于目标值区域的左边界和右边界的数组
return new int[]{less + 1,more};
}
性能分析:
快速排序的一次划分算法从两头交替搜索,直到low和high重合,因此其时间复杂度是O(n);而整个快速排序算法的时间复杂度与划分的趟数有关。
理想的情况是,每次划分所选择的中间数恰好将当前序列几乎等分,经过log2n趟划分,便可得到长度为1的子表。这样,整个算法的时间复杂度为O(nlog2n)。
最坏的情况是,每次所选的中间数是当前序列中的最大或最小元素,这使得每次划分所得的子表中一个为空表,另一子表的长度为原表的长度-1。这样,长度为n的数据表的快速排序需要经过n趟划分,使得整个排序算法的时间复杂度为O(n^2)。
最佳情况:T(n) = O(nlogn) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(nlogn)
算法优化
1、随机快速排序
为了避免最坏情况的发生,我们可以设置目标值为数组中的随机元素。
public static void quickSort(int[] arr,int p,int r){
if(p < r){
//设置目标值为数组中的随机元素,改善最坏情况下的时间性能
swap(arr,l + (int)(Math.random() * (r - p + 1)),r);
int[] p = partition(arr,p,r);
quickSort(arr,l,p[0] - 1);
quickSort(arr,p[1] + 1,r);
}
}
2、三点中值法
将数组中最左边,中间,最右边三个值对比,中值作为目标值
public static void quickSort(int[] arr, int p, int r) {
if (p < r) {
int q = partition4(arr, p, r);
quickSort(arr, p, q - 1);
quickSort(arr, q + 1, r);
}
}
/**
* 三点中值法
* @param arr
* @param p
* @param r
* @return
*/
public static int partition4(int[] arr,int p,int r){
//优化,在P,r,mid之间,选一个中间值作为主元
int midIndex = p + ((r - p) >> 1); //中间下标
int midValueIndex = -1 ; //中值下标
if(arr[p] <= arr[midIndex]&&arr[p] >= arr[r]){
midValueIndex = p;
} else if(arr[r] <= arr[midIndex]&&arr[r] >= arr[p]){
midValueIndex = r;
}else {
midValueIndex = midIndex;
}
swap(arr,p,midValueIndex);
int pivot = arr[p];
int left = p + 1; //扫描指针
int right = r; //右侧指针
while (left <= right){
//left不停的往右走,知道遇到大于主元的元素
while (left <= right&& arr[left] <= pivot) left++;//循环退出时,left一定指向第一个大于主元的元素
while (left <= right&& pivot < arr[right]) right--;//循环退出时,right一定指向第一个小于主元的元素
if(left<=right){
swap(arr,left,right);
}
}
//while退出时,两者交错,right一定指向第一个小于主元的元素
swap(arr, p, right);
return right;
}
但是三点中值法还是不能够保证没有最坏情况的发生,如果要完全避免最坏情况,则需要使用绝对中值法。
3、绝对中值法
通过将待排序数组以5个元素分为一组,取中间值,取到整个数组的各组中间值,再将这些数排序,再取中间值作为主元。因为寻找绝对中值,也会花费时间,所以使用三点中值法居多。
/**
* 获取绝对的中值数,O(N)的样子
*/
public static int getMedian(int[] arr, int p, int r) {
if (arr.length == 1)
return arr[p];
int size = r - p + 1;// 数组长度
//每五个元素一组
int groupSize = (size % 5 == 0) ? (size / 5) : (size / 5 + 1);
//存储各小组的中值
int medians[] = new int[groupSize];
int indexOfMedians = 0;
//对每一组进行插入排序
for (int j = 0; j < groupSize; j++) {
//单独处理最后一组,因为最后一组可能不满5个元素
if (j == groupSize - 1) {
InsertionSort.sort(arr, p + j * 5, r); // 排序最后一组
medians[indexOfMedians++] = arr[(p + j * 5 + r) / 2]; // 最后一组的中间那个
} else {
InsertionSort.sort(arr, p + j * 5, p + j * 5 + 4); // 排序非最后一组的某个组
medians[indexOfMedians++] = arr[p + j * 5 + 2]; // 当前组(排序后)的中间那个
}
}
return getMedian(medians, 0, medians.length - 1);
}
4、待排序列表较短时,用插入排序
当排序列表小于8个时,通过计算发现插入排序比快速排序的性能要好。