4.10
25. K 个一组翻转链表 - 力扣(LeetCode)
前置题为92. 反转链表 II - 力扣(LeetCode)
92题的题解如下:
/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode() : val(0), next(nullptr) {} * ListNode(int x) : val(x), next(nullptr) {} * ListNode(int x, ListNode *next) : val(x), next(next) {} * }; */ class Solution { public: ListNode* reverseBetween(ListNode* head, int l, int r) { ListNode dummy(0 , head); ListNode* p0 = &dummy; //找到l的前一个节点 for (int i = 0; i < l - 1; i++) { p0 = p0->next; } ListNode* pre = nullptr; ListNode* cur = p0->next; for (int i = 0; i < r - l + 1; i++) { ListNode* nxt = cur->next; cur->next = pre; pre = cur; cur = nxt; } p0->next->next = cur; p0->next = pre; return dummy.next; } };有两种写法:
- 如果定义 ListNode* dummyHead = new ListNode(0,head);那么此时dummyHead是链表指针,最后返回的是dummyHead->next
- 如果定义 ListNode dummy(0 , head),那么此时ListNode是链表结构体,最后返回的是dummy.next
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
// 统计节点个数
int n = 0;
for (ListNode* cur = head; cur; cur = cur->next) {
n++;
}
ListNode dummy(0, head);
ListNode* p0 = &dummy;
ListNode* pre = nullptr;
ListNode* cur = head;
// k 个一组处理
for (; n >= k; n -= k) {
for (int i = 0; i < k; i++) { // 同 92 题
ListNode* nxt = cur->next;
cur->next = pre; // 每次循环只修改一个 next,方便大家理解
pre = cur;
cur = nxt;
}
// 如图,pre为反转后的头,cur为下一组的头
//比92题多了记下p0->next然后赋值给p0这一步
ListNode* nxt = p0->next;//记录下翻转前的头,翻转后的末尾
p0->next->next = cur;//接到下一组的头
p0->next = pre;//p0接到翻转后的头
p0 = nxt;//翻转后的末尾是下一个循环的前驱结点
}
return dummy.next;
}
};
23. 合并 K 个升序链表 - 力扣(LeetCode)
1.最小堆
先把所有链表头放到最小堆里,当堆非空时,每次弹出堆头最小节点node
如果node->next非空,就加入堆
一直循环直到堆空
class Solution {
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
auto cmp = [](const ListNode* a , const ListNode* b){
return a->val > b->val;
};
priority_queue<ListNode*, vector<ListNode*> , decltype(cmp)> pq;
for (auto head : lists){
if(head) pq.push(head);
}
ListNode dummy{};
auto cur = &dummy;
while(!pq.empty()){
auto node = pq.top();
pq.pop();
if(node->next) pq.push(node->next);
cur->next = node;
cur = cur->next;
}
return dummy.next;
}
};
复杂度分析
- 时间复杂度:O(nlogk),其中k为lists的长度,n为所有链表的节点数之和。
- 空间复杂度:O(k)。堆中至多有k个元素。
方法二:分治
暴力做法是,按照
21. 合并两个有序链表的题解思路,先合并前两个链表,再把得到的新链表和第三个链表合并,再和第四个链表合并,依此类推。但是这种做法,平均每个节点会参与到O(k)次合并中(用
(1+2+⋯+k)/k粗略估计),所以总的时间复杂度为O(nk)。一个巧妙的思路是,把lists一分为二(尽量均分),先合并前一半的链表,再合并后一半的链表,然后把这两个链表合并成最终的链表。如何合并前一半的链表呢?我们可以继续一分为二。如此分下去直到只有一个链表,此时无需合并。
我们可以写一个递归来完成上述逻辑,按照一分为二再合并的逻辑,递归像是在后序遍历一棵平衡二叉树。由于平衡树的高度是
O(logk),所以每个链表节点只会出现在O(logk)次合并中!这样就做到了更快的O(nlogk)时间。假设输入链表为:这个例子中,合并三个链表的分治过程如下:
- List1: 1 → 4 → 5
- List2: 1 → 3 → 4
- List3: 2 → 6
分治步骤:
- 分割:将三个链表分成左半部分(List1)和右半部分(List2、List3)。
- 合并右半部分:
- 进一步分割为 List2 和 List3。
- 合并 List2 和 List3 → 1 → 2 → 3 → 4 → 6。
- 合并左半部分与右半结果:
- 合并 List1(1→4→5)和上一步的结果(1→2→3→4→6)→ 最终合并结果为 1→1→2→3→4→4→5→6。
最终结果:
1 → 1 → 2 → 3 → 4 → 4 → 5 → 6代码执行流程:
- 递归分割链表数组,逐层合并相邻链表,最终将所有链表合并为一个有序链表。
class Solution {
// 21. 合并两个有序链表
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode dummy{}; // 用哨兵节点简化代码逻辑
auto cur = &dummy; // cur 指向新链表的末尾
while (list1 && list2) {
if (list1->val < list2->val) {
cur->next = list1; // 把 list1 加到新链表中
list1 = list1->next;
} else { // 注:相等的情况加哪个节点都是可以的
cur->next = list2; // 把 list2 加到新链表中
list2 = list2->next;
}
cur = cur->next;
}
cur->next = list1 ? list1 : list2; // 拼接剩余链表
return dummy.next;
}
// 合并从 lists[i] 到 lists[j-1] 的链表
ListNode* merge(vector<ListNode*>& lists, int i, int j) {
int m = j - i;
if (m == 0) return nullptr; // 注意输入的 lists 可能是空的
if (m == 1) return lists[i]; // 无需合并,直接返回
auto left = merge(lists, i, i + m / 2); // 合并左半部分
auto right = merge(lists, i + m / 2, j); // 合并右半部分
return mergeTwoLists(left, right); // 最后把左半和右半合并
}
public:
ListNode* mergeKLists(vector<ListNode* > &lists) {
return mergeKLists(lists, 0, lists.size());
}
};
复杂度分析
- 时间复杂度:O(nlogk),其中k为lists的长度,n为所有链表的节点数之和。每个节点参与链表合并的次数为O(logk)次,一共有n个节点,所以总的时间复杂度为O(nlogk)。
- 空间复杂度:O(logk)。递归深度为O(logk),需要用到O(logk)的栈空间。Python 忽略切片产生的额外空间。
20. 有效的括号 - 力扣(LeetCode)
class Solution {
bool check(char a , char c){
if((a == '{' && c =='}') || (a == '(' && c == ')') || (a == '[' && c == ']')) return true;
return false;
}
public:
bool isValid(string s) {
stack<char> st;
for(char c : s){
if(st.empty() || c == '(' || c == '[' || c == '{'){
st.push(c);
continue;
}
char a = st.top();
st.pop();
if(!check(a ,c)) return false;
}
if (st.empty()) return true;
else return false;
}
};
470. 用 Rand7() 实现 Rand10() - 力扣(LeetCode)
解题思路
移位加法构造
题目给你一个等概率函数,你可以把他转换为一个等概率返回0和1的函数rand01();
因为f()可以构造任何等概率的函数,以题目举例;
已知等概率返回1-7,我们可以在rand01()循环中等于4重新计算,小于4返回0,大于4返回1。这是等概率的。
构造1-10等概率,可以先构造0-9等概率再加1。估算至少需要4个二进制位,相加即可。(9(10) = 1001(2))。从右向左来看0、1、2、3分别代表是否加1,加2,加4,加8,任何一种可能都是等概率的,所以返回等概率0到15,大于9的重新计算即可。
PS:此类题以后先转换成等概率返回0和1
// The rand7() API is already defined for you.
// int rand7();
// @return a random integer in the range 1 to 7
class Solution {
int rand01(){
int res;
do{
res = rand7();
}
while(res == 4);
return res < 4 ? 0 : 1;
}
public:
int rand10() {
int res;
do{
res = (rand01() << 3) + (rand01() << 2) + (rand01() << 1) + rand01();
} while(res > 9);
return res + 1;
}
};
万能构造法:独立随机事件+古典概型
乘法构造
[1,X] 的随机数发生器 randX() 很明显是一个古典概型:它的结果是有限的,且每个结果的概率相同。
独立随机事件的概率:P(AB)=P(A)∗P(B)
那么任意的 randX() 都可以用以下方法构造:
randA()构造randB()时,需要找一个最大质因子不超过 A 的数 n (n>=B),然后对 n 分解质因子就能找到每个采样需要取多少种结果。实际到具体数字时,可以把部分质因子合并成不超过 A 的数,从而减少采样次数。
- 构造 n 次相互独立的采样,其中第 i 次采样有 mi 种结果,且第 i 次采样中每种结果的概率是 mi1。n 要满足 m1∗m2∗⋯∗mn≥X ,即把所有采样结果组合起来,最终的结果数量不少于 X,保证可以映射到 [1,X] 的每一个元素。
这样做的好处是,我们构造了 m1∗m2∗⋯∗mn 个结果,并且每个结果的概率都是 m1∗m2∗⋯∗mn1。- 从 m1∗m2∗⋯∗mn 个结果中取 X 个,映射到 [1,X] 区间,我们就得到了一个均匀分布在 [1,X] 的随机数发生器。
第二步中的映射是 1:1 映射,实际运用中,第二步可以取 k∗X 个结果来做 k:1 映射,以减少调用 rand7() 次数的期望。
rand7() 构造 rand10()
取n = 10 ,n的最大不超过7的质因子为5,因此10= 5 * 2,调用两次rand7()即可。
- 构造 2 次采样,分别有 2 和 5 种结果,组合起来便有 10 种概率相同的结果。
- 把这 10 种结果映射到 [1,10] 即可。
第一步具体要如何构造采样是自由的,比如 rand7() 拒绝 7,然后对 [1,6] 采样,把奇数和偶数作为 2 种结果,这 2 种结果的概率均为 0.5 。
rand7() 拒绝 6,7 ,然后对 [1,5] 采样,有 5 种结果,每种概率均为 0.2 。
代码如下。// The rand7() API is already defined for you. // int rand7(); // @return a random integer in the range 1 to 7 class Solution { public: int rand10() { int a , b; while((a = rand7()) > 6); while((b = rand7()) > 5); return (a & 1) == 1 ? b : b + 5; //a是奇数则返回1-5,是偶数返回6-10 } };rand7() 构造任意范围的随机数发生器
上述方法理论上可以构造任何范围的随机数发生器,比如 rand11() :
- 构造 2 次采样,分别有 2 和 6 种结果,组合起来便有 12 种概率相同的结果。
- 把这 12 种结果映射到 [1,12] ,然后再拒绝 12 即可。
rand100() :
- 构造 3 次采样,分别有 4,5,5 种结果,组合起来便有 100 种概率相同的结果。
- 把这 100 种结果映射到 [1,100] 即可。
442. 数组中重复的数据 - 力扣(LeetCode)
解法:原地哈希
这道题要求找出数组 nums 中的所有出现两次的整数并返回。常规的做法是使用哈希表存储数组中的整数,遍历数组并将每个整数存入哈希表,如果遍历到一个元素时发现该元素已经在哈希表中,则该元素是出现两次的整数。对于长度为 n 的数组,使用哈希表的时间复杂度是 O(n),符合题目要求,但是空间复杂度是 O(n),不符合题目要求的常数空间。
为了将空间复杂度降低到常数,不能额外创建哈希表,只能原地修改数组。由于数组 nums 的长度是 n,下标范围是 [0,n−1],每个元素都在范围 [1,n] 内,因此可以将数组看成哈希表,利用数组下标的信息表示每个整数是否出现两次。对于下标 index,满足 0≤index<n,1≤index+1≤n,nums[index] 可以用于表示整数 index+1 是否出现两次。
遍历数组 nums。对于元素 num,其对应的下标为 index=∣num∣−1,根据 nums[index] 的正负性执行如下操作:
如果 nums[index]>0,则将 nums[index] 的值更新为其相反数;
如果 nums[index]<0,则 ∣nums∣=index+1 是出现两次的整数,将其添加到结果中。
上述做法的原理如下:
初始时数组 nums 中的整数都是正数,表示尚未被访问;
当一个整数被访问时,如果该整数对应的下标处的元素是正数,则该整数尚未被访问,因此将该整数对应的下标处的元素改成其相反数,相反数是负数,表示被访问了一次;
当一个整数被访问时,如果该整数对应的下标处的元素是负数,则该整数已经被访问,因此该整数被第二次访问,即该整数是出现两次的整数。
需要注意的是,遍历数组 nums 的过程中,遍历到的元素 num 可能已经被改成负数,因此在计算下标 index 时需要对 num 取绝对值然后减 1。
class Solution {
public:
vector<int> findDuplicates(vector<int>& nums) {
vector<int> res;
int n = nums.size();
for (int nums : nums){
int index = abs(nums) - 1;
if(nums[index] > 0) nums[index] = -nums[index];
else res.push_back(index + 1);
}
return res;
}
};





浙公网安备 33010602011771号