排序算法及其应用

148 阅读3分钟

问题总览

类型问题描述完成
215. 数组中的第K个最大元素
912. 排序数组
148. 排序链表
315. 计算右侧小于当前元素的个数
493. 翻转对
327. 区间和的个数
剑指 Offer 51. 数组中的逆序对

快速排序

912. 排序数组

//快速排序的标准写法
class Solution {
    public int[] sortArray(int[] nums) {
        int len = nums.length;
        // 打乱数组,避免出现极端情况
        random(nums);
        // 快速排序
        sort(nums, 0, len - 1);
        return nums;
    }

    public void sort(int[] nums, int lo, int hi) {
        // 不要忘记base条件
        if (lo >= hi) {
            return;
        }
        int mid = partition(nums, lo, hi);
        // 再分别对左子树和右子树排序
        sort(nums, lo, mid - 1);
        sort(nums, mid + 1, hi);
    }

    public int partition(int[] nums, int lo, int hi) {
        int p = lo;
        int i = lo + 1;
        int j = hi;
        while (i <= j) {
            while (i < hi && nums[i] <= nums[p]) {
                i++;
            }
            while (j > lo && nums[j] > nums[p]) {
                j--;
            }
            // 已经是排好序的,不需要处理
            if (i >= j) {
                break;
            }
            swap(nums, i, j);
        }
        // 最后把作为标准的第一个元素与j互换,并返回j的位置,此时[lo,hi]内左边都是小于等于nums[p],右边都是大于等于nums[p]
        swap(nums, p, j);
        return j;
    }

    public void random(int[] nums) {
        Random random = new Random();
        int len = nums.length;
        for (int i = 0; i < len; i++) {
            int r = i + random.nextInt(len - i);
            swap(nums, i, r);
        }
    }

    public void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

快速选择算法

215. 数组中的第K个最大元素

用大顶堆或者小顶堆都可以解决,时间复杂度为O(nlogk),空间复杂度为O(k)。

class Solution {
    public int findKthLargest(int[] nums, int k) {
        PriorityQueue<Integer> queue = new PriorityQueue<>();
        for (int num : nums) {
            queue.add(num);
            if(queue.size() > k){
                queue.poll();
            }
        }
        // 第K大的
        return queue.poll();
    }
}

用快速选择算法,时间复杂度可以认为是O(n)。

快速选择算法的精髓是,partition函数每次可以确保左边的都是比自己小,不管这些元素内部是有没有排序的。这样我们可以找到第len-k个元素,这个位置后有k-1个比自己大的。

最好的情况只需要partition每次都是二分: N + N/2 + N/4 + N/8 + ... + 1 = 2N = O(N)
最坏的情况每次都要遍历所有元素:N + (N - 1) + (N - 2) + ... + 1 = O(N^2)

import java.util.PriorityQueue;
import java.util.Random;

//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
    public int findKthLargest(int[] nums, int k) {
        // 还是先打散数组
        random(nums);
        int lo = 0;
        int hi = nums.length - 1;
        int target = nums.length - k;
        while (lo <= hi) {
            int p = partition(nums, lo, hi);
            if (target == p) {
                return nums[p];
            } else if (target < p) {
                hi = p - 1;
            } else {
                lo = p + 1;
            }
        }
        // 没找到
        return -1;
    }

    public int partition(int[] nums, int lo, int hi) {
        int p = lo;
        int i = lo + 1;
        int j = hi;
        while (i <= j) {
            while (i < hi && nums[i] <= nums[p]) {
                i++;
            }
            while (j > lo && nums[j] > nums[p]) {
                j--;
            }
            // 已经是排好序的,不需要处理
            if (i >= j) {
                break;
            }
            swap(nums, i, j);
        }
        // 最后把作为标准的第一个元素与j互换,并返回j的位置,此时[lo,hi]内左边都是小于等于nums[p],右边都是大于等于nums[p]
        swap(nums, p, j);
        return j;
    }

    public void random(int[] nums) {
        Random random = new Random();
        int len = nums.length;
        for (int i = 0; i < len; i++) {
            int r = i + random.nextInt(len - i);
            swap(nums, i, r);
        }
    }

    public void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}
//leetcode submit region end(Prohibit modification and deletion)

归并排序

归并排序就是对左边和右边分别排好序,然后再将左边和右边合并。

912. 排序数组

//归并排序的标准写法
class Solution {
    int[] temp;

    public int[] sortArray(int[] nums) {
        int len = nums.length;
        temp = new int[len];
        sort(nums, 0, len - 1);
        return nums;
    }

    public void sort(int[] nums, int lo, int hi) {
        if (lo == hi) {
            return;
        }
        int mid = lo + (hi - lo) / 2;
        // 左边排序
        sort(nums, lo, mid);
        // 右边排序
        sort(nums, mid + 1, hi);
        // 重新合并排好序的两边
        merge(nums, lo, mid, hi);
    }

    public void merge(int[] nums, int lo, int mid, int hi) {
        for (int i = lo; i <= hi; i++) {
            temp[i] = nums[i];
        }

        int i = lo;
        int j = mid + 1;

        for (int k = lo; k <= hi; k++) {
            if (i == mid + 1) {
                nums[k] = temp[j++];
            } else if (j == hi + 1) {
                nums[k] = temp[i++];
            } else if (temp[i] < temp[j]) {
                nums[k] = temp[i++];
            } else {
                nums[k] = temp[j++];
            }
        }
    }
}
//leetcode submit region end(Prohibit modification and deletion)

148. 排序链表

class Solution {

    public ListNode sortList(ListNode head) {
        return sort(head, null);
    }

    public ListNode sort(ListNode head, ListNode tail) {
        if (head == null) {
            return null;
        }
        // 将tail断开,不包含tail
        if (head.next == tail) {
            head.next = null;
            return head;
        }
        // 找到链表的中点,并将其分成两段
        ListNode mid = getMid(head, tail);

        // 再分别对左子树和右子树排序
        ListNode left = sort(head, mid);
        ListNode right = sort(mid, tail);
        return merge(left, right);
    }

    // 获取中点,这里注意fast是走到tail而不是走到null
    public ListNode getMid(ListNode head, ListNode tail) {
        ListNode fast = head;
        ListNode slow = head;
        while (fast != tail && fast.next != tail) {
            fast = fast.next.next;
            slow = slow.next;
        }
        return slow;
    }

    // 合并链表
    public ListNode merge(ListNode head1, ListNode head2) {
        ListNode dummyHead = new ListNode(0);
        ListNode temp = dummyHead, temp1 = head1, temp2 = head2;
        // 这里采用分隔链表的思路,不用创建新的节点
        while (temp1 != null && temp2 != null) {
            if (temp1.val <= temp2.val) {
                temp.next = temp1;
                temp1 = temp1.next;
            } else {
                temp.next = temp2;
                temp2 = temp2.next;
            }
            temp = temp.next;
        }
        if (temp1 != null) {
            temp.next = temp1;
        } else if (temp2 != null) {
            temp.next = temp2;
        }
        return dummyHead.next;
    }
}
//leetcode submit region end(Prohibit modification and deletion)

315. 计算右侧小于当前元素的个数

class Solution {
    private class Pair {
        int val, id;

        Pair(int val, int id) {
            // 记录数组的元素值
            this.val = val;
            // 记录元素在数组中的原始索引
            this.id = id;
        }
    }

    // 归并排序所用的辅助数组
    private Pair[] temp;
    // 记录每个元素后面比自己小的元素个数
    private int[] count;

    // 主函数
    public List<Integer> countSmaller(int[] nums) {
        int n = nums.length;
        count = new int[n];
        temp = new Pair[n];
        Pair[] arr = new Pair[n];
        // 记录元素原始的索引位置,以便在 count 数组中更新结果
        for (int i = 0; i < n; i++) {
            arr[i] = new Pair(nums[i], i);
        }
        // 执行归并排序,本题结果被记录在 count 数组中
        sort(arr, 0, n - 1);

        List<Integer> res = new LinkedList<>();
        for (int c : count) {
            res.add(c);
        }
        return res;
    }

    // 归并排序
    private void sort(Pair[] arr, int lo, int hi) {
        if (lo == hi) {
            return;
        }
        int mid = lo + (hi - lo) / 2;
        sort(arr, lo, mid);
        sort(arr, mid + 1, hi);
        merge(arr, lo, mid, hi);
    }

    // 合并两个有序数组
    private void merge(Pair[] arr, int lo, int mid, int hi) {
        for (int i = lo; i <= hi; i++) {
            temp[i] = arr[i];
        }

        int i = lo, j = mid + 1;
        for (int p = lo; p <= hi; p++) {
            if (i == mid + 1) {
                arr[p] = temp[j++];
            } else if (j == hi + 1) {
                arr[p] = temp[i++];
                // 更新 count 数组
                count[arr[p].id] += j - mid - 1;
            } else if (temp[i].val > temp[j].val) {
                arr[p] = temp[j++];
            } else {
                arr[p] = temp[i++];
                // 更新 count 数组
                count[arr[p].id] += j - mid - 1;
            }
        }
    }
}

493. 翻转对

老实说,这个思路就是每次看到了死记硬背一下,自己怎么都想不到。
相同问题:剑指 Offer 51. 数组中的逆序对

class Solution {
    int count;
    int[] temp;

    public int reversePairs(int[] nums) {
        int n = nums.length;
        temp = new int[n];
        sort(nums, 0, n - 1);
        return count;
    }

    public void sort(int[] nums, int lo, int hi) {
        if (lo == hi) {
            return;
        }
        int mid = lo + (hi - lo) / 2;
        sort(nums, lo, mid);
        sort(nums, mid + 1, hi);
        merge(nums, lo, mid, hi);
    }

    public void merge(int[] nums, int lo, int mid, int hi) {
        for (int i = lo; i <= hi; i++) {
            temp[i] = nums[i];
        }

        // 对左边的所有元素,去找右边的符合翻转对的索引
        int end = mid + 1;
        for (int i = lo; i <= mid; i++) {
            while (end <= hi && (long) nums[i] > (long) 2 * nums[end]) {
                end++;
            }
            count += end - mid - 1;
        }
        int i = lo;
        int j = mid + 1;
        for (int k = lo; k <= hi; k++) {
            if (i == mid + 1) {
                nums[k] = temp[j++];
            } else if (j == hi + 1) {
                nums[k] = temp[i++];
            } else if (temp[i] > temp[j]) {
                nums[k] = temp[j++];
            } else {
                nums[k] = temp[i++];
            }
        }
    }
}

桶排序

字典序

类型问题描述完成
386. 字典序排数
440. 字典序的第K小数字
1061. 按字典序排列最小的等效字符串

386. 字典序排数

暴力解法也能过。

class Solution {
    public List<Integer> lexicalOrder(int n) {
        PriorityQueue<Integer> queue = new PriorityQueue<>((o1, o2) -> {
            return (o1 + "").compareTo(o2 + "");
        });
        for (int i = 1; i <= n; i++) {
            queue.offer(i);
        }
        List<Integer> ans = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            ans.add(queue.poll());
        }
        return ans;
    }
}

参考官方题解。

class Solution {
    public List<Integer> lexicalOrder(int n) {
        int j = 1;
        List<Integer> ans = new ArrayList<>();
        // i表示层数
        for (int i = 0; i < n; i++) {
            ans.add(j);
            if (j * 10 <= n) {
                // 还可以遍历下一层,下一层从*10开始
                j *= 10;
            } else {
                // 下一层肯定会超出n,回到最初的位置下一层继续下一个数
                while (j % 10 == 9 || j + 1 > n) {
                    j = j / 10;
                }
                j++;
            }
        }
        return ans;
    }
}

拓扑排序

类型问题描述完成
207. 课程表
210. 课程表 II