11、【排序】快速排序(1)

2021-09-07  本文已影响0人  yscyber

1、概述

2、思想

[<pivot]\,\,\,pivot\,\,\,[>pivot](升序)
[>pivot]\,\,\,pivot\,\,\,[<pivot](降序)

上述这个过程,有的地方也称之为 partition,中文含义是“划分”。

经过一次 partition 后,然后再对左右两侧的数据(上面公式所示的[\,]中的部分)再进行 partition 操作。所以快速排序算法的实现也是递归

每经过一次 partition 操作,最后 pivot 在整个数据中的位置就是确定的了。

3、动画等演示

快速排序-动画演示-1

partition 的具体图解:

规定:pivot 为当前区间的第一个元素,比 pivot 小的元素最终将被放置在 pivot 的左侧,比 pivot 大的元素最终将被放置在 pivot 的右侧。最终实现升序排序

需要注意的是,partition 的操作是“就地”操作,“异地”操作 partition 是十分容易的,比如创建两个辅助空间leftright,遍历某一区间,比 pivot 小的依次放入left,比 pivot 大的依次放入right,最后left、pivot、right合并即可。但是为了提高效率没有必要这么做。

既然 partition 是“就地”操作,那么就使用需要一些索引变量。

2、图中 v 即 pivot,橙色部分为小于 pivot 的元素,紫色部分为大于 pivot 的元素,e 为当前遍历到的元素。
注意:pivot 的位置调整(到两部分中间)需要等全部遍历完毕后。图示的状态为没有全部遍历完的状态

快速排序-partition基本版-图解-1

3、当 e 大于 pivot 的时候,e 直接“并入”紫色部分,“并入”的实现将依靠的是索引变量的变化,然后继续遍历。


快速排序-partition基本版-图解-2

4、当 e 小于 pivot 的时候,e 需要“并入”橙色部分,通过“交换”操作(和紫色部分的第一个元素交换,相应的索引变量发生变化),然后继续遍历。


快速排序-partition基本版-图解-3

5、当前区间全部遍历完成的样子。


快速排序-partition基本版-图解-4

6、调整 pivot 的位置。


快速排序-partition基本版-图解-5

4、Java 代码实现

4.1、partition 实现 —— 最基础的实现

l:区间的左边界索引,arr[l]为 pivot

r:区间的右边界索引

cur:当前遍历到的元素的索引,初始值为l+1即 pivot 的下一个元素

bound:小于 pivot 部分的最后一个元素的索引(升序情况;降序为大于 pivot 部分的最后一个元素的索引),初始值为l

最终形成的局面:[l+1,bound]为左侧区间,[bound+1,cur-1]为右侧区间

如下图所示:

快速排序-v1-升序情况索引变量 快速排序-v1-降序情况索引变量

通过上图,得到一些 partition 方法中的基本逻辑:

  • 升序,如果arr[cur] < pivot,需并入黄色部分,先bound+1,然后将arr[bound]arr[cur]交换即可,最后cur+1

  • 升序,如果arr[cur] > pivot,需并入紫色部分,只需cur+1即可。

  • 降序,如果arr[cur] > pivot,需并入黄色部分,先bound+1,然后将arr[bound]arr[cur]交换即可,最后cur+1

  • 降序,如果arr[cur] < pivot,需并入紫色部分,只需cur+1即可。

public class Main {

    public static void main(String[] args) {
        // 随机生成一个长度为 10,范围是 [0,99] 的数组
        int[] arr = RandomArray.generateIntegerArray(10, 0, 99);
        System.out.println(arrayToString(arr));

        int pivotIndex = partition(arr, 0, arr.length - 1, true);

        System.out.println(pivotIndex);
        System.out.println(arrayToString(arr));
    }

    private static int partition(int[] arr, int l, int r, boolean isAscending) {
        // 忽略一些校验

        int bound = l;
        int pivot = arr[l];

        // partition
        for (int cur = l + 1; cur <= r; cur++) {
            if (isAscending) {
                // 升序,小于 pivot 交换;大于或等于 pivot 直接 cur++ 即可
                if (arr[cur] < pivot) {
                    bound++;
                    swap(arr, bound, cur);
                }
            } else {
                // 降序,大于 pivot 交换;小于或等于 pivot 直接 cur++ 即可
                if (arr[cur] > pivot) {
                    bound++;
                    swap(arr, bound, cur);
                }
            }
        }

        // 将 pivot (arr[l]) 调整到左右两侧区间的中间
        swap(arr, l, bound);

        // 返回 pivot 最新的索引
        return bound;
    }

    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    private static String arrayToString(int[] arr) {
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < arr.length; i++) {
            sb.append(arr[i]);
            if (i != arr.length - 1) {
                sb.append(", ");
            }
        }
        sb.append("]");
        return sb.toString();
    }

}

4.2、快速排序实现 —— 最基础的实现

/**
 * 快速排序
 *
 * 忽略一些校验
 */
public class QuickSort {

    /**
     * 该方法对外
     */
    public static void quickSort(int[] arr, boolean isAscending) {
        quickSort(arr, 0, arr.length - 1, isAscending);
    }

    /**
     * 递归
     */
    private static void quickSort(int[] arr, int l, int r, boolean isAscending) {
        if (l >= r) {
            return;
        }

        // 经过一次 partition 数组形态:[l,pivotIndex-1] pivotIndex [pivotIndex + 1,r]
        int pivotIndex = partition(arr, l, r, isAscending);

        quickSort(arr, l, pivotIndex - 1, isAscending);

        quickSort(arr, pivotIndex + 1, r, isAscending);
    }

    private static int partition(int[] arr, int l, int r, boolean isAscending) {
        int pivot = arr[l];
        int bound = l;

        for (int cur = l + 1; cur <= r; cur++) {
            if (isAscending) {
                if (arr[cur] < pivot) {
                    bound++;
                    swap(arr, cur, bound);
                }
            } else {
                if (arr[cur] > pivot) {
                    bound++;
                    swap(arr, cur, bound);
                }
            }
        }

        swap(arr, l, bound);

        return bound;
    }

    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

}
public class Main {

    public static void main(String[] args) {
        int[] arr = RandomArray.generateIntegerArray(8, 1, 50);
        RandomArray.printArray(arr);

        QuickSort.quickSort(arr, true);
        RandomArray.printArray(arr);
    }

}
public class QuickSort {

    public static <E extends Comparable<E>> void quickSort(E[] arr, boolean isAscending) {
        quickSort(arr, 0, arr.length - 1, isAscending);
    }

    private static <E extends Comparable<E>> void quickSort(E[] arr, int l, int r, boolean isAscending) {
        if (l >= r) {
            return;
        }
        int pivotIndex = partition(arr, l, r, isAscending);
        quickSort(arr, l, pivotIndex - 1, isAscending);
        quickSort(arr, pivotIndex + 1, r, isAscending);
    }

    private static <E extends Comparable<E>> int partition(E[] arr, int l, int r, boolean isAscending) {
        E pivot = arr[l];
        int bound = l;

        for (int cur = l + 1; cur <= r; cur++) {
            if (isAscending) {
                if (arr[cur].compareTo(pivot) < 0) {
                    bound++;
                    swap(arr, cur, bound);
                }
            } else {
                if (arr[cur].compareTo(pivot) > 0) {
                    bound++;
                    swap(arr, cur, bound);
                }
            }
        }

        swap(arr, l, bound);

        return bound;
    }

    private static <E> void swap(E[] arr, int i, int j) {
        E temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

}

5、优化

5.1、快速排序基础版面对已经有序的数据

快速排序-v1-面对有序数据出现的问题

一旦数据有序,每一次partition方法就会变成纯遍历操作,此时的时间复杂度已经是O(n^2)级别的了;另外这种情况会导致递归次数(递归深度)的增加,面对数据量大的情况,容易出现“栈溢出”。

import java.util.Random;

public class QuickSort {

    private static Random random = new Random();

    public static <E extends Comparable<E>> void quickSort(E[] arr, boolean isAscending) {
        quickSort(arr, 0, arr.length - 1, isAscending);
    }

    private static <E extends Comparable<E>> void quickSort(E[] arr, int l, int r, boolean isAscending) {
        if (l >= r) {
            return;
        }
        int pivotIndex = partition(arr, l, r, isAscending);
        quickSort(arr, l, pivotIndex - 1, isAscending);
        quickSort(arr, pivotIndex + 1, r, isAscending);
    }

    private static <E extends Comparable<E>> int partition(E[] arr, int l, int r, boolean isAscending) {
        // 随机选择一个 pivot
        int pivotInitIndex = getRandomInteger(l, r);
        E pivot = arr[pivotInitIndex];

        int bound = l;

        // 将选定的 pivot 调整至当前区间的首位,保证后续的代码逻辑与之前编写的 partition 是一致的!
        swap(arr, l, pivotInitIndex);

        for (int cur = l + 1; cur <= r; cur++) {
            if (isAscending) {
                if (arr[cur].compareTo(pivot) < 0) {
                    bound++;
                    swap(arr, cur, bound);
                }
            } else {
                if (arr[cur].compareTo(pivot) > 0) {
                    bound++;
                    swap(arr, cur, bound);
                }
            }
        }

        swap(arr, l, bound);

        return bound;
    }

    private static <E> void swap(E[] arr, int i, int j) {
        E temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    private static int getRandomInteger(int min, int max) {
        // random.nextInt(max - min + 1) -> [0,max-min+1) -> [0,max-min]
        // min + random.nextInt(max - min + 1) -> [min,max+1) -> [min,max]
        return min + random.nextInt(max - min + 1);
    }

}

较一直选择首个元素的策略,这个策略是较好的。但是,这个策略较容易被“击破”,因为可以根据这个策略编写出“数据生成器”,这个“生成器”是能够生成使快速排序“退化”的一组数据。
如果是随机选择 pivot,是很难被“破解”的。

上一篇 下一篇

猜你喜欢

热点阅读