快速排序(二十世纪十大算法之一)
每一个轮回随机(或直接选第一个)选一个基准元素,让所有大于该基准元素移动到右边,所有小于该基准元素移动到左边,从而将数列拆分为两部分;
继续对上述两部分子数列实施这种拆分,直到不可分为止。
此种方法正是分治法的核心思想。
在每一轮的比较和交换,需要将所有元素都遍历一遍,需要的时间复杂度是N;
而这样的遍历是需要logn轮的,所以总的时间复杂度是O(nlogn)。
并且是不稳定的算法,相同的元素的顺序可能会颠覆。
需要完成整个过程,基准元素的选取和元素交换方式是重点。
基准元素的选取
1.最简单的方法是直接选取第一个元素。
但是这种方法可能会有弊端。如下所示,原本有一个逆序的数列,期望排成顺序数列,这样就不能将数列分成两半,无法发挥分治法的优势,需要进行n轮排序,时间复杂度退化成了O(n2)。
2.为了避免上述情况的发生,可以随机选取一个元素作为基准元素
但是依然可能会选取到最小的元素,发生这种极端情况,所以快排平均时间复杂度是O(nlogn),最坏的情况下是的O(n2)。
元素交换方式
1.双边循环法
这样,基准元素就将整个数列分为两部分,比基准元素大或者小。
然后对小的区域和大的区域继续做一样的操作,这里就有点递归的味道了
代码如下:
public static void quickSort(int[] arr, int startIndex, int endIndex) {//实现了分而治之的
思想
// 递归结束条件:startIndex大等于endIndex的时候
if (startIndex >= endIndex) {
return;
}
// 得到基准元素位置
int pivotIndex = partition(arr, startIndex, endIndex);
// 根据基准元素,分成两部分递归排序
quickSort(arr, startIndex, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, endIndex);
}
/**
* 分治(双边循环法)
* @param arr 待交换的数组
* @param startIndex 起始下标
* @param endIndex 结束下标
*/
private static int partition(int[] arr, int startIndex, int endIndex) {
// 取第一个位置的元素作为基准元素(也可以选择随机位置)
int pivot = arr[startIndex];
int left = startIndex;
int right = endIndex;
while( left != right) {
//控制right指针比较并左移
while(left<right && arr[right] > pivot){//找一个比pivot小的元素就停
right--;
}
//控制left指针比较并右移
while( left<right && arr[left] <= pivot) {//找一个比pivot大的元素就停
left++;
}
//交换left和right指向的元素
if(left<right) {
int p = arr[left];
arr[left] = arr[right];
arr[right] = p;
}
}
//pivot和指针重合点交换,交换逻辑遵循首尾相接
arr[startIndex] = arr[left];
arr[left] = pivot;
return left;
}
public static void main(String[] args) {
int[] arr = new int[] {4,4,6,5,3,2,8,1};
quickSort(arr, 0, arr.length-1);
System.out.println(Arrays.toString(arr));
}
里面嵌套了两个循环,代码比较繁琐。
2.单边循环法
代码如下:
/**
* 分治(单边循环法)
* @param arr 待交换的数组
* @param startIndex 起始下标
* @param endIndex 结束下标
*/
private static int partitionV2(int[] arr, int startIndex, int endIndex) {
// 取第一个位置的元素作为基准元素(也可以选择随机位置)
int pivot = arr[startIndex];
int mark = startIndex;
for(int i=startIndex+1; i<=endIndex; i++){
if(arr[i]<pivot){
mark ++;
int p = arr[mark];
arr[mark] = arr[i];
arr[i] = p;
}
}
arr[startIndex] = arr[mark];
arr[mark] = pivot;
return mark;
}
非递归实现
所有的递归都可以用栈实现,因为这两者其实逻辑都是一样的,都是基于回溯的原理实现的。然后其实递归也是需要用到栈这种数据结构,只不过是隐式使用而已,然后下面的这种是显式调用
唯一需要作出改变的就是quickSort函数部分
public static void quickSort(int[] arr, int startIndex, int endIndex) {
// 用一个集合栈来代替递归的函数栈
Stack<Map<String, Integer>> quickSortStack = new Stack<Map<String, Integer>>();
// 整个数列的起止下标,以哈希的形式入栈
Map rootParam = new HashMap();
rootParam.put("startIndex", startIndex);
rootParam.put("endIndex", endIndex);
quickSortStack.push(rootParam);
// 循环结束条件:栈为空时结束
while (!quickSortStack.isEmpty()) {
// 栈顶元素出栈,得到起止下标
Map<String, Integer> param = quickSortStack.pop();
// 得到基准元素位置
int pivotIndex = partition(arr, param.get("startIndex"), param.get("endIndex"));
// 根据基准元素分成两部分, 把每一部分的起止下标入栈
if(param.get("startIndex") < pivotIndex -1){
Map<String, Integer> leftParam = new HashMap<String, Integer>();
leftParam.put("startIndex", param.get("startIndex"));
leftParam.put("endIndex", pivotIndex -1);
quickSortStack.push(leftParam);
}
if(pivotIndex + 1 < param.get("endIndex")){
Map<String, Integer> rightParam = new HashMap<String, Integer>();
rightParam.put("startIndex", pivotIndex + 1);
rightParam.put("endIndex", param.get("endIndex"));
quickSortStack.push(rightParam);
}
}
}
最后,分析一下空间复杂度的情况
快速排序递归和非递归方法的平均空间复杂度都是O(logn)。需要借用栈空间(递归只是隐式的使用栈而已,依然是在用栈)。