当前位置: 首页 > news >正文

【双链表】【数组】

数组的双指针技巧

数组中没有真正的指针,我们通常把索引当作数组中的指针取用,这样就可以在数组中也施展双指针技巧。

快慢指针技巧

删除有序数组中的重复项

  • 重点在于题目要求原地删除
  • 如果不要求原地删除的话可以直接new一个新的数组,把我们需要的数据填充进去。
  • slow走在后面,fast走在前面,
  • 保持nums[0, …, slow]的所有元素都是无重复的!
  • fast在前面探路,找到一个不重复的元素就赋值给slow
#include <vector>
using namespace std;
class Solution {
public:int removeDuplicates(vector<int>& nums) {// 特例:空集if (nums.size() == 0) {return 0;}// 双指针int slow = 0, fast = 0;while (fast < nums.size()) {if (nums[fast] != nums[slow]) {slow++;nums[slow] = nums[fast];}fast++;}return slow + 1;}
};

链表

/*** 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* deleteDuplicates(ListNode* head) {// 空链表if (head == nullptr) {return nullptr;}// 快慢指针法ListNode* fast = head;ListNode* slow = head;// while (fast != nullptr) {if (slow->val != fast->val) {slow->next = fast;slow = slow->next;}fast = fast->next;}slow->next = nullptr;return head;}
};

在处理链表时,删除节点后需要释放其内存以避免泄漏。以下是添加内存管理的改进版本:

/*** 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* deleteDuplicates(ListNode* head) {if (head == nullptr) {return nullptr;}ListNode* slow = head;ListNode* fast = head->next;while (fast != nullptr) {if (slow->val == fast->val) {ListNode* temp = fast;  // 保存待删除节点fast = fast->next;      // 移动快指针delete temp;            // 释放内存} else {slow->next = fast;      // 链接非重复节点slow = slow->next;      // 移动慢指针fast = fast->next;      // 移动快指针}}slow->next = nullptr;  // 断开后续可能存在的重复节点return head;}
};
  • 主要改进点:
  1. 使用ListNode* temp保持待删除的节点
  2. 发现重复节点时立即释放内存

移除元素

  • 维护索引0到slow的数组不包含要移除的val
  • 所以fast往前走,遇到了val就跳过,没遇到val就赋值给slow!
#include <vector>
class Solution {
public:int removeElement(vector<int>& nums, int val) {int slow = 0, fast = 0;// 维护slow之前(包含slow)都是无重复的while (fast < nums.size()) {if (nums[fast] != val) {nums[slow] = nums[fast];slow++;}fast++;}return slow;}
};

这道题是移除val元素,为啥这里是先赋值后slow++?

  • 保证nums[0…slow-1] 是不包含值为 val 的元素的
  • 最后结果数组长度就是slow

移除0

#include <vector>
using namespace std;
class Solution {
public:void moveZeroes(vector<int>& nums) {int slow = 0, fast = 0;while (fast < nums.size()) {// 遇到0就跳过if (nums[fast] != 0) {nums[slow] = nums[fast];slow++;}fast++;}// 保证了0到slow-1索引都是没有0的// 再把后续所有元素填充为0while (slow < nums.size()) {nums[slow] = 0;slow++;}}
};

滑动窗口——快慢双指针指针变体

两种场景:

  • 遍历所有子数组
  • 遍历所有字串
    本质都是一样的
    遍历左右边界

暴力解的情况,需要嵌套for穷举所有子数组

for (int left = 0;  left < nums.size(); left++) {for (int right = left; right < nums.size(); right++) {// nums[left, right] 是一个子数组}
}

暴力遍历与滑动窗口最大的区别在于,滑动窗口的两个指针都是只向前走,一步也不会退,暴力解法中的right是每次移动left后都回退到新left位置重新开始遍历的。

滑动窗口核心框架模板

  • 定义窗口左右边界left和right
  • 维护这个窗口
  • 不断滑动
  • 更新答案
// 索引区间 [left, right) 是窗口
int left = 0, right = 0;while (right < nums.size()) {// 增大窗口window.addLast(nums[right]);right++;// 当需要缩小窗口while (window needs shrink) {// 缩小窗口window.removeFirst(nums[left]);}
}
  • 滑动窗口只用O(n)就能穷举出所有子串么?
  • NO!穷举所有子串必须要用双层for循环
  • 对于大部分题目并不需要穷举所有字串
  • 只需要遍历符合题目条件的窗口即可
  • 滑动窗口相当于对穷举过程剪枝,避免冗余计算
  • 属于算法本质中的,聪明的穷举这一类
    滑动窗口难点:
  • 如何向窗口中添加新元素?
  • 如何缩小窗口?
  • 哪个阶段更新结果?
  • 如何找bug等?

滑动窗口算法的代码框架

void slidingWindow(string s) {// 用适合的数据结构记录窗口中的数据,根据具体的业务场景变通// 比如说,想记录窗口中元素出现的次数,就需要用map// 如果我想记录窗口中的元素和,就用intauto window = ...int left = 0, right = 0;while (right < s.size()) {// c 是将要移入窗口的字符char c = s[right];window.add(c);// 增大窗口right++;// 进行窗口内的一系列更新...// *** debug 输出的位置 ***printf("window: [%d, %d)\n", left, right);// 注意在最终的解法代码中不要 print// 因为 IO 操作很耗时,可能导致超时// 判断左窗口是否要收缩while (window needs shrink) {// d就是将要移出窗口的字符char d = s[left];window.remove(d);// 缩小窗口left++;// 进行窗口内数据的一系列更新...}}
}
  • 代码中的两处…代表更新窗口数据的位置
  • 这两处… 分别是扩大和缩小窗口的操作,完全是对称的

遇到子串/子数组的问题,只需要回答三个问题:

  1. 什么时候要移动right扩大窗口?窗口加入字符时,应该更新哪些数据?
  2. 什么时候窗口应该暂停扩大?开始移动left实现窗口缩小?从窗口移出字符时,应该更新哪些数据?
  3. 什么时候应该更新结果?

最小覆盖字串

lc.76
假设用暴力解法:

for () {  // 遍历ifor () { // 从i+1开始遍历jif s[i:j] 包含所有t中的字母:更新答案}
} 

以上暴力遍历思路很直接,但是时间复杂度太高了。
下面开始设计滑动窗口

  • 左闭右开 [ left, right)设计(主要是边界 条件处理问题):

    • 原则上双端都开,或者都闭是可以的
    • 如果(left,right)双端都开:
      • left,right都在开头0位置时,窗口内没有元素
      • right移动到1的时候,(0,1)还是没有元素
    • 如果 [ left,right ] 双端都闭:
      • [0, 0]初始区间就包含了一个元素
  • 起初 [0, 0) 没有元素

  • 移动一步后 [0, 1) 一个元素

  • 所以左闭右开非常符合滑窗的需求

  1. 不断扩大right指针,扩大窗口[left, right),直到窗口中字符串符合要求
  2. 此时,停止增加right,转而不断增加left,缩小窗口,直到窗口中的字符串不再符合要求,每次增加left都需要更新答案。
  3. 重复以上两步,直到right到达字符串s尽头。
  • 步骤2:寻找可行解
  • 步骤3:优化可行解
  • 最终找到最优解

初始状态:在这里插入图片描述
移动右边界right,寻找可行解,直到窗口[left,right),直到满足条件,即包含T所有字符
在这里插入图片描述
接着优化当前解,移动left指针,缩小[left,right)窗口,移动的过程中保证满足题目要求条件。
在这里插入图片描述
直到窗口字符串不再符合要求,left不再移动
在这里插入图片描述
之后重复上述过程,先移动right再移动left,直到right移动到s末端。

首先,初始化window和needs两个哈希表unordered_map,记录窗口中的字符和需要凑齐的字符:

unordered_map(char, int) need, window;

然后,使用left和right变量初始化窗口的两端,左闭右开,所以初始情况窗口没有包含任何元素

int left = 0;
int right= 0;
while (right < s.size()) {// c是移动到
}

三个问题:

  1. 什么时候应该移动 right 扩大窗口?窗口加入字符时,应该更新哪些数据?
  2. 什么时候窗口应该暂停扩大,开始移动 left 缩小窗口?从窗口移出字符时,应该更新哪些数据?
  3. 我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?
  • 当一个字符c进入到窗口时,应该增加window计数器
  • 如果一个字符c将要移出窗口时,应该减少window计数器
  • 当valid满足need时应该收缩窗口
  • 应该在收缩窗口时,更新最终结果
class Solution {
public:string minWindow(string s, string t) {// 两个map记录器:window 和 needunordered_map<char, int> need;unordered_map<char, int> window;for (char c : t) {need[c]++;}// 滑动窗口左右边界初始化int left = 0;int right = 0;// 记录下window中字符满足need条件的字符个数int valid = 0;// 初始化答案// 记录最小覆盖字串的起始索引以及长度int start = 0;int len = INT_MAX;          // 初始化窗口长度为无穷大while (right < s.size()) {// c 是即将移入窗口的字符char c = s[right];// 扩大窗口right++;// 进行窗口内数据的一系列更新if (need.count(c)) {            // 如果c是need需要的window[c]++;                // 拉入window if (window[c] == need[c]) { // 直接对比两个mapvalid++;}}// 判断左窗口是否需要收缩?while (valid == need.size()) {// 在这里更新最小覆盖字串if (right - left < len) {start = left;len = right - left;}// d 是需要移出窗口的字符char d = s[left];left++;// 进行窗口内数据一系列更新if (need.count(d)) {if (window[d] == need[d]) valid--;window[d]--;}}}return len == INT_MAX ? "" : s.substr(start, len);}
};
  • 发现某个字符在window的数量满足了need的需要,就要更新valid,表示有一个字符已经满足要求
  • 两次对窗口内数据的更新操作是完全对称的
  • 当 valid==need.size() 说明 t 中所有字符都已经被完全覆盖了,已经得到了一个可行的子串,可行解,现在改优化可行解,移动左边界了!
class Solution {
public:// 判断s1的排列之一是否是s2的子串// 可以包含重复字符,所以难度大的bool checkInclusion(string s1, string s2) {// 初始化两个map,need和windowunordered_map<char, int> need;unordered_map<char, int> window;for (char c : s1) need[c]++;int left = 0;int right = 0;int valid = 0;while (right < s2.size()) {char c = s2[right];right++;// 进行窗口内数据一系列更新if (need.count(c)) {window[c]++;if (window[c] == need[c])valid++;}// 判断窗口左边界是否要收缩?while (right - left >= s1.size()) {// 收缩前,判断是否找到了合法字串if (valid == need.size()) return true;// 可能要收缩的元素dchar d = s2[left];left++;// 进行窗口内的一系列更新if (need.count(d)) {if (window[d] == need[d])valid--;window[d]--;}}}return false;}
};
  • 本题与最小覆盖字串那道题几乎一模一样
  • 区别在于
  • 本题移动left缩小窗口的时机是窗口大于s1的长度时,因为题意要求的排列,所以长度需要一致
  • 当发现valid==need.size()时,说明窗口中就是一个合法的排列,所以立即返回true。
  • 至于如何处理窗口的扩大与缩小,和上一题完全相同。

继续

http://www.dtcms.com/a/278530.html

相关文章:

  • ubuntu(22.04)系统上安装 MuJoCo
  • 计算机网络(基础概念)
  • 网络协议和基础通信原理
  • qt-- 编译工具-Cmake的使用
  • 一文读懂循环神经网络(RNN)—语言模型+读取长序列数据(2)
  • Python----NLP自然语言处理(NLP自然语言处理解释,NLP的发展历程)
  • QT——文件操作类 QFile和QTextStream
  • 【同等学力-计算机-真题解析】离散数学-图论(握手定理、欧拉公式)
  • ARMv8.1原子操作指令(ll_sc/lse)
  • #Paper Reading# Apple Intelligence Foundation Language Models
  • 【Linux网络】:HTTP(应用层协议)
  • 深入解析 Transformer:开启自然语言处理新时代的革命性模型
  • uni-app在安卓设备上获取 (WIFI 【和】以太网) ip 和 MAC
  • 游戏框架笔记
  • SAP ERP与微软ERP dynamics对比,两款云ERP产品有什么区别?
  • [个人笔记] WSL 完整使用指南及 Claude Code 配置记录
  • 019_工具集成与外部API调用
  • 【HarmonyOS】元服务概念详解
  • ubuntu系统在线安装postgres
  • 【视频格式转换】.264格式转为mp4格式
  • React Three Fiber 实现 3D 模型视图切换、显隐边框、显隐坐标轴
  • R 语言科研绘图第 64 期 --- 哑铃图
  • Python 基础语法2:组合数据类型、异常
  • Kafka——集群核心参数配置
  • TensorFlow深度学习实战(26)——生成对抗网络详解与实现
  • [Dify]-进阶1- Dify 支持的多种 AI 模型解析与选择建议
  • 排序算法(二):插入排序
  • 数据安全防护技术:筑牢信息安全防线
  • IoC容器深度解析:架构、原理与实现
  • 区块链开发协作工具全景图:从智能合约管理到去中心化治理