11.13链表补充
11/13
每日一题:3261. 统计满足 K 约束的子字符串数量 II
思路:双指针+前缀和优化+二分查找
首先自己用昨天写的函数套上一个for循环,每次
substr截取子字符串然后存到数组里。时间复杂度是o(n^2)超时了,因此需要优化。
//超时代码 class Solution { private: long long func(string s, int k) { long long ans = 0, left = 0, cnt[2]{}; for (int i = 0; i < s.length(); i++) { cnt[s[i] & 1]++; while (cnt[0] > k && cnt[1] > k) { cnt[s[left] & 1]--; left++; } ans += i - left + 1; } return ans; } public: vector<long long> countKConstraintSubstrings(string s, int k, vector<vector<int>>& queries) { vector<long long> ans; for (int i = 0; i < queries.size(); i++) { int l = queries[i][0]; int r = queries[i][1]; string t = s.substr(l , r - l + 1); ans.push_back(func(t , k)); cout << ans[i]; } return ans; } };
首先在滑动窗口的基础上,每次记录下右端点r满足条件的最小左端点l,即left[r] = l,一旦左端点<l就不满足。
然后用前缀和数组记下每个右端点的合法字符串数量sum[r]。
对于每个询问[l , r]可以分两种情况讨论:
- 如果有left[r] <= l ,那么[l , r]里面的所有字符串均满足,字符串长度为r - l + 1,可以用等差数列直接计算数量= 1 + 2 + 3 + ····r - l + 1 = (r - l + 2) *(r - l + 1) / 2
- 如果left[r] > l,那么有部分合法有部分不合法,设分界点为k(left(k)>l && left(k - 1) <= l)由于left数组是有序的,那么k左边的可以用等差数列求和,右边的=num(k+1)+num(k+2)+···num(r)。而这部分和可以用前缀和计算=sum[r] - sum[k]。
- 所以关键点在于找分界点k,可以用二分查找。
二分找k:为第一个满足left(k) > l 的点,用
lower_bound函数实现
lower_bound函数从数组的
begin位置到end-1位置二分查找第一个大于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。关于本题中
lower_bound函数的用法解释:找第一个 >= l的数K,如果区间内所有数<l,结果是k = r + 1
int k = lower_bound(left.begin() + l , left.begin() + r + 1 , l) - left.begin();起始指针位置为
left.begin()向右偏移l个单位,因为left[i], i<l时必定有left[i] <l,意思是区间内均满足。下面要写到结尾位置+1。
class Solution {
public:
vector<long long> countKConstraintSubstrings(string s, int k, vector<vector<int>>& queries) {
int n = s.size();
vector<int> left(n);//记录每个位置满足条件的最小l下标
vector<long long> sum(n + 1);//前缀和数组
int cnt[2]{} , l = 0;
for (int r = 0; r < n; r++) {
cnt[s[r] & 1] ++;
while(cnt[0] > k && cnt[1] > k){
cnt[s[l ++] & 1] --;
}
left[r] = l;
sum[r + 1] = sum[r] + r - l + 1; // 计算 r-left[r]+1 的前缀和
}
vector<long long> ans(queries.size());
for (int i = 0; i < queries.size(); i++) {
int l = queries[i][0] , r = queries[i][1];
//也可写作:
// for (auto& query : queries) {
//int l = query[0], r = query[1];
//找第一个 >= l的数K,如果区间内所有数<l,结果是k = r + 1
int k = lower_bound(left.begin() + l , left.begin() + r + 1 , l) - left.begin();
//分成l~k - 1 和 k ~ r两部分,由于前缀和数组从1开始,注意要下标+1,即计算 k+1~r+1 内的总和
ans[i] = sum[r + 1] - sum[k] + (long long)(k - l + 1) * (k - l) / 2;
}
return ans;
}
};
- 时间复杂度:O(n+qlogn),其中 n 是 nums 的长度,q 是 queries 的长度。注意 l 只会增加不会减少,所以二重循环的时间复杂度为 O(n)。
- 空间复杂度:O(n)。返回值不计入。
19. 删除链表的倒数第 N 个结点
思路:双指针
经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。
/**
* 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* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* fast = dummyHead;
ListNode* slow = dummyHead;
while(n --) fast = fast->next;
while(fast->next != NULL){
fast = fast->next;
slow = slow->next;
}
//此时slow的下一个节点是待删除的节点
ListNode* tmp = slow->next;
slow->next = slow->next->next;
delete(tmp);
tmp = nullptr;
head = dummyHead->next;
delete dummyHead;
return head;
}
};
面试题 02.07. 链表相交
思路:双指针
如果有相交,相交节点以及后续部分一定是位于链表最后部分的。不可能出现中间一段相等而后续不相等的情况。
- 首先计算链表A、B的长度lenA,lenB,假设
lenA >lenB(如果不是则交换)。gap = lenA - lenB- 定义
curA = headA , curB = headB,把curA后移gap步保证AB结尾重合。- 然后
curAB移动判断是否相等。
半天AC不了,原来是等于号写成赋值了。。。
if(curA == curB) return curA;
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* curA = headA;
ListNode* curB = headB;
int lenA = 0 , lenB = 0;
while(curA != NULL){
curA = curA->next;
lenA ++;
}
while(curB != NULL){
curB = curB->next;
lenB ++;
}
if(lenA < lenB){
swap(headA , headB);
swap(lenA , lenB);
}
curA = headA;
curB = headB;
int gap = lenA - lenB;
while(gap --) curA = curA->next;
while(curA != NULL){
if(curA == curB) return curA;
curA = curA->next;
curB = curB->next;
}
return NULL;
}
};
- 时间复杂度:O(n + m)
- 空间复杂度:O(1)
M:142. 环形链表 II - 力扣(LeetCode)
思路:快慢指针
- 判断链表是否有环
可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
2.如果有环,如何找到这个环的入口
假设从头结点到环形入口节点 的节点数为x。 环形入口节点到fast指针与slow指针相遇节点 节点数为y。 从相遇节点再到环形入口节点节点数为 z。 如图所示:
那么相遇时: slow指针走过的节点数为:
x + y, fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走的圈数。(y+z)为 一圈内节点的个数A。fast指针走过的节点数 = slow指针走过的节点数 * 2:即
(x + y) * 2 = x + y + n (y + z)消掉一个(x+y)后:
x + y = n (y + z)因为要找环形的入口,那么要求的是
x,因为x表示 头结点到 环形入口节点的的距离。所以要求x ,将x单独放在左面:
x = n (y + z) - y,再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:
x = (n - 1) (y + z) + z注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。这个公式说明什么呢?先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。
- 当 n为1的时候,公式就化解为
x = z,这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。
也就是在相遇节点处,定义一个指针
index1,在头结点处定一个指针index2。让
index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。
- n大于1,就是fast指针在环形转n圈之后才遇到 slow指针。
其实这种情况和n为1的时候效果是一样的,一样可以通过这个方法找到环形的入口节点,只不过,
index1指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head;
ListNode* slow = head;
//这里while里面的条件是因为下面要用到:fast->next,因此保证fast非空;fast->next->next,因此保证fast->next非空。
while(fast != NULL && fast->next != NULL){
fast = fast->next->next;
slow = slow->next;
if(fast == slow) {
ListNode* cur1 = head;
int cnt = 0;
while(cur1 != slow){
cur1 = cur1->next;
slow = slow->next;
}
return cur1;
}
}
return NULL;
}
};
234. 回文链表
思路:有数组模拟法和双指针法
- 数组模拟
最直接的想法,就是把链表装成数组,然后再判断是否回文。
// 比较数组回文 for (int i = 0, j = vec.size() - 1; i < j; i++, j--) { if (vec[i] != vec[j]) return false; } return true; }
- 快慢指针法
定义fast每次走两步,slow每次走一步,快指针遇到终止位置时,慢指针就在链表中间位置(此时fast可能为NULL,但是没关系,我们只需要slow的位置)
如果链表长度为奇数,那slow恰好在中间位置,如{1 ,2,3 , 2 ,1}
如果链表长度为偶数,那应该在中点靠右的位置,如{1 , 2 , 2 , 1}
用
pre记下slow的前一个位置,以pre为界限将链表划分为两部分cur1、cur2,len(cur1)<=len(cur2)。然后用前面学过的链表翻转将cur2翻转,最后从头遍历cur1、cur2,判断是否相等即可。
/**
* 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* reverse(ListNode* head){
if(head == NULL || head->next == NULL) return head;
ListNode* last = reverse(head->next);
head->next->next = head;
head->next = NULL;
return last;
}
bool isPalindrome(ListNode* head) {
if(head == NULL || head->next == NULL) return true;
ListNode* fast = head;
ListNode* slow = head;
ListNode* pre = head;
while(fast && fast->next ){
pre = slow;
fast = fast->next->next;
slow = slow->next;
}
//此时pre指向中点,将pre右边全部翻转
pre->next = NULL;//分割链表
ListNode* last = reverse(slow);//last为新表2表头
ListNode* cur = head;
while(cur){//cur长度小,以cur非空为终止条件
if(cur -> val != last -> val) return false;
cur = cur->next;
last = last->next;
}
return true;
}
};
143.重排链表
思路:
- 数组模拟
先把所有节点存在数组vec里,然后通过cnt判断每次把
vec[j]加入还是vec[i]加入。cur = head; int i = 1; int j = vec.size() - 1; // i j为之前前后的双指针 int count = 0; // 计数,偶数去后面,奇数取前面 while (i <= j) { if (count % 2 == 0) { cur->next = vec[j]; j--; } else { cur->next = vec[i]; i++; } cur = cur->next; count++; }
- 双向队列模拟
把链表放进双向队列,然后通过双向队列一前一后弹出数据,来构造新的链表。这种方法比操作数组容易一些,不用双指针模拟一前一后了。
ListNode* node; while(que.size()){ if(cnt % 2 == 0){ node = que.back(); que.pop_back(); } else{ node = que.front(); que.pop_front(); } cnt ++; cur->next = node;//把node接到cur后边 cur = cur->next; }
将链表分割成两个链表,然后把第二个链表反转,之后在通过两个链表拼接成新的链表。
如图:
这种方法,比较难,平均切割链表,看上去很简单,真正代码写的时候有很多细节,同时两个链表最后拼装整一个新的链表也有一些细节需要注意!
本题因为cur1所在的链表1本身带有head节点,所以应该比链表2长一个,因此分割时使slow为分割点,链表2为slow->next后的部分。
if (!head) return; ListNode* fast = head; ListNode* slow = head; while(fast && fast->next && fast->next->next){ fast = fast->next->next; slow = slow->next; } //以slow为分割点,slow停在中点或中点左侧 ListNode* cur2 = reverse(slow->next);//cur2指向最后一个节点,slow->next为cur2的尾节点。 slow->next = NULL; ListNode* cur = head; ListNode* cur1 = cur->next;
vec数组:
class Solution {
public:
void reorderList(ListNode* head){
vector<ListNode*> vec;
ListNode* cur = head;
if(!cur) return;
while(cur){
vec.push_back(cur);
cur = cur->next;
}
cur = head;
int i = 1;
int j = vec.size() - 1;
int cnt = 0;
while(i <= j){
if(cnt % 2 == 0){
cur->next = vec[j];
j --;
}
else {
cur->next = vec[i];
i ++;
}
cur = cur->next;
cnt ++;
}
cur->next = NULL;
}
};
deque:
class Solution {
public:
void reorderList(ListNode* head){
deque<ListNode*> que;
ListNode* cur = head;
if(!cur) return;
//从第二个节点开始存入双向队列
while(cur->next){
que.push_back(cur->next);
cur = cur->next;
}
cur = head;
int cnt = 0;
ListNode* node;
while(que.size()){
if(cnt % 2 == 0){
node = que.back();
que.pop_back();
}
else{
node = que.front();
que.pop_front();
}
cnt ++;
cur->next = node;//把node接到cur后边
cur = cur->next;
}
cur->next = NULL;
}
};
分割翻转法:
class Solution {
private:
ListNode* reverse(ListNode* head){
ListNode* tmp;
ListNode* cur = head;
ListNode* pre = NULL;
while(cur){
tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
public:
void reorderList(ListNode* head){
if (!head) return;
ListNode* fast = head;
ListNode* slow = head;
while(fast && fast->next && fast->next->next){
fast = fast->next->next;
slow = slow->next;
}
//以slow为分割点
ListNode* cur2 = reverse(slow->next);//cur2指向最后一个节点
slow->next = NULL;
ListNode* cur = head;
ListNode* cur1 = cur->next;
int cnt = 0;
while(cur1 && cur2){
if(cnt % 2 ==0){
cur->next = cur2;
cur2 = cur2->next;
}
else{
cur->next = cur1;
cur1 = cur1->next;
}
cnt++;
cur = cur->next;
}
if(cur2) cur->next = cur2;
if(cur1) cur->next = cur1;
}
};








浙公网安备 33010602011771号