快速排序主要采用分治的思想。假设对一个数组arr[l…r]进行快速排序,采用分治思想可以分为三分:
分解:选取一个主元,假设它的索引位置为q,将arr[l…r]以q为分割点分割为两个(可能为空)子数组arr[l…q-1]和arr[q+1…r],使得arr[l…q-1]这部分的元素都小于arr[q],而arr[q+1…r]这部分的元素都大于arr[q],同时在划分的过程中也要计算出分割点q。
解决:通过递归调用快速排序,对子数组arr[l..q-1]和arr[q+1…r]进行快速排序。
合并:因为子数组都是原址排序,所以不需要合并,因为数组arr[l…r]已经有序。
下面是实现代码:
public static void quickSort1(int[] nums){
quickSort1(nums, 0, nums.length - 1);
}
private static void quickSort1(int[] nums, int start, int end) {
if (start < end){
int mid = partition(nums,start,end);
quickSort1(nums, start, mid - 1);
quickSort1(nums, mid + 1, end);
}
}
private static int partition(int[] nums, int start, int end) {
int i = start-1;
for (int j = start; j < end; j++) {
if (nums[j] < nums[end]){
i++;
swap(nums,i,j);
}
}
swap(nums,i+1,end);
return i+1;
}
// 交换元素
private static void swap(int[] nums, int i, int j){
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
优化1
我们知道快速排序在划分子数组的时候最理想的状况就是将划分出来的两个子数组的长度都相等。那我们就很容易想到遍历数组来找到一个主元使得划分的两个子数组的长度相等,但是遍历数组要消耗大量的时候,所以这样做并不会节省运行时间。那么我们能否找到一种简便的方案是我们不必要遍历数组呢?很容易想到假如我们取start,mid,end这三个位置的中间值作为主元。下面是实现代码:
// 目标使得 nums[start] < nums[end] < nums[mid]
private static void threeMedian(int[] nums, int start, int end) {
int mid = (start+end) >> 1;
// 使得 nums[start] < nums[mid]
if (nums[start] > nums[mid])
swap(nums,start,mid);
// 使得 nums[start] < nums[end]
if (nums[start] > nums[end])
swap(nums,start,end);
// 使得 nums[end] < nums[mid]
if (nums[end] > nums[mid])
swap(nums,mid,end);
}
private static void quickSort2(int[] nums, int start, int end) {
if (start < end){
// 三数取中
threeMedian(nums,start,end);
int mid = partition(nums,start,end);
quickSort2(nums, start, mid - 1);
quickSort2(nums, mid + 1, end);
}
}
优化2
为了避免划分数组出现最坏的情况(此情况出现在数组是有序数组,所以子数组划分就会出现一个子数组长度为0,另一个长度为end-start),此时快速排序的时间复杂度为O(n*n),我们在长度小于一定长度的时候使用插入排序(插入排序数组完全有序的时候时间复杂度为O(n))。下面是实现代码:
// 使用插入排序
private static void insertSort(int[] nums,int start,int end){
int j;
for (int i = start+1; i <= end; i++) {
int tmp = nums[i];
for (j = i; j > start && tmp < nums[j-1]; j--)
nums[j] = nums[j-1];
nums[j] = tmp;
}
}
private static void quickSort3(int[] nums, int start, int end) {
//当数组的长度小于10调用插入排序
if (end - start + 1 < 10){
insertSort(nums,start,end);
return;
}
if (start < end){
// 三数取中
threeMedian(nums,start,end);
int mid = partition(nums,start,end);
quickSort3(nums, start, mid - 1);
quickSort3(nums, mid + 1, end);
}
}
单链表的快速排序
首先我们来看一下partition的实现代码:
private static ListNode partition(ListNode start, ListNode end) {
// 防止划分的子链表为null
if (start == null || end == null)
return null;
int key = start.val;
ListNode p = start;
ListNode q = start.next;
while (q != null){
if (q.val < key){
p = p.next;
swap(p,q);
}
q = q.next;
}
swap(start,p);
return p;
}
和数组的快速排序的思路一样,利用主元节点将链表划分为两个部分。下面是实现代码:
public static void quickSortList(ListNode head){
if (head == null || head.next == null)
return;
ListNode tail = head;
while (tail.next != null)
tail = tail.next;
quickSortList(head,tail);
}
private static void quickSortList(ListNode start, ListNode end) {
if (start != end){
ListNode q = partition(start,end);
if (q == null)
return;
quickSortList(start,q);
quickSortList(q.next,end);
}
}
private static void swap(ListNode p, ListNode q) {
int tmp = p.val;
p.val = q.val;
q.val = tmp;
}