148. 排序链表

一般都用归并排序,因为是单向链表,其它排序算法根据下标找元素,向前遍历等都比较困难

主函数流程是:

  • 如果 head==null || head.next==null return head。因为 head.next == null 即只有一个元素时,不用再划分了,而且一个元素本身也是有序的,所以返回就返回这一个元素
  • 通过找链表中点算法找到 midNode
  • 左半数组是 head~midNode,右半数组是 midNode.next~结尾null 。因此令 rightListHead=midNode.next。此外为了使坐半链表有正确的边界,结尾指向null,把链表从中间分割开来,令 midNode.next = null
  • 左半递归 rightHead = sortList(head) 右半递归  rightHead = sortList(rightListHead) 
  • 最后合并左半和右半两个有序链表 return merge2OrderList(leftHead, rightHead)

过程中会用到链表的两个经典算法:

  • 合并两个有序链表:虚拟头节点,ListNode head = new ListNode(0); curPre = head 最后返回 head.next。while(p1!=null || p2!=null) if(p1==null)......
  • 找链表中点:快指针走两步,慢指针走一步,但又一点于以前不同: 我们希望奇数个如 1 2 3 时返回 2,偶数个如 1 2 3 4 时返回 2 。因为我们认为 mid.next 是右半的起点,并且在这时候会令 mid.next = null。这与之前的不同,之前偶数个如 1 2 3 4 时希望返回 3

之前的找链表中点:奇数个如 1 2 3 时返回 2,偶数个如 1 2 3 4 时返回 3 。

private static ListNode findMidNode(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        while (fast != null) {
            fast = fast.next;
            if (fast != null) {
                fast = fast.next;
                slow = slow.next;
            }
        }
        return slow;
    }
  • 奇数 1 2 3:初始 fast=1 slow=1 ;第一次循环:fast=2 fast=3 slow=2  ;第二次循环 fast=3 ;最后返回 slow=2
  • 偶数 1 2 3 3:初始 fast=1 slow=1 ;第一次循环:fast=2 fast=3 slow=2 ;第二次循环 fast=4 fast=null slow=3 ;最后返回 slow=3

现在的找链表中点:奇数个如 1 2 3 时返回 2,偶数个如 1 2 3 4 时返回 2 。

private static ListNode findMidNode(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        while (fast != null) {
            fast = fast.next;
            // 这里要上一个  && fast.next != null 的条件
            if (fast != null && fast.next != null) {
                fast = fast.next;
                slow = slow.next;
            }
        }
        return slow;
    }
  • 奇数 1 2 3:初始 fast=1 slow=1 ;第一次循环:fast=2 fast=3 slow=2 ;第二次循环 fast=3 ;最后返回 slow=2
  • 偶数 1 2 3 3:初始 fast=1 slow=1;第一次循环:fast=2 fast=3 slow=2 ;第二次循环 fast=4 因为fast.next==null退出了循环;最后返回 slow=2
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {

    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            // head.next == null 即只有一个元素时,不用再划分了,而且一个元素本身也是有序的,所以返回就返回这一个元素
            // 而且只有一个元素时,也没法找中点
            return head;
        }
        ListNode midNode = findMidNode(head);
        // 右半数组的起始是中间节点的下一个
        ListNode rightListHead = midNode.next;
       // 为了使链表有正常的边界,从中间节点断开
        midNode.next = null;
        ListNode leftHead = sortList(head);
        ListNode rightHead = sortList(rightListHead);
      // 合并两个有序链表
      ListNode list = merge2OrderList(leftHead, rightHead);
     return list;
    }

    /*
        经典链表算法:合并两个有序链表
     */
    private static ListNode merge2OrderList(ListNode head1, ListNode head2) {
        // 这个 head 是可以造的,只是为了后面可以不对头节点做特殊判断
        ListNode head = new ListNode(0);
        ListNode curPre = head;
        ListNode p1 = head1;
        ListNode p2 = head2;
        // 两个有一个没完,就继续循环,注意是 || 不是 &&
        while (p1 != null || p2 != null) {
            // list1完了。list2没完
            // 请注意:【刚开始写成了p1!=null】 应该是【p1==null】
            if (p1 == null) {
                curPre.next = p2;
                curPre = p2;
                p2 = p2.next;
            }
            // list2完了。list1没完
            else if (p2 == null) {
                curPre.next = p1;
                curPre = p1;
                p1 = p1.next;
            }
            else {
                if (p1.val <= p2.val) {
                    curPre.next = p1;
                    curPre = p1;
                    p1 = p1.next;
                }
                else {
                    curPre.next = p2;
                    curPre = p2;
                    p2 = p2.next;
                }
            }
        }
        // 最后返回 head.next,因为 head 就是个虚拟结点
        return head.next;
    }

    /*
        经典链表算法:合并两个有序链表
     */
    private static ListNode findMidNode(ListNode head) {
        // 我们希望奇数个如 1 2 3 时返回 2,偶数个如 1 2 3 4 时返回 2
        // 因为我们认为 mid.next 是右半的起点,并且在这时候会令 mid.next = null
        // 这与之前的不同,之前偶数个如 1 2 3 4 时希望返回 3
        ListNode fast = head;
        ListNode slow = head;
        while (fast != null) {
            fast = fast.next;
            // 所以这里要上一个  && fast.next != null 的条件
            if (fast != null && fast.next != null) {
                fast = fast.next;
                slow = slow.next;
            }
        }
        return slow;
    }
}

 

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

堆排序

class Solution {
    public int findKthLargest(int[] nums, int k) {
        int N = nums.length;
        // 注意这里是从 N/2-1 开始调,它的孩子已经可以包含堆的最后一个元素了
        // 注意 i>=0,因为堆顶可能也不满足堆定义
        for (int i=N/2-1;i>=0;i--) {
            heapfy(nums, N, i);
        }
        for (int i=N-1;i>=N-k;i--) {
            // 把末位元素提到顶部下标为0,堆顶最大放到末尾
            swap(nums, 0, i);
            // 现在这个新提上来的顶部下标为0的元素,不满足堆的定义,因此要调整0的位置。堆的边界是i,调整0。
            heapfy(nums, i, 0);
        }
        return nums[N-k];
    }

    private void heapfy(int[] nums, int N, int i) {
        // 先令最大的等于i
        int largest = i;
        // 左孩子
        int leftChilrd = 2*i+1;
        // 右孩子
        int rightChilrd = 2*i+2;
        // 左孩子比 largest 大
        if (leftChilrd < N && nums[leftChilrd] > nums[largest]) {
            largest = leftChilrd;
        }
        // 右孩子比 largest 大
        if (rightChilrd < N && nums[rightChilrd] > nums[largest]) {
            largest = rightChilrd;
        }
        // 说明左右孩子比 i 大,i 所处三角不满足堆的定义
        if (largest != i) {
            // 和更大的那个交换
            swap(nums, largest, i);
            // 继续调整 i 的位置(现在已经换到了largest),直到到一个满足堆定义的地方
            heapfy(nums, N, largest);
        }
    }

    private void swap(int[] nums, int a, int b) {
        int tmp = nums[a];
        nums[a] = nums[b];
        nums[b] = tmp;
    }
}

 

347. 前 K 个高频元素

 

最简单的做法是给「出现次数数组」排序。但由于可能有 O(N) 个不同的出现次数(其中 N 为原数组长度),故总的算法复杂度会达到 O(Nlog⁡N),不满足题目的要求。

在这里,我们可以利用堆的思想:建立一个小顶堆,然后遍历「出现次数数组」:

  • 如果堆的元素个数小于 k,就可以直接插入堆中。
  • 如果堆的元素个数等于 k,则检查堆顶与当前出现次数的大小。如果堆顶更大,说明至少有 kkk 个数字的出现次数比当前值大,故舍弃当前值;否则,就弹出堆顶,并将当前值插入堆中。

遍历完成后,堆中的元素就代表了「出现次数数组」中前 kkk 大的值。

class Solution {

    private static class Num implements Comparable<Num> {
        public Integer val;
        public Integer freq;
        public Num(int val, int freq) {
            this.val = val;
            this.freq = freq;
        }
        public int compareTo(Num num2) {
            return this.freq.compareTo(num2.freq);
        }
    }

    public int[] topKFrequent(int[] nums, int k) {

        // 整理频率到 Map
        Map<Integer, Integer> map = new HashMap();
        for (int i=0;i<nums.length;i++) {
            Integer freq = map.get(nums[i]);
            if (freq == null) {
                map.put(nums[i], 1);
            }
            else {
                map.put(nums[i], freq+1);
            }
        }

        PriorityQueue<Num> queue = new PriorityQueue();
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            Num num = new Num(entry.getKey(), entry.getValue());
            // 堆的个数小于 k ,直接加入
            if (queue.size() < k) {
                queue.offer(num);
            }
            else {
                // 如果当前元素的频率比堆顶当前最小的大,就可以取代当前堆顶这个
                if (num.freq > queue.peek().freq) {
                    queue.poll();
                    queue.offer(num);
                }
            }
        }

        int[] res = new int[k];
        int i=0;
        while(!queue.isEmpty()) {
            res[i] = queue.poll().val;
            i++;
        }
        return res;

    }
}

 

295. 数据流的中位数

使用两个优先队列(堆)来维护整个数据流数据,维护中位数左半边数据的优先队列(大根堆)为 l,维护中位数右半边数据的优先队列(小根堆)为 r。

人为固定 l 和 r 之前存在如下的大小关系:

  • 当数据流元素数量为偶数:l 和 r 大小相同此时动态中位数为两者堆顶元素的平均值;
  • 当数据流元素数量为奇数:l 比 r 多一,此时动态中位数为 l 的堆顶原数。

为了满足上述说的奇偶性堆大小关系,在进行 addNum 时,我们应当分情况处理:

  • 插入前两者大小相同,说明插入前数据流元素个数为偶数,插入后变为奇数。我们期望操作完达到「l 的数量为 r 多一,同时双堆维持有序」,进一步分情况讨论:
    • 如果 r 为空,而两者大小相同,说明 l 也为空,当前插入的是首个元素,直接添加到 l 即可;
    • 如果 r 不为空,且 num <= r.peek(),说明 num 的插入位置不会在后半部分(不会在 r 中),直接加到 l 即可;
    • 如果 r 不为空,且 num > r.peek(),说明 num 的插入位置在后半部分,为了使 l 比 r 多 1 ,此时将 r 的堆顶元素放到 l 中,再把 num 放到 r(相当于从 r 中置换一位出来放到 l 中)。
  • 插入前两者大小不同,说明前数据流元素个数为奇数,插入后变为偶数。我们期望操作完达到「l 和 r 数量相等,同时双堆维持有序」,进一步分情况讨论(此时 l 必然比 r 元素多一):
    • 如果 num >= l.peek(),说明 num 的插入位置不会在前半部分(不会在 l 中),直接添加到 r 即可。
    • 如果 num < l.peek(),说明 num 的插入位置在前半部分,此时将 l 的堆顶元素放到 r 中,再把 num 放入 l 中(相等于从 l 中替换一位出来当到 r 中)。

作者:宫水三叶
链接:https://leetcode.cn/problems/find-median-from-data-stream/solutions/961319/gong-shui-san-xie-jing-dian-shu-ju-jie-g-pqy8/

class MedianFinder {

    PriorityQueue<Integer> leftDescQueue = null;

    PriorityQueue<Integer> rightAscQueue = null;

    public MedianFinder() {
        // 大根堆降序:(a,b)->(b-a) 相反减
        leftDescQueue = new PriorityQueue<>((a, b) -> (b-a));
        // 小根堆升序:(a, b)->(a-b) 顺序减
        rightAscQueue = new PriorityQueue<>((a, b) -> (a-b));
    }
    
    public void addNum(int num) {
        int leftSize = leftDescQueue.size();
        int rightSize = rightAscQueue.size();
        // 两个size相等。加入前是偶数,加入后会成奇数。希望左边比右边多1且有序
        if (leftSize == rightSize) {
            // 注意这里千万不能判断 num<leftDescQueue.peek()
            // 比右边最小的小。直接加入左边,刚好比右边多1
            // 它们两大小相等且右边空,说明两个都是空的,这种情况也是直接加入左边
            if (rightAscQueue.isEmpty() || num<=rightAscQueue.peek()) {
                // 本来左右相等。左边+1。最后左比右多1
                leftDescQueue.offer(num);
            } 
            // 否则按数值大小来说应该加入右边的。这样为了维持左边比右边多1,就应该把右边的最大的那个放到左边
            else {
                // 本来左右相等。左边+1,右边-1+1不变。最后左比右多1
                leftDescQueue.offer(rightAscQueue.poll());
                rightAscQueue.offer(num);
            }
        }
        // 两个size不等。加入前是奇数,加入后会成偶数。
        // 两个size不等的情况,只可能是左边的大于右边的,因为在前面的if加num成奇数个的情况,一直都在维持左边的size比右边的size多1
        else {
            // 前提:这里一定是左边的size比右边的size多1
            // 比左边最大的大,那么应该直接加入右边,这样左右两边刚好一样多
            if (num>=leftDescQueue.peek()) {
                // 本来左比右多1。右边+1。最后二者相等。
                rightAscQueue.offer(num);
            }
            // 按数值应该加入左边。但是左边已经比右边多1了,要维持相等,只能把左边最大的挪到右边
            else {
                // 本来左比右多1。左边-1+1不变,右边+1。最后二者相等。
                rightAscQueue.offer(leftDescQueue.poll());
                leftDescQueue.offer(num);
            }
        }
    }
    
    public double findMedian() {
        int leftSize = leftDescQueue.size();
        int rightSize = rightAscQueue.size();
        if (leftSize == rightSize) {
            double a = leftDescQueue.peek() + rightAscQueue.peek();
            return a/2.00;
        }
        else {
            return leftDescQueue.peek();
        }
    }
}

/**
 * Your MedianFinder object will be instantiated and called as such:
 * MedianFinder obj = new MedianFinder();
 * obj.addNum(num);
 * double param_2 = obj.findMedian();
 */