一、hoare法
1、思路
Ⅰ:通过比较将第一个值找到适合的地方放,同时将将这个值的左边比他小,右边比他大
Ⅱ:再通过递归将左边的值也排有序,左边排完排右边
下面开始代码实现
第一部分:设置好递归的结束条件,获取中间值,递归左边,再递归右边
public static void quick(int[] array,int start, int end){
if(start >= end){
return;
}
//求基准,也是中兴值
int pivot = parttion(array,start,end);
//得到中间值再来递归排左树
quick(array,start,pivot-1);
//再来递归排右树
quick(array,pivot+1,end);
}
第二部分:求中间值
private static int parttion(int[] array, int left,int right){
//用index来记录left的下标(左树可以用0,但是到右树就不能用0了,所以需要一个值来记住)
int index = left;
//再定义一个tmp来记住left下标的值,方便进行比较
int tmp = array[left];
//当left >= right时表示相遇,可以退出
while(left < right){
//(为什么right要先走,left后走?)
//往左走找比tmp小的值,如果大于等于就right--
while(left < right && array[right] >= tmp){
right--;
}
//往右走找比tmp大的值,如果小于等于就left++
while(left < right && array[left] <= tmp){
left++;
}
//此时left指向的值大于tmp,right指向的值小于tmp,将他们进行交换
swap(array,left,right);
}
//此时退出循环表示left和right相遇了,再将left指向的值和index指向的值进行交换,使得index左边的值比他小,右边比他大
swap(array,index,left);
//最后返回left,也就是中间值的下标
return left;
}
//交换方法
public static void swap(int[]array, int left, int right){
int tmp = array[left];
array[left] = array[right];
array[right] = tmp;
}
这里有个小问题:为什么要右边先走而不是左边先走呢?
二、挖坑法
大体思路与hoare法差不多,也是找中间值,递归左边,再递归右边,不过找中间值的方法不一样
hoare是先找到大的,再找到小的,再交换,挖坑法是将0下标的值存入tmp,然后在右边找到比他
大的填入0下标,再从左边找到比他小的填入之前填到0下标的空
如图:
代码实现:第一部分与hoare一样
private static int parttion(int[] array, int left, int right){
//先将left位置的值存到tmp
int tmp = array[left];
while(left < right){
//往左走找比tmp小的值,如果大于等于就right--
while(left < right && array[right] >= tmp) {
right--;
}
//找到后直接将right下标的值放到left的位置
array[left] = array[right];
//往右走找比tmp大的值,如果小于等于就left++
while(left < right && array[left] <= tmp){
left++;
}
//找到后直接将left下标的值放到right下标的位置
array[right] = array[left];
}
//退出表示left 和right 相遇,将开始存的tmp的值放到left/right
array[left] = tmp;
//最后返回left(中间值下标
// .)
return left;
}
三、双指针法
图可能有点长,讲有点抽象,建议结合代码一起看
代码图:
在传参时传入了left值,就不用使用index来记录了,直接使用left就行
private static int parttion(int[] array, int left, int right){
//把left的位置给prev,也就是初始位置
int prev = left;
//cur为初始位置+1
int cur = left+1;
//判断cur是否已经超过数组的长度,也就是结束位置
while(cur <= right){
//如果cur处的值比left(初始位置)的值小
//并且prev+1位置的值不是cur,就进行交换
if(array[cur] < array[left] && array[++prev] != array[cur]) {
/**
* 比如6 1 2 7 9 3 4 5 10 8,此时的prev是6,cur是1
* cur小于left成立,执行后面的表达式++prev,也就是此时prev的值为1 ,与cur相等,结果为假,cur++
* 此时的prev是1,cur是2,一直到7cur < left 不成立 ,那么prev不会执行了,也就是不会在++
* cur会一直加到3,此时的cur < left成立,执行后面的表达式,++prev的值为7,cur的值为3,条件成立,进行交换,cur++
* 此时数组为6 1 2 3 9 7 4 5 10 8,prev指向3,cur指向4
* 继续判断4 < 6 成立,++prev为9,cur为4,成立,4和9进行交换
* 6 1 2 3 4 7 9 5 10 8一直这样继续下去直到cur>right停止
*/
//总的来说就是prev在比left下标大的值前面停下,cur在比left小的值停下,在判断prev后面的这个值是否与cur相等,相等不交换
QuickHoare.swap(array,cur,prev);
}
cur++;
}
QuickHoare.swap(array,prev,left);
return prev;
}
四:三数取中法(也可以认为是优化)
为什么要进行优化呢?
快速排序在好的情况下复杂度是O(n*log N),坏的情况就是O(N^2)层数太多,
那么三数取中是怎么做的呢?
即知道这组无序数列的首和尾后,我们便可以求出这个无序数列的中间位置的数,我们只需要在首,中,尾这三个数据中,选择一个排在中间的数据作为基准值,进行快速排序,即可进一步提高快速排序的效率(在首尾中三个数据中,找到一个中间值,然后将这个中间值放到数组的首位置,那么排序就会按照这个轴来排,就可以提高效率)
首先我们已知首(left)尾(right),那么中间值就是mid(left+right) / 2,
那么怎么取中间值呢?
六种情况 假设三个数为 l r m
那么只有 l < r < m --> l < m< r --> m < l < r (这是 l 小于 r的情况)
r < l < m --> r < m < r --> m < r < l (这是 l 大于 r 的情况)
代码实现:
public static int threeNum(int[] array, int left, int right) {
//先找到left和right的中间值
int mid = (left + right) / 2;
//再比较left和right谁的值比较大
if (array[left] > array[right]) {
//进入说明l大于f,再去判断中间值在哪
if (array[mid] > array[left]) {
//如果mid(中间值)大于left,说明mid 》 left 》 right,此时的中间值是left
return left;
} else if (array[mid] < array[right]) {
//如果mid小于right 说明left 》 right 》 mid,此时中间值为right
return right;
} else {
//两种情况都不是说明left 》 mid 》 right
return mid;
}
} else {
//如上
if (array[mid] > array[right]) {
return right;
} else if (array[mid] < array[left]) {
return left;
} else {
return mid;
}
}
三数取中的方法写完,我们就可以在快排方法中进行调用,用index来接收,再将index和首位进行交换
然后再进行快排(这里选择挖坑法进行快排)
private static void quick(int[] array, int start, int end) {
//等于表示只有一个值了,所以不用再排了·
if (start >= end) {
return;
}
//三数取中
int index = threeNum(array, start, end);
//交换
QuickHoare.swap(array, start, index);
//找基准值
int flag = prattion(array, start, end);
quick(array, start, flag - 1);
quick(array, flag + 1, end);
}
这里三数取中优化就完成了,其实还能再进行优化,插入排序是越有序越快
在快排到最后一层的时候数据太多了,那么我们可以给定一个区间,在这个区间内直接进行插入排序
private static void quick(int[] array, int start, int end) {
//等于表示只有一个值了,所以不用再排了·
if (start >= end) {
return;
}
//选择一个范围进行插入排序,因为数据太大的话底层递归太多了,越往后面数据越有序,
//左闭右闭区间 1 2 3 三个数 假设下标为0 1 2 就是2 - 0 + 1 = 3(个数)不加1就少一个
if(end - start + 1 <= 20){
insertSort(array,start,end);
return;
}
//三数取中
int index = threeNum(array, start, end);
//交换
QuickHoare.swap(array, start, index);
//找基准值
int flag = prattion(array, start, end);
quick(array, start, flag - 1);
quick(array, flag + 1, end);
}
插入排序代码:
public static void insertSort(int[] array, int left, int right) {
for (int i = left + 1; i <= right; i++) {
int tmp = array[i];
int j = i - 1;
for (; j >= left; j--) {
if (array[j] > tmp) {
array[j + 1] = array[j];
} else {
break;
}
}
array[j + 1] = tmp;
}
}
五:非递归快排
上面讲的都是使用递归来进行排序,如果数据太大的话,底层占用的内存太多,是会栈溢出的。
下面是非递归实现快排思路:
后面的就直接来实现代码:
private static void quick(int[] array){
//创建一个栈来保存左右下标
Stack<Integer> stack = new Stack<>();
//首下标为0
int start = 0;
//尾下标尾长度-1
int end = array.length-1;
//求中间值
int index = parttion(array,start,end);
//判断中间值左右是否有两个数
if(index -1 > start){
stack.push(start);
stack.push(index-1);
}
if(end > index+1){
stack.push(index+1);
stack.push(end);
}
//进入循环,判断栈是否为空
while(!stack.isEmpty()){
//弹出的元素先给右边的尾
end = stack.pop();
//再给右边的首
start = stack.pop();
//求中间值
index = parttion(array,start,end);
if(index > start+1){
stack.push(start);
stack.push(index-1);
}
if(end > index+1){
stack.push(index+1);
stack.push(end);
}
}
}
private static int parttion(int[] array, int left, int right){
//先将left位置的值存到tmp
int tmp = array[left];
while(left < right){
//往左走找比tmp小的值,如果大于等于就right--
while(left < right && array[right] >= tmp) {
right--;
}
//找到后直接将right下标的值放到left的位置
array[left] = array[right];
//往右走找比tmp大的值,如果小于等于就left++
while(left < right && array[left] <= tmp){
left++;
}
//找到后直接将left下标的值放到right下标的位置
array[right] = array[left];
}
//退出表示left 和right 相遇,将开始存的tmp的值放到left/right
array[left] = tmp;
//最后返回left(中间值下标)
return left;
}
我们也可以将三数取中优化加入到循环当中来
private static int parttion(int[] array, int left, int right){
//先将left位置的值存到tmp
int tmp = array[left];
while(left < right){
//往左走找比tmp小的值,如果大于等于就right--
while(left < right && array[right] >= tmp) {
right--;
}
//找到后直接将right下标的值放到left的位置
array[left] = array[right];
//往右走找比tmp大的值,如果小于等于就left++
while(left < right && array[left] <= tmp){
left++;
}
//找到后直接将left下标的值放到right下标的位置
array[right] = array[left];
}
//退出表示left 和right 相遇,将开始存的tmp的值放到left/right
array[left] = tmp;
//最后返回left(中间值下标)
return left;
}
/**
* 非递归快速排序
* @param array
* 思路:定义一个栈,在用start存起始位置,end存结束位置
* 再使用parttion方法求出中间值
* 再判断中间值左边是否有两个数 index-1 > start? 成立将start和index-1存入栈
* 再判断中间值右边是否有两个数 index+1 < end? 成立将index+1 和 end存入栈
* 循环判断栈是否为空
* 不为空弹出栈的值,先给end,再给start(先进后出)
* 再使用parttion方法求出中间值
* 再判断中间值左边是否有两个数 index-1 > start? 成立将start和index-1存入栈
* 再判断中间值右边是否有两个数 index+1 < end? 成立将index+1 和 end存入栈
*/
private static void quick(int[] array){
Stack<Integer> stack = new Stack<>();
int start = 0;
int end = array.length-1;
//当区间范围不大时表示已经趋于有序,可以使用插入排序
// if(end - start + 1 <= 20){
// QuickThreeNum.insertSort(array,start,end);
// }
// //使用三数取中进行优化
// int flag = QuickThreeNum.threeNum(array,start,end);
// //三数取中完将中间值与start进行交换
// QuickHoare.swap(array,0,flag);
int index = parttion(array,start,end);
if(index -1 > start){
stack.push(start);
stack.push(index-1);
}
if(end > index+1){
stack.push(index+1);
stack.push(end);
}
while(!stack.isEmpty()){
end = stack.pop();
start = stack.pop();
// if(end - start + 1 <= 20){
// QuickThreeNum.insertSort(array,start,end);
// }
// flag = QuickThreeNum.threeNum(array,start,end);
//
// QuickHoare.swap(array,0,flag);
index = parttion(array,start,end);
if(index > start+1){
stack.push(start);
stack.push(index-1);
}
if(end > index+1){
stack.push(index+1);
stack.push(end);
}
}
}