快速排序

主要从以下几个方面讲解:

1 动图了解

2 原理讲解

3 递归的实现

4 非递归的实现

动画(双边循环法)来源与五分钟学算法公众号:

详解

同冒泡排序一样,快速排序也属于交换排序,通过元素之间的比较和交换位置来达到排序的目的。

不同的是,冒泡排序在每一轮只把一个元素冒泡到数列的一端,而快速排序在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成了两个部分。

 

这种思路就叫做分治法

每次把数列分成两部分,究竟有什么好处呢?

假如给定8个元素的数列,一般情况下冒泡排序需要比较8轮,每轮把一个元素移动到数列一端,时间复杂度是O(n^2)。

而快速排序的流程是什么样子呢?

如图所示,在分治法的思想下,原数列在每一轮被拆分成两部分,每一部分在下一轮又分别被拆分成两部分,直到不可再分为止。

这样一共需要多少轮呢?平均情况下需要logn轮,因此快速排序算法的平均时间复杂度是 O(nlogn)

基准元素的选择

基准元素,英文pivot,用于在分治过程中以此为中心,把其他元素移动到基准元素的左右两边。

那么基准元素如何选择呢?

最简单的方式是选择数列的第一个元素:

这种选择在绝大多数情况是没有问题的。但是,假如有一个原本逆序的数列,期望排序成顺序数列,那么会出现什么情况呢?

时,整个数列没有被分成两列,每一轮都只确定了基准元素的位置。

在这种情况下,快速排序需要进行n轮,时间复杂度退化为O(n^2)。

所以,虽然快速排序的平均时间复杂度是0(logn),但最坏情况下的时间复杂度我O(n^2),即上面的这种情况。

双边循环法(递归方式)

思想:

给定原始数列如下,要求从小到大排序:

我们首先选定基准元素Pivot,并且设置两个指针left和right,指向数列的最左和最右两个元素:

接下来是第一次循环,从right指针开始,把指针所指向的元素和基准元素做比较。如果大于等于pivot,则指针向移动;如果小于pivot,则right指针停止移动,切换到left指针。

在当前数列中,1<4,所以right直接停止移动,换到left指针,进行下一步行动。

轮到left指针行动,把指针所指向的元素和基准元素做比较。如果小于等于pivot,则指针向移动;如果大于pivot,则left指针停止移动。

由于left一开始指向的是基准元素,判断肯定相等,所以left右移一位。

由于7 > 4,left指针在元素7的位置停下。这时候,我们让left和right指向的元素进行交换

接下来,我们进入第二次循环,重新切换到right向左移动。right先移动到8,8>4,继续左移。由于2<4,停止在2的位置。

切换到left,6>4,停止在6的位置。

元素6和2交换。

进入第三次循环,right移动到元素3停止,left移动到元素5停止。

元素5和3交换。

进入第四次循环,right移动到元素3停止,这时候请注意,left和right指针已经重合在了一起。

当left和right指针重合之时,我们让pivot元素和left与right重合点的元素进行交换。此时数列左边的元素都小于4,数列右边的元素都大于4,这一轮交换终告结束。

代码:

/**
 *   获取基准元素
 *    注意: 一定要先从右边开始
 * @param array
 * @param startPoint
 * @param endPoint
 * @return
 */
private static int partition(int[] array, int startPoint, int endPoint) {
    // 1 获取第一个元素作为基准元素
    int baseValue = array[startPoint];
    // 2 设置左右指针
    int leftPoint = startPoint;
    int rightPoint = endPoint;
    while (leftPoint != rightPoint) {
        // 3 右指针向左移动的情况
        while (leftPoint < rightPoint && array[rightPoint] > baseValue) {
            rightPoint--;
        }
        // 4 左指针向右移动的情况
        while (leftPoint < rightPoint && array[leftPoint] <= baseValue) {
            leftPoint++;
        }
        // 5 交换left和right指针所指向的元素
        if (leftPoint < rightPoint) {
            int temp = array[leftPoint];
            array[leftPoint] = array[rightPoint];
            array[rightPoint] = temp;
        }
    }
    // 6 最终左右指针重合出,则是我们的基准元素的位置,而我们一开始,我们的基准元素就取第一个位置
    //   这里的array[leftPoint],也可以换为array[rightPoint],因为此时左右指针是重合状态
    array[startPoint] = array[leftPoint];
    array[leftPoint] = baseValue;
    return leftPoint;
}
/**
 *   递归方式实现
 * @param array
 * @param startPoint
 * @param endPoint
 */
public static void quickSore(int[] array, int startPoint, int endPoint) {
    // 1 如果开始的位置大于结束的位置,跳出循环
    if (startPoint >= endPoint) {
        return ;
    }
    // 2 获取基准元素
    int basePoint = partition(array, startPoint, endPoint);
    // 3 将基准元素左边再进行快排
    quickSore(array, startPoint, basePoint - 1);
    // 4 将基准元素右边进行快排
    quickSore(array, basePoint + 1, endPoint);
}

public static void main(String[] args) {

    int[] array = new int[]{4, 7, 6, 5, 3, 2, 8, 1};
    quickSore(array, 0, array.length - 1);
    System.out.print(Arrays.toString(array));
}

单边循环法(递归方式)

单边循环法,则主要是多了一个指针mark,用来表示小于基准元素的区域边界。

开始和双边循环相似,首先选取基准元素pivot,同时,设置一个mark指针指向数列其实位置,这个mark指针代表小于基准元素的区域边界。

从基准元素的下一个元素开始遍历:

如果遍历到的元素大于基准元素,就继续往后遍历。

如果遍历到的元素小于基准元素,则需要做两件事:

第一: 把mark指针右移1位,因为小于pivot(基准元素)的区域变大了1;

第二: 让最新遍历到的元素和mark指针所在的位置的元素交换位置,因为最新遍历的元素归属与小于pivot(基准元素)的区域。

首先遍历到元素7,7>4,所以继续遍历。

接下来遍历到的元素是3,3<4,所以mark指针右移1位。

随后,让元素3和mark指针所在位置的元素交换,因为元素3归属于小于pivot的区域。

按照这个思路,继续遍历,后续步骤如图所示。

 

代码:

/**
 * 单边循环法获取基准元素
 *
 * @param array
 * @param startPoint
 * @param endPoint
 * @return
 */
private static int partition1(int[] array, int startPoint, int endPoint) {
    // 基准元素值
    int baseValue = array[startPoint];
    // 表示小于基准元素的区域边界
    int mark = startPoint;
    for (int i = startPoint; i <= endPoint; i++) {
        // 小于基准元素,则对应代表小于基准元素的边界扩大,并交换彼此的位置
        if (array[i] < baseValue) {
            mark++;
            int temp = array[mark];
            array[mark] = array[i];
            array[i] = temp;
        }
    }
    array[startPoint] = array[mark];
    array[mark] = baseValue;
    return mark;
}

/**
 *   单边循环方式的递归方式实现
 * @param array
 * @param startPoint
 * @param endPoint
 */
public static void quickSore1(int[] array, int startPoint, int endPoint) {
    // 1 如果开始的位置大于结束的位置,跳出循环
    if (startPoint >= endPoint) {
        return ;
    }
    // 2 获取基准元素
    int basePoint = partition1(array, startPoint, endPoint);
    // 3 将基准元素左边再进行快排
    quickSore1(array, startPoint, basePoint - 1);
    // 4 将基准元素右边进行快排
    quickSore1(array, basePoint + 1, endPoint);
}

非递归方式

非递归方式,则是使用栈来代替。非递归方式代码的变动只发生在quickSort方法中。该方法引入一个存储Map类型的栈,用于存储每一次交换时的起始下标和结束下标。

每一次循环,都会让栈顶元素出栈,通过partition方法进行分治,并且按照基准元素的位置分为左右两个部分,左右两个部分再分别入栈。当栈为空时,说明排序已经完毕,退出循环。

/**
 *  非递归方式实现
 * @param arr
 * @param startIndex
 * @param endIndex
 */
public static void quickSort(int[] arr, int startIndex, int endIndex) {
    Stack<Map<String, Integer>> quickSortStack = new Stack<Map<String, Integer>>();

    // 先将首尾指针压入栈中
    Map<String, Integer> rootParam = new HashMap<>();
    rootParam.put("startIndex", startIndex);
    rootParam.put("endIndex", endIndex);
    quickSortStack.push(rootParam);

    // 循环结束条件为栈为空,则循环结束
    while (!quickSortStack.isEmpty()) {
        // 弹出栈
        Map<String, Integer> param = quickSortStack.pop();
        // 获取基准元素
        int basePoint = partition(arr, param.get("startIndex"), param.get("endIndex"));
        // 根据基准元素进行入栈,分治思想
        if (param.get("startIndex") < basePoint - 1) {
            // 入栈
            Map<String, Integer> leftParam = new HashMap<>();
            leftParam.put("startIndex", param.get("startIndex"));
            leftParam.put("endIndex", basePoint - 1);
            quickSortStack.push(leftParam);
        }

        if (param.get("endIndex") > basePoint + 1) {
            // 入栈
            Map<String, Integer> rightParam = new HashMap<>();
            rightParam.put("startIndex", basePoint+1);
            rightParam.put("endIndex", param.get("endIndex"));
            quickSortStack.push(rightParam);
        }
    }
}

/**
 * 分治(单边循环法)
 * @param arr     待交换的数组
 * @param startIndex    起始下标
 * @param endIndex    结束下标
 */
private static int partition(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;
}
public static void main(String[] args) {
    int[] arr = new int[] {4,7,6,5,3,2,8,1};
    quickSort(arr, 0, arr.length-1);
    System.out.println(Arrays.toString(arr));
}

说明: 该文章中的知识点是出自于《漫画算法》本书,本人主要是将书中知识点提炼到文章中,方便后期的回顾。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值