算法 - 排序算法

1 快速排序

  • 平均时间复杂度 O(nlogn),具体时间复杂度取决于递归时数组的分割比例,数组的分割比例取决于选择的基准 pivot
  • 不稳定,相同值会产生交换
  • 在大多数情况下都是适用的,尤其在数据量大的时候性能优越性更加明显

快速排序的退化和优化

  • 两种退化的情况: 根本原因还是递归时数组分割的比例
    1. 数据正序或倒序时,每次选择首部的元素,退化成冒泡排序 O(n2)
      • 可以先用O(n)的时间复杂度检测是否有序
      • 也可以三数取中法,或者随机选取 pivot
    2. 大量重复数据
      • 每次划分为三个部分:大于、小于、等于 pivot 的三个部分,仅递归排序大于和小于 pivot 的两部分
  • 优化
    1. 每次随机选取 privot
    2. 三数取中法选取 privot,在大多数情况下都能较好地平衡划分(最坏的情况是当选定的三个元素恰好是最小、最大或完全相同,概率很低)
    3. 对于小规模子数组采用其他排序算法,如插入排序或选择排序等
    private void binarySort(int start, int end, int[] nums) {
        if (start >= end) {
            return;
        }
        int flag = nums[start];
        int l = start;
        int r = end;
        while (l < r) {
            while (l < r && nums[r] >= flag) {
                r--;
            }
            if (l >= r) {
                break;
            } else {
                nums[l] = nums[r];
            }
            while (l < r && nums[l] <= flag) {
                l++;
            }
            if (l >= r) {
                break;
            } else {
                nums[r] = nums[l];
            }
        }
        nums[l] = flag;
        binarySort(start, l - 1, nums);
        binarySort(l + 1, end, nums);
    }

2 堆排序

  • 时间复杂度 O(nlogn)
  • 把最大堆堆顶的最大数取出,将剩余的堆继续调整为最大堆,再次将堆顶的最大数取出,这个过程持续到剩余数只有一个时结束
  • 不稳定
  • 建立堆和调整堆的过程中会产生比较大的开销,在元素太少的时候并不适用
// 例题:寻找最小的K个数
public int[] getLeastNumbers(int[] arr, int k) {
    // 维护一个大小为k的最大堆,遍历数组即可
    int[] heap = new int[k + 1];
    // 堆不满,持续插入元素
    int numsAdd = 0;
    for (int i = 0; i < k; i++) {
        heap[i + 1] = arr[i];
        numsAdd++;
        int curr = numsAdd;
        // 新值入堆
        while (curr / 2 > 0 && heap[curr / 2] < heap[curr]) {
            int temp = heap[curr / 2];
            heap[curr / 2] = heap[curr];
            heap[curr] = temp;
            curr = curr / 2;
        }
    }
    // 堆满,需要有元素的删除和添加
    for (int i = k; i < arr.length; i++) {
        if (arr[i] < heap[1]) {
            // 旧值出堆,堆末尾的值移动到首位,然后不断下沉
            heap[1] = heap[k];
            heap[k] = -1;
            int curr = 1;
            int l = 2;
            int r = 3;
            while ((l <= k && heap[curr] < heap[l]) || (r <= k && heap[curr] < heap[r])) {
                if (r <= k && heap[l] < heap[r]) {
                    int temp = heap[curr];
                    heap[curr] = heap[r];
                    heap[r] = temp;
                    curr = r;
                } else {
                    int temp = heap[curr];
                    heap[curr] = heap[l];
                    heap[l] = temp;
                    curr = l;
                }
                l = 2 * curr;
                r = l + 1;
            }
            // 新值入堆,新值加入到堆末尾,不断上浮
            heap[k] = arr[i];
            curr = k;
            while (curr / 2 > 0 && heap[curr / 2] < heap[curr]) {
                int temp = heap[curr / 2];
                heap[curr / 2] = heap[curr];
                heap[curr] = temp;
                curr = curr / 2;
            }
        }
    }
    return Arrays.copyOfRange(heap, 1, k + 1);
}

3.冒泡排序

  • 时间复杂度O(n2)
  • 完全反序时交换次数最多
  • 在相邻元素相等时,它们并不会交换位置,所以冒泡排序是稳定排序
def bubble(self, array, length):
    for i in range(length - 1, -1, -1):  # 外层循环:每次需要排序的长度
        for j in range(i):  # 内层循环:从第一个元素到第i个元素
            if array[j] > array[j + 1]:
                pre, post = array[j + 1], array[j]
                array[j + 1], array[j] = post, pre
    return array

4.选择排序

  • 时间复杂度O(n2)
  • 和冒泡排序有一定的相似度,可以认为选择排序是冒泡排序的一种改进
  • 不稳定的排序算法
def select(self, array, length):
    for i in range(length - 1):  # 外层循环的i代表本次循环需要填的位置
        min_val = array[i]
        min_idx = i
        
        for j in range(i + 1, length):
            if min_val > array[j]:
                min_val = array[j]
                min_idx = j

        array[min_idx] = array[i]
        array[i] = min_val

    return array

5.插入排序

  • 时间复杂度O(n2)
  • 把待排序的数组分成已排序和未排序两部分,初始的时候把第一个元素认为是已排好序的。从第二个元素开始,在已排好序的子数组中寻找到该元素合适的位置并插入该位置
def insert(self, array, length):
    for i in range(1, length):  # 外层循环的i代表当前要处理的位置,i之前已经全部有序
        val = array[i]
        ptr = i
        while (ptr > 0 and array[ptr - 1] > val):  # 内层循环寻找插入位置
            array[ptr] = array[ptr - 1]
            ptr -= 1
        array[ptr] = val
        
    return array

6.归并排序【数组中逆序对的个数、合并K个升序链表、链表排序】

  • 例题:数组中逆序对的个数
    • 问题的关键在于合并两个数组时,每当右侧数组出一个元素,且左侧没有走完时,增加逆序对个数
    • 画图解题
class Solution {

    int reverseCount = 0;

    public int reversePairs(int[] nums) {
        if (nums.length <= 1) {
            return reverseCount;
        }
        mergeSortAndCount(nums, 0, nums.length - 1);
        return reverseCount;
    }

    private void mergeSortAndCount(int[] nums, int start, int end) {
        if (start >= end) {
            return;
        }
        // 分治
        int mid = (start + end) / 2;
        mergeSortAndCount(nums, start, mid);
        mergeSortAndCount(nums, mid + 1, end);
        // 合并
        int[] copy = Arrays.copyOfRange(nums, start, end + 1);
        int l = 0;
        int r = mid - start + 1;
        for (int i = start; i <= end; i++) {
            // 1.左侧已经走完
            if (l == mid - start + 1) {
                nums[i] = copy[r];
                r++;
            } else if (r == copy.length) {  // 2.右侧已经走完
                nums[i] = copy[l];
                l++;
            } else {  // 3.两侧都没有走完,需要比较大小
                if (copy[l] <= copy[r]) {
                    nums[i] = copy[l];
                    l++;
                } else {
                    // 出现逆序
                    nums[i] = copy[r];
                    r++;
                    reverseCount += (mid - start - l + 1);
                }
            }
        }
    }
}
  • 例题:合并K个升序链表
    • 每次比较时,用大小为K的小根堆,取堆顶的节点
class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        ListNode head = new ListNode(-1);
        ListNode curr = head;
        Queue<ListNode> pq = new PriorityQueue<>((a, b) -> a.val - b.val);  // 升序
        for (int i = 0; i < lists.length; i++) {
            if (lists[i] != null) {
                pq.offer(lists[i]);
            }
        }
        while (pq.size() > 0) {
            // 获取归并排序最小值
            ListNode node = pq.poll();
            curr.next = node;
            curr = node;
            // 下移
            node = node.next;
            // 将新节点加入堆
            if (node != null) {
                pq.offer(node);
            }
        }
        return head.next;
    }
}
  • 例题:排序链表
    • 添加链接描述
    • 快慢指针寻找链表中点,左右递归执行排序
    • 合并排序结果
      在这里插入图片描述
class Solution {
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode fast = head;
        ListNode slow = head;
        ListNode slowPrev = slow;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slowPrev = slow;
            slow = slow.next;
        }
        // 划分并排序
        slowPrev.next = null;
        ListNode l = sortList(head);
        ListNode r = sortList(slow);
        // 合并
        ListNode dummy = new ListNode(-1);
        ListNode curr = dummy;
        while (l != null && r != null) {
            if (l.val <= r.val) {
                curr.next = l;
                curr = l;
                l = l.next;
            } else {
                curr.next = r;
                curr = r;
                r = r.next;
            }
        }
        curr.next = (l == null) ? r : l;
        return dummy.next;
    }
}

7.定制排序【合并区间、把数组排成最小的数】

  • 对排序容器(例如 TreeSet, TreeMap)的灵活运用
  • 例题:合并区间
    • 将每个 (from, to) 包装成 Turple 对象,放在 TreeSet 里,先按 from 后按 to 排序
    • 遍历 TreeSet 执行合并
      在这里插入图片描述
class Solution {
    public int[][] merge(int[][] intervals) {
        // 定制排序
        Set<Turple> treeSet = new TreeSet<>((a, b) -> {
            if (a.from == b.from) {
                return a.to - b.to;
            }
            return a.from - b.from;
        });
        for (int i = 0; i < intervals.length; i++) {
            treeSet.add(new Turple(intervals[i][0], intervals[i][1]));
        }
        // 遍历Set并合并区间
        List<Turple> resultList = new LinkedList<>();
        Iterator<Turple> iter = treeSet.iterator();
        int currFrom = -1;
        int currTo = -1;
        while (iter.hasNext()) {
            Turple t = iter.next();
            // 情况1:能续上
            if (t.from == currFrom || currTo >= t.from) {
                currTo = Math.max(t.to, currTo);
            } else {  // 情况2:不能续上
                if (currFrom != -1) {
                    resultList.add(new Turple(currFrom, currTo));
                }
                currFrom = t.from;
                currTo = t.to;
            }
        }
        resultList.add(new Turple(currFrom, currTo));
        // 结果转换
        int[][] resultArr = new int[resultList.size()][2];
        for (int i = 0; i < resultList.size(); i++) {
            resultArr[i][0] = resultList.get(i).from;
            resultArr[i][1] = resultList.get(i).to;
        }
        return resultArr;
    }

    class Turple {
        int from;
        int to;

        public Turple(int from, int to) {
            this.from = from;
            this.to = to;
        }
    }
}
  • 例题:把数组排成最小的数
    • 定制排序,对于两数 a 和 b,比较 ab 和 ba 的大小,选择较小的排序方法
      在这里插入图片描述
class Solution {
    public String minNumber(int[] nums) {
        int length = nums.length;
        if (length == 0) {
            return "0";
        }
        List<Integer> list = Arrays.stream(nums).boxed().sorted((a, b) -> {
            // 定制排序判断 ab 和 ba 哪个大
            String aa = a.toString();
            String bb = b.toString();
            String ab = aa + bb;
            String ba = bb + aa;
            for (int i = 0; i < aa.length() + bb.length(); i++) {
                if (ab.charAt(i) > ba.charAt(i)) {
                    return 1;
                } else if (ab.charAt(i) < ba.charAt(i)) {
                    return -1;
                }
            }
            return 0;
        }).collect(Collectors.toList());
        
        // 输出结果
        StringBuilder sb = new StringBuilder();
        for (Integer num : list) {
            sb.append(num);
        }
        return sb.toString();
    }
}

8.拓扑排序【循环依赖问题】

课程表
在这里插入图片描述

class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        // 拓扑排序做法
        int[] inDegree = new int[numCourses];
        Set<Integer> validCourses = new HashSet<>();
        // 统计节点入度
        for (int[] turple : prerequisites) {
            inDegree[turple[0]]++;
        }
        Queue<Integer> course0InDegree = new LinkedList<>();
        // 初始化队列
        for (int i = 0; i < numCourses; i++) {
            if (inDegree[i] == 0) {
                course0InDegree.offer(i);
            }
        }
        while (course0InDegree.size() > 0) {
            int currCourse = course0InDegree.poll();
            if (validCourses.contains(currCourse)) {
                continue;
            }
            // 添加结果
            validCourses.add(currCourse);
            // 更新入度
            for (int[] turple : prerequisites) {
                if (turple[1] == currCourse) {
                    inDegree[turple[0]]--;
                    if (inDegree[turple[0]] == 0) {
                        course0InDegree.add(turple[0]);
                    }
                }
            }
        }
        return validCourses.size() == numCourses;
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值