面试基础篇——快速排序

简介: 面试基础篇——快速排序

快速排序


要求

算法描述

算法实现

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

算法描述

图形化展示

代码展示

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

算法描述

图形化展示

算法展示

快排特点

洛穆托分区方案 vs 霍尔分区方案

其他排序代码补充

空穴法改进的双边快排

霍尔分区

四种分区实现的移动次数比较


要求


能够用自己语言描述快速排序算法

掌握手写单边循环、双边循环代码之一

能够说明快排特点

了解洛穆托与霍尔两种分区方案的性能比较


算法描述


每一轮排序选择一个基准点(pivot)进行分区

让小于基准点的元素的进入一个分区,大于基准点的元素的进入另一个分区

当分区完成时,基准点元素的位置就是其最终位置

在子分区内重复以上过程,直至子分区元素个数少于等于 1,这体现的是分而治之的思想 (divide-and-conquer)

从以上描述可以看出,一个关键在于分区算法,常见的有洛穆托分区方案、双边循环分区方案、霍尔分区方案


算法实现


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


算法描述


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


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


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


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


图形化展示

1.png

代码展示


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) {
            if (i != j) {
                swap(a, i, j);
            }
            i++;
        }
    }
    if (i != h) {
        swap(a, h, i);
    }
    System.out.println(Arrays.toString(a) + " i=" + i);
    // 返回值代表了基准点元素所在的正确索引,用它确定下一轮分区的边界
    return i;
}
public static void swap(int[] array, int i, int j) {
        int t = array[i];
        array[i] = array[j];
        array[j] = t;
}


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


算法描述


选择最左元素作为基准点元素

j 指针负责从右向左找比基准点小的元素,i 指针负责从左向右找比基准点大的元素,一旦找到二者交换,直至 i,j 相交

最后基准点与 i(此时 i 与 j 相等)交换,i 即为分区位置

要点

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

while( i< j && a[j] > pv ) j–

while ( i< j && a[i] <= pv ) i++


图形化展示


1.png

算法展示


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[] array, int i, int j) {
        int t = array[i];
        array[i] = array[j];
        array[j] = t;
}


快排特点


1、平均时间复杂度是image.png,最坏时间复杂度image.png

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


3、属于不稳定排序


洛穆托分区方案 vs 霍尔分区方案


霍尔的移动次数平均来讲比洛穆托少3倍

https://qastack.cn/cs/11458/quicksort-partitioning-hoare-vs-lomuto


其他排序代码补充


空穴法改进的双边快排


import java.util.Arrays;
// 空穴法(双边循环法改进)
@SuppressWarnings("all")
public class QuickSort3 {
    public static void main(String[] args) {
        int[] a = {5, 3, 7, 2, 9, 8, 1, 4};
        System.out.println(Arrays.toString(a));
        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--;
            }
            if (i < j) {
                a[i] = a[j];
                i++;
            }
            // i 从左找大的
            while (i < j && a[i] <= pv) {
                i++;
            }
            if (i < j) {
                a[j] = a[i];
                j--;
            }
        }
        a[j] = pv;
        System.out.println(Arrays.toString(a) + " j=" + j);
        return j;
    }
}

霍尔分区


import java.util.Arrays;
public class QuickSortHoare {
    public static void main(String[] args) {
//        int[] a = {1,2,3};
//        int[] a = {9, 3, 2, 1};
//        int[] a = {9, 3, 7, 2, 5, 8, 1, 4};
        int[] a = {1, 2, 3, 4, 5, 7, 8, 9};
        System.out.println(Arrays.toString(a));
        quick0(a, 0, a.length - 1);
    }
    private static void quick0(int[] a, int l, int h) {
        if (l >= h) {
            return;
        }
        int p = partition(a, l, h);
        colorPrint(a, l, p, p + 1, h);
        // 注意如果左边界选择了 p-1, 则会因为返回的 p 可能不是基准点位置导致出错
        quick0(a, l, p);
        quick0(a, p + 1, h);
    }
    private static void colorPrint(int[] a, int r1l, int r1h, int r2l, int r2h) {
        System.out.print("[");
        for (int i = 0; i < a.length; i++) {
            if (i >= r1l && i <= r1h) {
                System.out.print("\033[31m" + a[i] + "\033[0m");
            } else if (i >= r2l && i <= r2h) {
                System.out.print("\033[34m" + a[i] + "\033[0m");
            } else {
                System.out.print("_");
            }
            if (i < a.length - 1) {
                System.out.print(" ");
            }
        }
        System.out.println("]");
    }
    private static int partition(int[] a, int l, int h) {
//        int pv = a[l];
        int pv = a[(l + h) >>> 1];
        System.out.println("pv=" + pv);
        int i = l - 1;
        int j = h + 1;
        while (true) {
            while (a[++i] < pv) {
            }
            while (a[--j] > pv) {
            }
            if (i >= j) {
                return j;
            }
            swap(a, i, j);
        }
    }
}
public static void swap(int[] array, int i, int j) {
        int t = array[i];
        array[i] = array[j];
        array[j] = t;
}

四种分区实现的移动次数比较


import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class LomutoVsHoare {
    public static void main(String[] args) {
        List<int[]> all1 = new ArrayList<>();
        List<int[]> all2 = new ArrayList<>();
        List<int[]> all3 = new ArrayList<>();
        List<int[]> all4 = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            int[] array = Utils.randomArray(10000);
            all1.add(array);
            all2.add(Arrays.copyOf(array, array.length));
            all3.add(Arrays.copyOf(array, array.length));
            all4.add(Arrays.copyOf(array, array.length));
        }
        System.out.println("hoarePartition");
        testPartition(all1, LomutoVsHoare::hoarePartition);
        System.out.println("LomutoPartition");
        testPartition(all2, LomutoVsHoare::LomutoPartition);
        System.out.println("otherPartition");
        testPartition(all3, LomutoVsHoare::otherPartition);
        System.out.println("moveInsteadSwapPartition");
        testPartition(all4, LomutoVsHoare::moveInsteadSwapPartition);
    }
    private static void testPartition(List<int[]> all, FourConsumer consumer) {
        List<AtomicInteger> cs = new ArrayList<>();
        for (int[] array : all) {
            AtomicInteger c = new AtomicInteger();
            consumer.apply(array, 0, array.length - 1, c);
            cs.add(c);
        }
        // 打印的是平均移动次数,而非交换次数,一次交换有3次移动
        System.out.println(cs + " avg:" + cs.stream().mapToLong(AtomicInteger::get).average().orElse(0));
    }
    interface FourConsumer {
        void apply(int[] a, int b, int c, AtomicInteger d);
    }
    private static int hoarePartition(int[] a, int l, int h, AtomicInteger c) {
//        int pv = a[l];
        int pv = a[(l + h) >>> 1];
        int i = l - 1;
        int j = h + 1;
        while (true) {
            while (a[++i] < pv) {
            }
            while (a[--j] > pv) {
            }
            if (i >= j) {
                return j;
            }
            c.accumulateAndGet(3, Integer::sum);
            swap(a, i, j);
        }
    }
    private static int LomutoPartition(int[] a, int l, int h, AtomicInteger c) {
        int pv = a[h];
        int i = l;
        for (int j = l; j < h; j++) {
            if (a[j] < pv) {
                if (i != j) {
                    c.accumulateAndGet(3, Integer::sum);
                    swap(a, i, j);
                }
                i++;
            }
        }
        if (i != h) {
            c.accumulateAndGet(3, Integer::sum);
            swap(a, h, i);
        }
        return i;
    }
    private static int otherPartition(int[] a, int l, int h, AtomicInteger c) {
        int pv = a[l];
        int i = l;
        int j = h;
        while (i < j) {
            while (i < j && a[j] > pv) {
                j--;
            }
            while (i < j && a[i] <= pv) {
                i++;
            }
            c.accumulateAndGet(3, Integer::sum);
            swap(a, i, j);
        }
        c.accumulateAndGet(3, Integer::sum);
        swap(a, l, i);
        return i;
    }
    private static int moveInsteadSwapPartition(int[] a, int l, int h, AtomicInteger c) {
        int pv = a[l];
        int i = l;
        int j = h;
        while (i < j) {
            // j 从右找小的
            while (i < j && a[j] > pv) {
                j--;
            }
            if (i < j) {
                c.incrementAndGet();
                a[i] = a[j];
                i++;
            }
            // i 从左找大的
            while (i < j && a[i] <= pv) {
                i++;
            }
            if (i < j) {
                c.incrementAndGet();
                a[j] = a[i];
                j--;
            }
        }
        c.incrementAndGet();
        a[j] = pv;
        return j;
    }
    public static void swap(int[] array, int i, int j) {
        int t = array[i];
        array[i] = array[j];
        array[j] = t;
     }
}


相关文章
|
3月前
|
机器学习/深度学习 算法 搜索推荐
|
人工智能 搜索推荐
【面试必备】——快速排序算法
【面试必备】——快速排序算法
98 0
【面试必备】——快速排序算法
|
JavaScript 前端开发 索引
手撕前端面试题(Javascript~事件委托、数组去重、合法的URL、快速排序、js中哪些操作会造成内存泄漏......
手撕前端面试题(Javascript~事件委托、数组去重、合法的URL、快速排序、js中哪些操作会造成内存泄漏......
124 0
手撕前端面试题(Javascript~事件委托、数组去重、合法的URL、快速排序、js中哪些操作会造成内存泄漏......
|
人工智能 算法 搜索推荐
【面试:基础题05:快速排序】
【面试:基础题05:快速排序】
88 0
|
算法 搜索推荐 C++
【算法】面试必备之0基础学算法 快速排序(详细讲解+私人笔记+代码展示)
二分查找又称折半查找、二分搜索、折半搜索等,是在分治算法基础上设计出来的查找算法,对应的时间复杂度为O(logn)。到这里是不是感觉很熟悉,我们前两期的算法知识,也是基于分治的方法去进行学习的,如果有这方面还不了解的朋友,你可以到我的第一篇文章(0基础学算法)里面去查看一下。
113 0
【算法】面试必备之0基础学算法 快速排序(详细讲解+私人笔记+代码展示)
|
人工智能 算法
【算法】面试必备之0基础学算法 快速排序(详细讲解+私人笔记+代码展示)
快速排序是我们在编程技术中十分常见的一种排序方式,其由于排序效率在同为O(N*logN)的几种排序方法中效率较高,所以经常会被采用,再加上处理快速排序时使用的分治法思想十分实用,这就导致了很多的软件公司(腾讯,微软等知名IT公司)在笔试面试的时候也会很喜欢考这个,还有我们在程序方面的考试(软考等)和一些比赛(PAT,蓝桥杯等)还有考研中都会经常出现他的身影。
184 0
【算法】面试必备之0基础学算法 快速排序(详细讲解+私人笔记+代码展示)
|
搜索推荐 算法
面试官:手写一个快速排序,并对其改进
快速排序算法算是所有排序算法中知名度最高的了,应用也超级广泛,正是由于其良好的性能才独得恩宠。今天就来好好的认识一下快速排序。
144 0
面试官:手写一个快速排序,并对其改进
|
搜索推荐 PHP
PHP面试题:使用PHP描述快速排序算法,对象可以是一个数组?
PHP面试题:使用PHP描述快速排序算法,对象可以是一个数组?
81 0
|
2月前
|
Java 程序员
java线程池讲解面试
java线程池讲解面试
62 1