您的位置:首页 > 其它

经典的排序算法--快速排序

2017-03-27 18:26 232 查看
快排的核心思想:确定基准,然后按照基准进行分割。递归

0.快速排序的框架

快速排序(升序)的思想就是先确定一个基准,然后通过partition函数去让数组保持小于等于和大于分别置于数组两端。然后通过递归不断对数组进行划分,最终达到有序。

例如:ABCDEFGHIJK;

如果此序列的基准为F,那么我们最终通过一次partition要达到的效果就是:ABCDE都小于等于F,GHIJK都大于F。ABCDE和GHIJK不需要保证相互有序。

紧接着,我们分别对ABCDE和GHIJK进行partition过程。递归对其分割子序列进行partition。

在递归进行回退时,所有的子序列已经有序,所以最终的序列形成了整体有序。

总的来说,快速排序是一种分治思想。

到这,很多人大体应该明白快排的基本思路了,但是一个疑问来了:如果找到基准,而且partition过程是怎样的?

partition过程是快速排序中的关键点,它有多种变体实现:

Hoare版本

Lomuto版本(算法导论版)

等等…

public class QuickSort {

public static void main(String[] args) {
QuickSort qs = new QuickSort();
int[] s = new int[] { 3, 1, 5, 7, 4, 3, 2, 1, 6 };
qs.quickSort(s, 0, s.length-1);
System.out.println(Arrays.toString(s));
}

public void sort(int[] s, int low, int high) {
if (low < high) {
int mid = partition2(s, low, high);
quickSort(s, low, mid - 1);
quickSort(s, mid + 1, high);
}
}

public void swap(int[] s, int a, int b) {
int tmp = s[a];
s[a] = s;
s[b] = tmp;
}

public int partition(int[] s, int low, int high) {
//具体实现版本见下方
}
}


[b]1.Hoare版本快速排序

Hoare版本的partition过程中可能最困惑你的是:为什么s[i] = s[j];和s[j] = s[i];可以直接赋值,那岂不是把值给覆盖了?NO,完全不会的。仔细分析你会发现,被覆盖的数其实都是有备份的,这个备份的初始动力是来源于对基准s[i]的备份,所以在每次覆盖前,我们都能找到被覆盖数的备份。此处要正确理解备份的含义。

public int partition(int[] s, int low, int high) {
int i = low, j = high;//定义两个指针,j指针指向从尾到首遍历中大于基准t的值,i指针指向从首到尾遍历中小于等于基准t的值
int t = s[i];//基准定义为t,实际选取了low位置上的值为基准
while (j > i) {
//从尾部到首部遍历,发现小于等于基准t的值则进行对s[i]的覆盖
while (s[j] > t && j > i)
j--;
s[i] = s[j];
//从首部到尾部遍历,发现大于基准t的值则进行对s[j]的覆盖
while (s[i] <= t && j > i)
i++;
s[j] = s[i];
}
s[j] = t;//此时i等于j,将基准覆盖该值
return j;
}


2.Lomuto版本快速排序(算法导论版)

Lomuto版本的partition过程则没有了覆盖,取而代之的是swap互相替换。理解Lomuto版本的partition过程最重要就是要搞懂i和j的作用。

public int partition(int[] s, int low, int high) {
int i = low;//指向小于等于基准t的‘当前j范围’内的最高位,比较拗口,细细体会
int t = s[low];
for (int j = i + 1; j <= high; j++) {//j作用就是遍历指针
if (s[j] <= t) {//遇到小于等于基准t的数,与i+1位的值进行交换,实质是与大于基准t的最低位的数进行交换
i++;
swap(s, i, j);
}
}
swap(s, low, i);//交换基准与小于等于基准的最高位
return i;
}


3.基准问题

到这,你会发现一个问题,也是我们一开始提到的问题之一,基准的选择,我们可以看到在上述的两种partition过程中,基准都是选取的数组最低位值。然而,数组可能会出现一种可怕的情况,对于升序,我们可能每次都选取到数组中的最大值,结果就是我们的快速排序退化了,退化为插入排序了。

那么我们怎样才能避免这种情况的发生呢?下面我们列举几种解决方法。

随机基准(每次基准的选择都是随机产生的,不进行人为指定)

三数取中法(选取数组首尾和中间位的值,找其中的中间值作为基准,较为靠谱,不会严重退化)

有了思路,解决起来就容易多了,只需要改变partition中基准的选择即可。我们下面给出三数取中的函数,So easy…

//三数取中基准选择函数
public static void getMid(int[] s, int low, int high) {
int mid = (low+ high) / 2;
//三数排序
if (s[low] > s[mid]) {
swap(s, low, mid);
}
if (s[low] > s[high]) {
swap(s, low, high);
}
if (s[high] < s[mid]) {
swap(s, high, mid);
}
//基准和首位值进行交换
swap(s, low, mid);
}


参考:

大神对快排的讲解

http://www.cnblogs.com/chengxiao/p/6262208.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐