目录
一、基本原理
快速排序的基本原理为,选取一个元素作为参照物,遍历整个数组,将小于等于参照物的元素放在数组左边位置,将大于参照物的元素放在数组右边,中间的位置存放参照物(左边的所有元素都比右边的所有数据都要小)。然后按此方法递归地对这两部分分别进行同样的操作(分治算法),直到整个数组有序(直到所有子序列无法继续划分,即所有子序列的左边界等于其右边界)。
参照物的选取、元素的移动则是快速排序的两个关键步骤。参照物的选取一般有以下方法:1)选取区间的第一个/最后一个元素/中间元素;2)选择随机元素。元素的移动一般有以下方法:1)挖坑法;2)指针交换法。这两个步骤的目的在于,将作为参照物的元素放在排序之后应该存放的位置,并且小于等于它的元素都在它的左边,大于它的元素都在它的右边。
快速排序的时间复杂度表达式为:T(n) = T(k) + T(n-k-1) + θ(n)。在最坏情况(如采用第一个元素作为参照元素,并且数组本身逆序的时候。选择随机参照元素可以有效避免这种情况的出现)下有T(n) = T(0) + T(n-1) + θ(n) = T(n-1) + θ(n),时间复杂度为O(n^2)。在最好情况下有T(n) = 2T(n/2) + θ(n),时间复杂度为O(nlogn)。
实际使用的快排通常需要满足以下三点:1)随机选取参照元素使得最坏情况尽可能少出现;2)在数组元素较少时调用插入排序以减少递归调用次数;3)使用尾递归实现快排,从而可以做到尾调用优化。
与冒泡排序一样,快速排序也属于交换排序,通过元素之间的比较和交换位置来达到排序的目的。与之不同的是,冒泡排序每一轮将最大元素冒泡到数组的右端,而快速排序则是每一轮确定某一个元素的最终位置,该元素称为基准元素,一轮结束后数组中小于等于它的元素都在它的左边,大于它的元素都在它的右边。
快速排序和归并排序一样都是时间复杂度为O(nlogn)级别的排序,它们有什么区别呢?首先,快排更适合用于对数组排序。因为快排是本地排序(对内存限制友好)而归并排序需要O(n)的额外空间,归并排序分配和回收额外的空间的操作增加了算法的运行时间,因此虽然两者在平均情况下时间复杂度的量级相同,但是常数不同。而且,实际使用的快排不是采用默认实现,而是采用随机元素的实现,可以避免很多特殊输入(如数组逆序)下出现时间为O(n^2)的情况(注意这种情况还是可能出现),确保时间复杂度基本为O(nlogn)。其次,归并排序更适合用于对链表排序。链表不像数组,相邻的节点之间在物理内存上不相邻,链表上的插入只需要O(1)的额外空间和O(1)的时间,因此归并排序的合并操作在实现时可以不使用额外空间。链表不像数组可以随机访问(A[i]的地址=A[0]的地址+4*i),而快排需要大量的访问特定下标的元素的操作,这只能通过遍历链表来实现,因此会增加消耗的时间。归并排序则只是顺序地访问元素,不需要随机访问。
二、代码实现
1、快速排序的基本实现
public class Sort {
//快速排序(递归版本)
public static void quickSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
quickSort(arr, 0, arr.length-1);
}
//区间的快速排序(递归实现)
private static void quickSort(int[] arr, int left, int right) {
//出口:
if (left >= right) {
return;
}
//每一轮的操作:移动元素,并返回参照物索引
int pivotIndex = partition1(arr, left, right);
//子问题:采用分治算法,递归地缩小问题规模
quickSort(arr, left, pivotIndex-1);
quickSort(arr, pivotIndex+1, right);
}
//移动元素(交换指针法)
private static int partition1(int[] arr, int left, int right) {
//参照物
int pivot = arr[left];
int i = left, j = right;
while (i < j) {
//在右端找到第一个小于等于pivot的元素
//while (i<j && arr[j] > pivot){
// j--;
//}
//
while (j > i) {
if (arr[j] <= pivot) {
break;
}
j--;
}
//在左端找到第一个大于pivot的元素
while (i < j) {
if (arr[i] > pivot) {
break;
}
i++;
}
//交换以上两个元素
if (i < j) {
swap(arr, i, j);
//
//如果移动两个指针,则当相邻两个元素发生交换时,将会出错。比如6, 3, 4, 2, 1, 5, 6, 7, 8, 9
//i++;
//j--;
}
}
//i==j的重合点是参照物最终位置,因此需要交换该位置的元素和left处的参照物
swap(arr, left, i);
//System.out.println(i == j);
return i;
}
//移动元素(挖坑法)
private static int partition(int[] arr, int left, int right) {
//参照物
int pivot = arr[left];
int pivotIndex = left; //初始坑位
int i = left, j = right;
while (i <= j) {
//从右到左进行比较
while (i <= j) {
if (arr[j] <= pivot) {
arr[i++] = arr[j];
pivotIndex = j;
break;
}
j--;
}
//从左到右进行比较
while (i <= j) {
if (arr[i] > pivot) {
arr[j--] = arr[i];
pivotIndex = i;
break;
}
i++;
}
}
arr[pivotIndex] = pivot;
return pivotIndex;
}
}
2、快速排序的迭代实现
快速排序的递归实现类似于树的先根遍历(根节点-左子树-右子树),先确定完根节点(left-right区间)的一个节点的位置pivotIndex,再确定完左子树(left-pivotIndex-->1)的一个节点的位置,当左子树的所有节点都确定完毕后,再确定右子树(pivotIndex+1-->right)的一个节点的位置,直到右子树所有节点都确定完毕。而快速排序的迭代实现则是利用栈记忆左右子树的根节点,从而使得遍历完右子树之后可以找到左子树的根节点(下面的实现按照根节点-右子树-左子树的顺序遍历,当然也可以反过来)。
import java.util.Stack;
public class Sort {
//快速排序(迭代版本)
public static void quickSort1(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
quickSort1(arr, 0, arr.length-1);
}
//区间的快速排序(迭代实现)
private static void quickSort1(int[] arr, int left, int right) {
if (left >= right) {
return;
}
Stack<Integer> stack = new Stack<>();
stack.push(left);
stack.push(right);
//
/*
int[] stack = new int[right - left + 1];
int top = -1;
stack[++top] = left; //初始情况存放一对边界数据
stack[++top] = right;
*/
while (!stack.empty()) {
//
//while (top != -1) {
int high = stack.pop();
int low = stack.pop();
//
//int high = stack[top--];
//int low = stack[top--];
//int pivotIndex = partition1(arr, high, low); //参数写反导致无法排序成功
int pivotIndex = partition1(arr, low, high);
if (pivotIndex-1 > low) {
stack.push(low);
stack.push(pivotIndex - 1);
//
//stack[++top] = low;
//stack[++top] = pivotIndex - 1;
}
if (pivotIndex+1 < high) {
stack.push(pivotIndex + 1);
stack.push(high);
//
//stack[++top] = pivotIndex + 1;
//stack[++top] = high;
}
}
}
}
另一种写法:
import java.util.Stack;
import java.util.HashMap;
public class Sort {
//区间的快速排序(迭代实现的另一种写法)
private static void quickSort2(int[] arr, int left, int right) {
if (left >= right) {
return;
}
Stack<HashMap<String, Integer>> stack = new Stack<>();
HashMap<String, Integer> rootParam = new HashMap<>();
rootParam.put("startIndex", left);
rootParam.put("endIndex", right);
stack.push(rootParam); //忘了这一句会导致没有排序成功
while (!stack.isEmpty()) {
HashMap<String, Integer> param = stack.pop();
int pivotIndex = partition(arr, param.get("startIndex"), param.get("endIndex"));
//if (pivotIndex-1 > left) { //死循环
if (pivotIndex-1 > param.get("startIndex")) {
HashMap<String, Integer> leftParam = new HashMap<>();
leftParam.put("startIndex", left);
leftParam.put("endIndex", pivotIndex-1);
stack.push(leftParam); //忘了这一句会导致没有排序成功
}
//if (pivotIndex+1 < right) { //死循环
if (pivotIndex+1 < param.get("endIndex")) {
HashMap<String, Integer> rightParam = new HashMap<>();
rightParam.put("startIndex", pivotIndex+1);
rightParam.put("endIndex", right);
stack.push(rightParam); //忘了这一句会导致没有排序成功
}
}
}
}
3、划分方法的其他实现方式
划分方法的第三种实现方式:
public class Sort {
//划分方法的第三种实现
private static int partition2_1(int[] arr, int left, int right) {
//参照物
int pivot = arr[right];
//第一个子序列的右边界
int leftEnd = left - 1;
for (int i=left; i<=right-1; i++) {
if (arr[i] <= pivot) {
leftEnd++;
swap(arr, leftEnd, i);
}
}
swap(arr, leftEnd+1, right);
return leftEnd+1;
}
private static int partition2_2(int[] arr, int left, int right) {
//参照物
int pivot = arr[left];
//第二个子序列的左边界
int rightStart = right + 1; //第一个大于pivot的元素的下标,初始化为right+1表示此时还没有大于pivot的元素
for (int i=right; i>=left+1; i--) {
//if (arr[i] > pivot) {
//在数组存在重复元素时,会出现死循环
//
if (arr[i] >= pivot) {
//System.out.println("if");
rightStart--;
swap(arr, rightStart, i);
}
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// System.out.println(i +" "+ (left+1));
}
swap(arr, left, rightStart-1);
return rightStart - 1;
}
}
选择随机元素作为参照元素:
在上面的基本实现中,最坏情况一般有以下三种:本身有序、逆序、所有元素都相同(前两种的特例),在这些情况中时间复杂度降为O(n^2)。而采用随机元素作为参照元素可以将这些特殊情况的输入下的时间复杂度量级降为O(nlogn)。但是,时间复杂度为O(n^2)的最坏情况依然无法避免(比如总是选中最大元素或最小元素作为参照元素),也就是说,此种方式只能减少O(n^2)出现的概率。
public class Sort {
//划分方法的随机实现:选择随机元素作为参照元素
private static int partition3_1(int[] arr, int left, int right) {
//选择随机元素作为参照元素
Random random = new Random();
//int randomIndex = left + random.nextInt(right - left);
//Exception in thread "main" java.lang.IllegalArgumentException: bound must be positive
//
//int randomIndex = left;
//random.nextInt(0);
//Exception in thread "main" java.lang.IllegalArgumentException: bound must be positive
//
int randomIndex = (right - left == 0) ? left : left + random.nextInt(right - left);
swap(arr, randomIndex, right);
return partition2_1(arr, left, right);
}
private static int partition3_2(int[] arr, int left, int right) {
//选择随机元素作为参照元素
Random random = new Random();
int randomIndex = (right - left == 0) ? right : left + random.nextInt(right - left);
swap(arr, left, randomIndex);
//选择left作为参照元素
return partition2_2(arr, left, right);
}
}
4、稳定的快排
快排的基本实现是不稳定排序,因为快排交换元素的操作可能改变相同元素的相对次序,而没有考虑它们原先的顺序。这里实现一下稳定的快排版本。
public class Sort {
//稳定的快排
private static int partition4(int[] arr, int left, int right) {
//参照物
int pivot = arr[right];
//第一个子序列的右边界
int leftEnd = left - 1;
for (int i=left; i<=right-1; i++) {
if (arr[i] <= pivot) {
leftEnd++;
//swap(arr, leftEnd, i);
//
int temp = arr[i];
for (int j=i; j>leftEnd; j--) {
arr[j] = arr[j-1];
}
arr[leftEnd] = temp;
}
}
swap(arr, leftEnd+1, right);
return leftEnd+1;
}
}
5、三路快排
某些输入可能有很多冗余数据,比如,4, 1, 2, 4, 2, 4, 1, 2, 4, 1, 2, 2, 2, 4, 1, 4, 4, 4。快速排序的基本实现在处理重复元素时,把它放在了左部分或右部分,导致下一轮进行分区时还需检测它。如果需要排序的数组含有大量重复元素,则这个问题会造成性能浪费。三路快排将元素划分成三个部分:小于pivot、等于pivot以及大于pivot。3-way的快速排序可以基于荷兰国旗算法来实现。
public class Sort {
//三路快排
public static void quickSortThreeWay(int[] arr, int left, int right) {
//出口:
if (right <= left) {
return;
}
//划分操作:进行一轮区间的遍历
//参照物
int pivot = arr[left];
//左边子序列的右边界,右边子序列的左边界
int lt = left, gt = right;
//遍历区间
int i = left;
while (i <= gt) {
if (arr[i] < pivot) {
swap(arr, i++, lt++);
} else if (arr[i] > pivot) {
System.out.println(i +" "+ gt);
swap(arr, i, gt--);
} else {
i++;
}
}
//子问题:缩减问题规模
quickSortThreeWay(arr, left, lt - 1);
quickSortThreeWay(arr, gt + 1, right);
}
}
参考:
http://www.wanfangdata.com.cn/details/detail.do?_type=perio&id=wdzxyjsj200206002
https://baike.baidu.com/item/%E5%B0%BE%E9%80%92%E5%BD%92/554682?fr=aladdin
http://ruanyifeng.com/blog/2015/04/tail-call.html
https://www.zhihu.com/question/20761771
https://site.douban.com/196781/widget/notes/12161495/note/262014367/
http://users.monash.edu/~lloyd/tildeAlgDS/Sort/Flag/
https://blog.csdn.net/zju_sutton/article/details/8844059
https://blog.csdn.net/qq_42034068/article/details/83545458