快速排序及其优化与扩展

快速排序简介:
快速排序算法是二十世纪十大算法之一,最早由Tony Hoare爵士设计。
快速排序是从冒泡排序演变而来,同冒泡排序一样快速排序也属于交换排序。
不同的是,冒泡排序在每一轮中只把一个元素冒泡到数列的一端,而快速排序采用分治法 ,在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成两部分。
下面主要分四大部分来详述快速排序的实现:
双边循环法,单边循环法,三路快排以及非递归快排
双边循环法:
双边循环法有两个函数,主函数负责递归遍历每个数,partition函数负责确定给定的数最终应位于有序序列的哪个位置。
主函数理解比较简单,这里不做细说,partition函数的执行流程大致如下:
先把要确定比较的数记为tmp,从high开始向左遍历,如果当前值比tmp大,high-1,否则把当前值赋值给low所在的位置,然后从low开始向右遍历,如果当前值比tmp小low,low+1,否则把当前值赋值给high所在的位置,一直循环下去直到high==low结束循环,最后再把low位置的值赋值为tmp即可。
以下举了一个双边循环法的例子:
在这里插入图片描述
代码实现如下:
partition函数

public int partion1(int[] array, int low, int high) {
        int tmp = array[low];
        while (low < high) {
            while ((low < high) && array[high] >= tmp) {
                high--;
            }
            if (low == high) {
                break;
            } else {
                array[low] = array[high];
            }
            while ((low < high) && array[low] <= tmp) {
                low++;
            }
            if (low == high) {
                break;
            } else {
                array[high] = array[low];
            }
        }
        array[low] = tmp;
        return low;
    }

递归函数

public void quick1(int[] array, int start, int end) {
        if (end <= start) {
            return;
        }
        int par = partion1(array, start, end);
        //找左边是否有两个数据以上
        if (par > start + 1) {
            quick1(array, start, par - 1);
        }
        //右边是否有两个数据以上
        if (par < end - 1) {
            quick1(array, par + 1, end);
        }
    }

快排优化:
在这里我们可以对tmp的取值进行以及整体算法进行优化。我们知道对于插入排序而言,它有一个性质是越有序越快,在某些情况下甚至比快速排序都快,所以我们可以在end与start的差值很小时直接用插入排序就行排序来提高性能。另一方面,在某些极端的条件下,顺序的开始取值tmp会使快排的时间复杂度降低到接近O(n^2),所以这里我们采用三数取中法确定tmp来避免这类情况的发生,优化后的代码如下:

//三数取中法(快排的一种优化)
    public void medianOfThree(int[] array, int low, int high) {
        int mid = (low + high) >> 1;
        if (array[mid] > array[low]) {
            swap(array, mid, low);
        }
        if (array[mid] > array[high]) {
            swap(array, mid, high);
        }
        if (array[low] > array[high]) {
            swap(array, low, high);
        }
    }
public void quick1(int[] array, int start, int end) {
        if (end <= start) {
            return;
        }
        //递归到小的子区间时,考虑使用插入排序(快排的一种优化)
        if (end - start + 1 <= 5) {
            insertSort(array, start, end);
            return;
        }
        medianOfThree(array, start, end);
        int par = partion1(array, start, end);
        //找左边是否有两个数据以上
        if (par > start + 1) {
            quick1(array, start, par - 1);
        }
        //右边是否有两个数据以上
        if (par < end - 1) {
            quick1(array, par + 1, end);
        }
    }

单边循环法:
单边循环法也是有两个函数,递归主函数与双边循环法一样,partition函数不一样。
partition函数执行流程:首先也是确定一个tmp,然后把初始位置标识为mark,然后从初始位置+1处开始遍历数组,如果遇到当前位置的值比tmp小,先让mark+1,然后再交换mark与当前位置的值(mark始终指向小于tmp的数中最右边的那个位置),循环结束后,先把mark位置的值赋给初始位置,然后再把tmp赋值给mark位置的值即可。
以下举了一个单边循环法的例子:
在这里插入图片描述
代码实现:
partition函数

public int partion2(int[] array, int low, int high) {
        int tmp = array[low];
        int mark = low;
        for (int i = low + 1; i <= high; i++) {
            if (array[i] < tmp) {
                mark++;
                swap(array, mark, i);
            }
        }
        array[low] = array[mark];
        array[mark] = tmp;
        return mark;
    }

递归函数:

public void quick2(int[] array, int start, int end) {
        if (end <= start) {
            return;
        }
        if (end - start + 1 <= 5) {
            insertSort(array, start, end);
            return;
        }
        medianOfThree(array, start, end);
        int par = partion2(array, start, end);
        if (par > start + 1) {
            quick2(array, start, par - 1);
        }
        if (par < end - 1) {
            quick2(array, par + 1, end);
        }
    }

单边循环法与双边循环法的思路区别:
单边循环法从头向尾只是记录并把小于tmp的值往左边移,至于比tmp大的值不做过问,当把所有比tmp小的数都放到其左边时,那么最后一个位置也就是tmp应属的位置。
双边循环法头尾同时出发向中间靠拢,遇小移左,遇大移右,最终当头尾指针在某个位置相遇时,那么此位置也就是tmp的位置。
三路快排:
三路快排顾名思义,有三个指针的快速排序,基于双边快排,这里多了一个处理==tmp的情形,也算快排的一种优化。这里的三路快排采用一个函数实现,具体流程如下:
定义tmp为起始点位置的值,定义指针lt指向起点位置,gt 指向终点位置,i指向起点的下一个位置,循环的终止条件是i大于gt,由i开始进行遍历,若当前位置的值小于tmp,交换ilt位置的值,并且两者都自增1;如果当前位置的值小于tmp,交换igt位置的值,gt自减1,i不变;如果相等,直接i自增1。
以下举了一个三路快排的例子:
在这里插入图片描述
代码实现:

public void quick3(int[] array, int low, int high) {
        if (high < low) {
            return;
        }
        int tmp = array[low];
        int lt = low;
        int gt = high;
        int i = low + 1;
        while (i <= gt) {
            if (array[i] < tmp) {
                swap(array,i,lt);
                lt++;
                i++;
            } else if (array[i] > tmp) {
                swap(array,i,gt);
                gt--;
            } else {
                i++;
            }
        }
        //lt与gt之间保存的是等于tmp的值
        quick3(array, low, lt - 1);
        quick3(array, gt + 1, high);
    }

非递归快排:
代码中一层一层的方法调用,本身就是使用了一个方法调用栈。每次进入一个新方法,就相当于入栈;每次有方法返回,就相当于出栈。所以,可以把原本的递归实现转化成一个栈的实现,在栈中存储每一次方法调用的参数。
非递归快排中partition函数与递归形式的partition函数一样,主调函数的流程思路:
每一次循环,都会让栈顶元素出栈,通过partition函数进行分治,并且按照par元素的位置分成左右两部分,左右两部分再分别入栈。当栈为空时,说明排序已经完毕,退出循环。
代码实现:
partition函数(双边快排):

public int partion(int[] array, int low, int high) {
        int tmp = array[low];
        while (low < high) {
            while ((low < high) && array[high] >= tmp) {
                high--;
            }
            while ((low < high) && array[low] <= tmp) {
                low++;
            }
            if (low < high) {
                swap(array, low, high);
            }
        }
        array[low] = tmp;
        return low;
    }
    public void swap(int[] array, int start, int end) {
        int tmp = array[start];
        array[start] = array[end];
        array[end] = tmp;
    }

主调函数:

public void quickSort(int[] array) {
        int[] stack = new int[2 * array.length];
        int top = 0;
        int low = 0;
        int high = array.length - 1;
        int par = partion(array, low, high);
        if (par > low + 1) {
            stack[top++] = low;
            stack[top++] = par - 1;
        }
        if (par < high - 1) {
            stack[top++] = par + 1;
            stack[top++] = high;
        }
        while (top > 0) {
            high = stack[top - 1];
            top--;
            low = stack[top - 1];
            top--;
            par = partion(array, low, high);
            if (par > low + 1) {
                stack[top++] = low;
                stack[top++] = par - 1;
            }
            if (par < high - 1) {
                stack[top++] = par + 1;
                stack[top++] = high;
            }
        }
    }

上面给出了另外一种双边快排的写法,但是思路和实质与第一种双边快排大同小异。
比较递归的代码会发现,非递归形式的代码复杂度和代码直观性下降不少,所以在实际写快速排序的代码时,建议使用递归形式。

发布了49 篇原创文章 · 获赞 18 · 访问量 4358

猜你喜欢

转载自blog.csdn.net/asd0356/article/details/96274415