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(NlogN),不满足题目的要求。
在这里,我们可以利用堆的思想:建立一个小顶堆,然后遍历「出现次数数组」:
- 如果堆的元素个数小于 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 中)。
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(); */
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· DeepSeek “源神”启动!「GitHub 热点速览」
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器