一文搞懂快速排序

733 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第19天,点击查看活动详情

一、前言

快速排序:是由东尼·霍尔提出的一种高效的排序算法,简称快排。

可以用 3 个步骤 6 个字来概括:选基、分割、递归:

  • 选基: 首先挑选基准值
  • 分割: 分割数组,把小于基准值的元素放到基准值前面,大于基准值的元素放到基准值后面
  • 递归: 递归地对小于基准值的子序列和大于基准值的子序列进行排序

举栗,动图如下:

quickSort.gif

为什么快排会比冒泡等快?

因为其是跳跃式交换,即每次交换相隔较远的 2个元素。


快排实现方式有两种:Lomuto 分割方法 和 霍尔分割方法

  • Lomuto 分割方法:选择开头或者结尾作为基准值。(实现简单,不易出错,效率差一些)
  • 霍尔分割方法:选中间值为基准值。
  1. Lomuto 快排实现:

快排-2022-08-1012-01-52.png

快排-2022-08-1012-01-53.png

public class QuickSort {

    // Time: O(n * log(n)), Space: O(n)
    public void lomutoSort(int [] arr) {

        if (arr == null || arr.length == 0) return;
        lomutoSort(arr, 0, arr.length - 1);
    }

    private void lomutoSort(int [] arr, int low, int high) {
        if (low < high) {
            int k = lomutoPartition(arr, low, high); // 1. 选基
            // 2. 分割数组
            lomutoSort(arr, low, k - 1);   // 3. 递归处理:左侧边
            lomutoSort(arr, k + 1, high);  // 3. 递归处理:右侧边
        }
    }

    private int lomutoPartition(int[] arr, int low, int high) {
        int pivot = arr[high]; // 选择数组尾部为基准值 pivot
        int i = low;
        for (int j = low; j < high; ++j) {
            if (arr[j] < pivot) {
                swap(arr, i, high);
                ++i;
            }
        }
        swap(arr, i, high); // 最后交换基准值
        return i;
    }

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

  1. 霍尔分割快排实现(推荐)
  • 基准线:以中间点

  • i,j 两游标分别指向数组的头尾,向中间移动

    • i < privot,就向右移动
    • j > privot,就向左移动
    • i > privot || j < privot,则进行交换

快排-2022-08-1013-00-47.png

public class QuickSort {

    // Time: O(n * log(n)), Space: O(n)
    public void hoareSort(int [] arr) {

        if (arr == null || arr.length == 0) return;
        hoareSort(arr, 0, arr.length - 1);
    }

    private void hoareSort(int [] arr, int low, int high) {
        if (low < high) {
            int k = hoarePartition(arr, low, high); // 1. 选基:中间点
            // 2. 分割数组
            hoareSort(arr, low, k);       // 3. 递归:左侧边
            hoareSort(arr, k + 1, high);  // 3. 递归:右侧边
        }
    }

    private int hoarePartition(int [] arr, int low, int high) {
        int pivot = arr[low + (high - low) / 2];
        int i = low, j = high;
        while (true) {
            while (arr[i] < pivot) ++i;
            while (arr[j] > pivot) --j;
            if (i >= j) return j;
            swap(arr, i++, j--);
        }
    }
    
    private void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
}



二、题目

单链表排序(中)

leetcode 148

题干分析

这个题目说的是,给你一个单链表,你要写一个函数,对它进行排序,然后返回排序后的链表。

# 比如说,给你的单链表是:
4 -> 8 -> 2 -> 1
​
# 你要返回排序后的链表:
1 -> 2 -> 4 -> 8

思路解法

解法有二: 快排 和 归并排序。

  1. 方法一:快排
  • 选基: 首先挑选基准值
  • 分割: 分割数组,把小于基准值的元素放到基准值前面,大于基准值的元素放到基准值后面
  • 递归: 递归地对小于基准值的子序列和大于基准值的子序列进行排序

Tips:链表交换,只需要交换节点里的值即可。

快排-链表.png

// Time: O(n*log(n)), Space: O(n), Faster: 6.60%
public ListNode quickSortList(ListNode head) {
    quickSort(head, null);
    return head;
}

private void quickSort(ListNode head, ListNode end) {
    if (head == end || head.next == end) return;
    int pivot = head.val; // 1. 选择基准线:头节点
    ListNode slow = head, fast = head.next;
    while (fast != end) {
        if (fast.val <= pivot) {
            slow = slow.next;
            swap(slow, fast);
        }
        fast = fast.next;
    }
    // 2. 分割链表
    swap(head, slow);
    quickSort(head, slow);     // 3. 递归:左侧链表
    quickSort(slow.next, end); // 3. 递归:右侧链表
}

private void swap(ListNode a, ListNode b) {
    int tmp = a.val;
    a.val = b.val;
    b.val = tmp;
}