面试:java 快速排序

**算法描述**

1. 每一轮排序选择一个基准点(pivot)进行分区
   1)让小于基准点的元素的进入一个分区,大于基准点的元素的进入另一个分区
   2)当分区完成时,基准点元素的位置就是其最终位置
2. 在子分区内重复以上过程,直至子分区元素个数少于等于 1,这体现的是分而治之的思想 ([divide-and-conquer])
3. 从以上描述可以看出,一个关键在于分区算法,常见的有洛穆托分区方案、双边循环分区方案、霍尔分区方案

**单边循环快排(lomuto 洛穆托分区方案)**

1. 选择最右元素作为基准点元素

2. j 指针负责找到比基准点小的元素,一旦找到则与 i 进行交换

3. i 指针维护小于基准点元素的边界,也是每次交换的目标索引

4. 最后基准点与 i 交换,i 即为分区位置

首先看第一轮分区的代码

    public static void main(String[] args) {
        int[] a = {5, 3, 7, 2, 9, 8, 1, 4};

        partition(a, 0, a.length-1);

    }

    private static int partition(int[] a, int l, int h) {
        int pv = a[h]; // 基准点元素
        int i = l;
        for (int j = l; j < h; j++) {
            System.out.println("i=" + i + ";j=" + j);
            System.out.println(Arrays.toString(a));
            if (a[j] < pv) {
                swap(a, i, j);
                i++;
            }
            System.out.println("i=" + i + ";j=" + j);
            System.out.println(Arrays.toString(a));
            System.out.println("------------------------");
        }
        swap(a, h, i);
        System.out.println(Arrays.toString(a));
        // 返回值代表了基准点元素所在的正确索引,用它确定下一轮分区的边界
        return i;
    }

    public static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }

控制台输出:
i=0;j=0
[5, 3, 7, 2, 9, 8, 1, 4]
i=0;j=0
[5, 3, 7, 2, 9, 8, 1, 4]
------------------------
i=0;j=1
[5, 3, 7, 2, 9, 8, 1, 4]
i=1;j=1
[3, 5, 7, 2, 9, 8, 1, 4]
------------------------
i=1;j=2
[3, 5, 7, 2, 9, 8, 1, 4]
i=1;j=2
[3, 5, 7, 2, 9, 8, 1, 4]
------------------------
i=1;j=3
[3, 5, 7, 2, 9, 8, 1, 4]
i=2;j=3
[3, 2, 7, 5, 9, 8, 1, 4]
------------------------
i=2;j=4
[3, 2, 7, 5, 9, 8, 1, 4]
i=2;j=4
[3, 2, 7, 5, 9, 8, 1, 4]
------------------------
i=2;j=5
[3, 2, 7, 5, 9, 8, 1, 4]
i=2;j=5
[3, 2, 7, 5, 9, 8, 1, 4]
------------------------
i=2;j=6
[3, 2, 7, 5, 9, 8, 1, 4]
i=3;j=6
[3, 2, 1, 5, 9, 8, 7, 4]
------------------------
[3, 2, 1, 4, 9, 8, 7, 5]

 注意 i、j 的值,以及数组的变化

一轮分区以后,数组变成了[3, 2, 1, 4, 9, 8, 7, 5],发现基准点左侧,都是比它小的,右侧都是比它大的

对基准点左侧子分区及右侧子分区,改变左右边界,继续进行分区操作,以此类推,直到区间内元素个数<=1时,就表示这个区间是有序的了

添加递归

    public static void main(String[] args) {
        int[] a = {5, 3, 7, 2, 9, 8, 1, 4};

        quick(a, 0, a.length-1);

        System.out.println(Arrays.toString(a));
    }

    public static void quick(int[] a, int l, int h) {
        if (l >= h) {
            return;
        }
        int p = partition(a, l, h); // p 索引值
        quick(a, l, p - 1); // 左边分区的范围确定
        quick(a, p + 1, h); // 左边分区的范围确定
    }

    private static int partition(int[] a, int l, int h) {
        int pv = a[h]; // 基准点元素
        int i = l;
        for (int j = l; j < h; j++) {
            if (a[j] < pv) {
                swap(a, i, j);
                i++;
            }
        }
        swap(a, h, i);
        // 返回值代表了基准点元素所在的正确索引,用它确定下一轮分区的边界
        return i;
    }

    public static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }

控制台输出:
[1, 2, 3, 4, 5, 7, 8, 9]

**双边循环快排(不完全等价于 hoare 霍尔分区方案)**

1. 选择最左元素作为基准点元素
2. j 指针负责从右向左找比基准点小的元素,i 指针负责从左向右找比基准点大的元素,一旦找到二者交换,直至 i,j 相交
3. 最后基准点与 i(此时 i 与 j 相等)交换,i 即为分区位置

首先看第一轮分区操作

    public static void main(String[] args) {
        int[] a = {5, 3, 7, 2, 9, 8, 1, 4};

        partition(a, 0, a.length-1);
    }

    private static int partition(int[] a, int l, int h) {
        int pv = a[l];
        int i = l;
        int j = h;
        while (i < j) {
            System.out.println("i=" + i + ";j=" + j);
            System.out.println(Arrays.toString(a));
            // j 从右找小的
            while (i < j && a[j] > pv) {
                j--;
            }
            // i 从左找大的
            while (i < j && a[i] <= pv) {
                i++;
            }
            System.out.println("i=" + i + ";j=" + j);
            swap(a, i, j);
            System.out.println(Arrays.toString(a));
            System.out.println("-------------------------");
        }
        swap(a, l, j);
        System.out.println(Arrays.toString(a));
        return j;
    }

    public static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }

控制台输出:
i=0;j=7
[5, 3, 7, 2, 9, 8, 1, 4]
i=2;j=7
[5, 3, 4, 2, 9, 8, 1, 7]
-------------------------
i=2;j=7
[5, 3, 4, 2, 9, 8, 1, 7]
i=4;j=6
[5, 3, 4, 2, 1, 8, 9, 7]
-------------------------
i=4;j=6
[5, 3, 4, 2, 1, 8, 9, 7]
i=4;j=4
[5, 3, 4, 2, 1, 8, 9, 7]
-------------------------
[1, 3, 4, 2, 5, 8, 9, 7]

分区操作的结果是所有的比基准点小元素的在左边,所有比基准点大元素的在右边

添加上递归

    public static void main(String[] args) {
        int[] a = {5, 3, 7, 2, 9, 8, 1, 4};

        quick(a, 0, a.length-1);
    }

    private static void quick(int[] a, int l, int h) {
        if (l >= h) {
            return;
        }
        int p = partition(a, l, h);
        quick(a, l, p - 1);
        quick(a, p + 1, h);
    }

    private static int partition(int[] a, int l, int h) {
        int pv = a[l];
        int i = l;
        int j = h;
        while (i < j) {
            // j 从右找小的
            while (i < j && a[j] > pv) {
                j--;
            }
            // i 从左找大的
            while (i < j && a[i] <= pv) {
                i++;
            }
            swap(a, i, j);
        }
        swap(a, l, j);
        System.out.println(Arrays.toString(a) + " j=" + j);
        return j;
    }

    public static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }

控制台输出:
[1, 3, 4, 2, 5, 8, 9, 7] j=4
[1, 3, 4, 2, 5, 8, 9, 7] j=0
[1, 2, 3, 4, 5, 8, 9, 7] j=2
[1, 2, 3, 4, 5, 7, 8, 9] j=6

要点

1. 基准点在左边,并且要先 j 后 i

2. while( **i** **< j** && a[j] > pv ) j-- 
3. while ( **i** **< j** && a[i] **<=** pv ) i++

疑问一,为什么 i 在从左往右,找大的的时候,需要 a[i] <= pv ,而 j 从右往左找小的的时候,不需要

a[j] > pv?

假如是这样 a[i] < pv,没有 =,执行代码,就会出现这个结果

    public static void main(String[] args) {
        int[] a = {5, 3, 7, 2, 9, 8, 1, 4};

        partition(a, 0, a.length-1);
    }

    private static int partition(int[] a, int l, int h) {
        int pv = a[l];
        int i = l;
        int j = h;
        while (i < j) {
            System.out.println("i=" + i + ";j=" + j);
            System.out.println(Arrays.toString(a));
            // j 从右找小的
            while (i < j && a[j] > pv) {
                j--;
            }
            // i 从左找大的
            while (i < j && a[i] < pv) {
                i++;
            }
            System.out.println("i=" + i + ";j=" + j);
            swap(a, i, j);
            System.out.println(Arrays.toString(a));
            System.out.println("-------------------------");
        }
        swap(a, l, j);
        System.out.println(Arrays.toString(a));
        return j;
    }

    public static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }

控制台输出:
i=0;j=7
[5, 3, 7, 2, 9, 8, 1, 4]
i=0;j=7
[4, 3, 7, 2, 9, 8, 1, 5]
-------------------------
i=0;j=7
[4, 3, 7, 2, 9, 8, 1, 5]
i=2;j=7
[4, 3, 5, 2, 9, 8, 1, 7]
-------------------------
i=2;j=7
[4, 3, 5, 2, 9, 8, 1, 7]
i=2;j=6
[4, 3, 1, 2, 9, 8, 5, 7]
-------------------------
i=2;j=6
[4, 3, 1, 2, 9, 8, 5, 7]
i=4;j=6
[4, 3, 1, 2, 5, 8, 9, 7]
-------------------------
i=4;j=6
[4, 3, 1, 2, 5, 8, 9, 7]
i=4;j=4
[4, 3, 1, 2, 5, 8, 9, 7]
-------------------------
[5, 3, 1, 2, 4, 8, 9, 7]

在第一轮分区,第一次遍历时,错误的把基准点元素交换掉了

疑问二,为什么有了外层的 while (i < j) ,内层还需要判断 i < j ?

假设没有i < j 判断,有数组[5, 1, 2, 3, 7, 8, 9] 对它就行分区操作,j 从右向左找小的,到了 2 停下,i 从左向右找大的,到7停下,交换,数组就变成了[5, 1, 7, 3, 2, 8]

i < j 判断,确保了所有的比基准点小元素的在左边,所有比基准点大元素的在右边

疑问三,目前是先运行 j ,后运行 i ,二者的顺序是够可以调换?

    public static void main(String[] args) {
        int[] a = {5, 3, 7, 2, 9, 8, 1, 4};

        partition(a, 0, a.length-1);
    }

    private static int partition(int[] a, int l, int h) {
        int pv = a[l];
        int i = l;
        int j = h;
        while (i < j) {
            System.out.println("i=" + i + ";j=" + j);
            System.out.println(Arrays.toString(a));
            // i 从左找大的
            while (i < j && a[i] <= pv) {
                i++;
            }

            // j 从右找小的
            while (i < j && a[j] > pv) {
                j--;
            }
            System.out.println("i=" + i + ";j=" + j);
            swap(a, i, j);
            System.out.println(Arrays.toString(a));
            System.out.println("-------------------------");
        }
        swap(a, l, j);
        System.out.println(Arrays.toString(a));
        return j;
    }

    public static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }

控制台输出:
i=0;j=7
[5, 3, 7, 2, 9, 8, 1, 4]
i=2;j=7
[5, 3, 4, 2, 9, 8, 1, 7]
-------------------------
i=2;j=7
[5, 3, 4, 2, 9, 8, 1, 7]
i=4;j=6
[5, 3, 4, 2, 1, 8, 9, 7]
-------------------------
i=4;j=6
[5, 3, 4, 2, 1, 8, 9, 7]
i=5;j=5
[5, 3, 4, 2, 1, 8, 9, 7]
-------------------------
[8, 3, 4, 2, 1, 5, 9, 7]

发现分区有误

**快排特点**

1. 平均时间复杂度是 $O(nlog_2⁡n )$,最坏时间复杂度 $O(n^2)$

2. 数据量较大时,优势非常明显

3. 属于不稳定排序

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值