数据结构与算法复习
之前学习过的学校的课程数据结构时有接触过快速排序,那时学习的是C语言版本,现在就以java语言版本来实现快速排序,作为再次熟悉。
注:想直接看代码往下翻,或者找目录。
算法思想
在数组中取一个数作为中间值,然后经过一趟快速扫描排序后,数组中左边的数都比中间值小,数组右边的数都比中间值大。然后对划分的左右两个子数组递归重复之前的操作,也就是再次取中间值,然后扫描一趟后中间值又把数组分为两半。就这样直到划分完成,即排序完成。
提炼一下就是:一个数组以其中一个数把数组划分为两个子数组,小的在左,大的在右。然后递归操作子数组直到没有序列需要划分,则排序结束。
最关键的三个操作:选定关键字--->划分----->递归
算法图示过程
文字可能比较苦涩难解,那就用图形来解释
个人用简单的数组来阐述一次快速排序的过程,是如何把小的数放在关键字左边,把大的数放在关键字的右边
初始数组:2、4、5、1、3
规则:
下划线指示的就是关键字(是数组中的一员);
有两个扫描指针我叫作 a,b。其中 a 指针先从左往右扫描,b指针在a指针停下来后从右往左扫描;
当a指针找到了比关键字大的数就停下来,b指针找到了比关键字小的就停下来,停下来的意义就是这个数需要调换位置了;
一旦a,b两指针到了扫描到了同一位置,则本轮扫描结束。
如图关键字为3,a指针从第一个位置开始,找到了4这个比关键字3大的数,那么就停下来;b指针就从关键字前面这个位置开始往左扫描,找到了1这个比关键字3小的数。那么就把这两个数(4和1)交换,也就是把比关键字小的数放在左边,比关键字大的数放在右边。
交换后的结果也是继续扫描的开始:
继续扫描,a指针继续往右走,然后b指针继续往左走:
此时a指针右走找到了5这个比关键字大的数,等待放置到关键字右边;b指针往左走也走到了5这个数的位置上,但是此时两个指针位置相同,b指针就没有必要继续左走了,因为左边的数已经被a指针判断完毕符合划分要求。
此时因为每次都是a指针先走,所以最后两指针相碰一定是因为a指针找到了比关键字大的数,然后b指针只是刚好走的碰上了a指针,并没有找到比关键字小的数,所以,直接让a指针走到的位置的数 与 关键字交换即可。这里就是 5 和3 交换位置。
此时本轮划分结束。自此,3的左边全是比3小的,3的右边全是比3大的如下:
这样就划分了两个子数组。这就是一趟快速排序的具体过程。接下来就是对每个子数组进行递归上方同样的过程即可得到最后的有序数组。
这里放上《数据结构》书上完整的排序过程:
那么以上还处于理论过程,java代码实现如下:
java代码更新版本(简洁一些)
public static void quickSort(int startIndex, int endIndex, int[] a){
if(startIndex >= endIndex){
return;
}
// 中间位置
int middleIndex = (startIndex + endIndex) / 2;
// 双指针位置
int startIndexPoint = startIndex;
int endIndexPoint = endIndex;
// 开始扫描与划分:从小到大排的话,划分规则就是把小的数放在左边,大的放在右边
while(startIndexPoint < endIndexPoint){
// 指针往右走,直到找到不符合划分规则的
while(a[startIndexPoint] <= a[middleIndex] && startIndexPoint < middleIndex) startIndexPoint++;
// 指针往左走,直到找到不符合划分规则的
while(a[endIndexPoint] >= a[middleIndex] && endIndexPoint > middleIndex) endIndexPoint--;
// 直接交换不符合规则的左右指针的值
int t = a[startIndexPoint];
a[startIndexPoint] = a[endIndexPoint];
a[endIndexPoint] = t;
// 若有左右指针已经到达了划分位置,那不仅值要交换,也要把划分位置进行更新
if(startIndexPoint == middleIndex){
middleIndex = endIndexPoint;
} else if(endIndexPoint == middleIndex){
middleIndex = startIndexPoint;
}
}
// 对子问题继续进行快排划分
quickSort(startIndex, middleIndex - 1, a);
quickSort(middleIndex + 1, endIndex, a);
}
java代码实现
public class SortTest {
/**
*title:main
*@param args
*/
public static void main(String[] args) {
/*
* 获得随机8个0到99之间数的数组
*/
int[] arr = new int[8];
for(int i = 0;i < 8;i++){
arr[i] = (int) (Math.random() * 99);
}
//排序前的输出
for(int a:arr){
System.out.print(a+" ");
}
System.out.println();
quickSort(arr,0,arr.length - 1);
//System.out.println("划分后关键字的位置:"+division(arr, 0, arr.length - 1));
//排序后的输出
for(int a:arr){
System.out.print(a+" ");
}
}
//=======================QUICK SORT=============================================================================
//首先对当前需要快速排序的序列进行一次快排,最后基数的左边全是小的,右边全是大的
//关键步骤:设置关键字、划分、递归
/**
* 快速扫描,划分数组
*title:division
*@param arr 要排序的数组
*@param left 左边界,边界指的是扫描指针的起始位置
*@param right 右边界
*@return 返回本轮扫描后关键字的位置,方便得到子数组的边界位置
*/
public static int division(int[] arr,int left,int right){
int point = right; //选取最右边数为关键字
/*
* 这里为了减少语句,就先设置左边扫描起点-1,右边扫描起点+1,
* 这里由于选取的是最右边的数为关键字,所以右边扫描就是从right-1开始,所以就不需要再减
*/
int leftPre = left - 1;
int rightPre = right;
//让它一直扫描,进行划分
while(true){
/*
* 从左往右扫描,比关键字小就继续扫描,若找到了比关键字大的当前
* 扫描就先停下来,等待交换。这也就是把比关键字小的放在关键字左边
*/
while(leftPre < rightPre && arr[++leftPre] < arr[point]);
/*
* 从右往左扫描,比关键字大就继续扫描,若找到了比关键字小的当前
* 扫描就先停下来,等待交换。这也就是把比关键字大的放在关键字右边
*/
while(leftPre < rightPre && arr[--rightPre] > arr[point]);
//当两个扫描停下来了,如果是leftPre >= rightPre则本轮扫描结束,否则就是需要交换位置了
if(leftPre >= rightPre){
break;
}else{
//如果是等待交换(左扫描找到了比关键字大的或右扫描找到了比关键字小的)那么就交换
int temp = arr[leftPre];
arr[leftPre] = arr[rightPre];
arr[rightPre] = temp;
}
}
/*
* 本轮扫描划分结束,那么关键字就应该出于中间位置,由于选取的是最右边的数做关键字,
* 那么和右边的比关键字大的数组的第一个位置交换即可,也就是和leftPre所在位置的值交换
*/
int temp = arr[leftPre];
arr[leftPre] = arr[point];
arr[point] = temp;
//返回本轮划分的划分点,也就是关键字交换后所在的中间位置
return leftPre;
}
/**
* 递归完成快速排序:左边界至右边界之间的数 构成子数组,进行子数组的快速排序
*title:quickSort
*@param arr 需要排序的数组
*@param left 当前排序的左边界
*@param right 当前排序的右边界
*/
public static void quickSort(int[] arr,int left,int right){
//递归划分,完成排序,递归的出口就是当left>=right时,也就时排序完成了
if(left >= right){
return;
}else{
int pointPosition = division(arr,left,right);
//对划分后关键字左边的数进行排序
quickSort(arr, left, pointPosition - 1);
//对划分后关键字右边的数进行排序
quickSort(arr, pointPosition + 1, right);
}
}
//==========================QUICK SORT 完======================================================================================
}
由于采取的是随机8个数,可以防止出现偶然性。这里贴出3次的执行结果:
第一次:
81 64 2 24 62 30 6 12
2 6 12 24 30 62 64 81
第二次:
63 62 16 31 95 97 34 94
16 31 34 62 63 94 95 97
第三次:
3 30 52 34 98 54 79 50
3 30 34 50 52 54 79 98
时间复杂度
时间复杂度与关键字的选择数值有很大的关系。如果每次选择的关键字都能均分两个子数组的长度,这样的快速排序过程就是一个完全二叉树的结构。关于二叉树后续会进行复习。那么此时的分解次数就等于完全二叉树的深度log2n(其中2是脚下标2),每次快速排序过程无论把数组怎样划分,全部的比较次数都接近与n-1次
所以最好的情况下时间复杂度是O(nlog2n);
最坏的情况,数据元素已经全部有序,这时分解次数就够构成一棵二叉退化树(单分支二叉树),所以退化的深度是n,那么最坏时间复杂度就是O(n2)--->n的平方
那么一般情况下关键字的值是随机的,所以快速排序分解过程构成一棵二叉树,深度接近log2n,所以快速排序的算法平均时间复杂度是O(nlog2n)
空间复杂度
快排需要堆栈空间临时保存递归调用参数,堆栈空间使用个数和递归调用的次数(二叉树的深度)有关,因此最好情况下空间复杂度为:O(log2n)
最坏情况下O(n),平均O(log2n)
稳定性
由稳定性规则(有序的两个数排序前位置和排序后位置是否一致。即1,2两个序列,排序后序列还是1,2则是稳定的;若变成2,1则就是不稳定的)可知,快速排序不是稳定的排序方法
后续将陆续将其它排序算法总结