快速排序
1、算法实现
快速排序的核心方法:partition,它首先随机选择一个数,然后以这个数为轴,小于它的放在它前面,大于它的放在后面,然后放回这个轴数的排序位置。有很多种实现方法,选择我比较熟悉的一种:
private int partition(int[] nums, int begin, int end){
int pivot = nums[end];
int pivotIndex = begin - 1;
for(int i = begin; i <= end; i++){
if(nums[i] <= pivot){
pivotIndex++;
int tmp = nums[i];
nums[i] = nums[pivotIndex];
nums[pivotIndex] = tmp;
}
}
return pivotIndex;
}
然后是轴心的这个数的位置已经是正确的(左边都比它小,右边都比它大)然后利用分治的思想,将左右两边用同样的方法处理,这里有递归和非递归的版本,非递归的版本利用栈实现。
递归:
public void quickSort(int[] nums, int begin, int end){
if(nums == null || begin < 0 || end <= 0 || begin > end)
return;
int pivotIndex = partition(nums, begin, end);
if(pivotIndex > begin){
quickSort(nums, begin, pivotIndex-1);
}
if(pivotIndex < end){
quickSort(nums, pivotIndex+1, end);
}
}
非递归:
用栈模拟递归,根据后进先出的特点,先压入end再压入begin
public void quickSortStack(int[] nums, int begin, int end){
if(nums == null || begin < 0 || end <= 0 || begin > end)
return;
Stack<Integer> stack = new Stack<>();
stack.push(end);
stack.push(begin);
int beginIndex, endIndex;
while(!stack.empty()){
beginIndex = stack.pop();
endIndex = stack.pop();
if(beginIndex < endIndex){
int pivotIndex = partition(nums, beginIndex, endIndex);
if(pivotIndex > beginIndex){
stack.push(pivotIndex-1);
stack.push(beginIndex);
}
if(pivotIndex < endIndex){
stack.push(endIndex);
stack.push(pivotIndex+1);
}
}
}
}
2、复杂度分析
时间复杂度:
每一层递归的运算次数是 n
每一层递归,都将一个区间按一定比例划分成两个,所以递归的层数是 O(logn)
整体的时间复杂度是:O(nlogn)
而最差的情况是每一次都是取到最大或最小,也就是等于冒泡排序,时间复杂度是O(n^2)
空间复杂度:
空间复杂度是 O(1),没有额外申请空间
但是无论是递归方式,还是用栈的方式,都有额外的空间保存状态,所以真实的空间消耗是:
一般情况:O(logn)
最差的情况:O(n^2)
习题:
解题思路:
利用快排的思想,每一次partition,可以排对一个数的位置,且大于它的在它后面,小于它的在它前面,所以我们只需要递归地在数多的部分进行partition的操作,就能用分治的方法找出中位数。
时间复杂度:
第一次处理的数量是n,然后后面处理的数量成指数减少
递归的次数是logn
根据等比数列的运算公式,估计算出时间复杂度是 O(n)
另外的解法:
如果不能改变数组的顺序,且要求用的额外空间较少,这题可以用bitmap做
1、lintcode 80 找中位数
public class FindMedian{
public int findMedian(int[] nums){
return recursion(nums, 0, nums.length-1);
}
private int recursion(int[] nums, int begin, int end){
int pivotIndex = partition(nums, begin, end);
if(pivotIndex == (nums.length-1) / 2){
return nums[pivotIndex];
}else if(pivotIndex > (nums.length-1) / 2){
return recursion(nums, begin, pivotIndex-1);
}else{
return recursion(nums, pivotIndex+1, end);
}
}
private int partition(int[] nums, int begin, int end){
int pivot = nums[end];
int pivotIndex = begin - 1;
for(int i = begin; i < end; i++){
if(nums[i] <= pivot){
pivotIndex++;
int tmp = nums[i];
nums[i] = nums[pivotIndex];
nums[pivotIndex] = tmp;
}
}
nums[end] = nums[pivotIndex+1];
nums[pivotIndex+1] = pivot;
return pivotIndex+1;
}
public static void main(String[] args) {
int[] arr = {6, 5, 2, 3, 1, 7, 9, 10, 8, 4};
FindMedian qs = new FindMedian();
int res = qs.findMedian(arr);
System.out.println(res);
}
}
2、lintcode 5 第k大元素
解题思路:
跟找中位数类似,只是要找位置为K的数,我们只要在每次划分的两个区间中,含有K的那个区间去做partition就可以了。
时间复杂度是 O(n)
另外的解法:
如果K值比较小,或者要找出前k大的所有数,可以维护一个小顶堆,然后遍历数组 : 海量数据编程常见问题及编程题
public class KthLargestElement {
public int kthLargestElement(int k, int[] nums){
return recursion(nums, 0, nums.length-1, nums.length+1-k);
}
private int recursion(int[] nums, int begin, int end, int k){
int pivotIndex = partition(nums, begin, end);
if(pivotIndex == k-1){
return nums[pivotIndex];
}else if(pivotIndex > k-1){
return recursion(nums, begin, pivotIndex-1, k);
}else{
return recursion(nums, pivotIndex+1, end, k);
}
}
private int partition(int[] nums, int begin, int end){
int pivot = nums[end];
int pivotIndex = begin - 1;
for(int i = begin; i <= end; i++){
if(nums[i] <= pivot){
pivotIndex++;
int tmp = nums[i];
nums[i] = nums[pivotIndex];
nums[pivotIndex] = tmp;
}
}
return pivotIndex;
}
public static void main(String[] args) {
int[] arr = {6, 4, 2, 5, 3, 1};
KthLargestElement ke = new KthLargestElement();
int res = ke.kthLargestElement(2, arr);
System.out.println(res);
}
}