数据结构算法刷题笔记——一、极简随记
- 1 题目类型与结题方法汇总
- ----------------数组、链表-----------------
- ----------------队列、栈-----------------
- 2 数据类型汇总
- 2.1 vectro容器
- 2.2 ListNode链表结构
- 2.3 unordered_map 哈希表
- 2.4 map 哈希表
- 2.5 Arrays.sort(nums1)
- 2.6 new()与delete()
- 2.7 stack栈
- 2.8 Queue普通队列
- 2.9 MontonicQueue 单调队列
- 2.10 deque队列
- 2.11 priority_queue优先级队列(二叉堆)
- 2.12 reverse(res.begin(), res.end)
- 2.13 unordered_set
- 2.14 upper_bound
- 2.15 binary_search
- 2.16 *max_element(dp.begin(), dp.end());
- 2.17 sort(matrix.begin(), matrix.end(), [](const auto& e1, const auto& e2) {
- 2.18 lower_bound和upper_bound( )
- 2.19 memset(array, 0, sizeof(array))
- 3 注意事项汇总
1 题目类型与结题方法汇总
----------------数组、链表-----------------
1.1 前缀和
前缀和:用于快速、频繁地计算一个索引区间内地元素之和(原始数组不会被修改的情况下)
1.1.1 一维数组中的前缀和
问题:求一维数组,索引[left, right]区间的和,时间复杂度O(1)
解法:
- 思路:以空间来换时间,空间复杂度O(n),时间复杂度O(1)
- 新建一个preSum数组
- 在类的构造函数中直接计算并求出各元素的前缀和
- 在求索引区间和的时候直接(right前缀和 - left前缀和)即可
1.1.2 二维数组中的前缀和
问题:求二维矩阵,左上角索引[row1, col1],右下角索引[roe2, col2],区间内的元素和
解法
- 思路:以空间来换时间,空间复杂度O(n),时间复杂度O(1)
- 新建一个二维preSum矩阵,存放以原点为顶点的矩阵的元素之和
- 在构造函数中,直接计算并存储 [0, 0] 到 [i, j] 的区域元素和
- 通过几次加减法运算出任何一个子矩阵的元素和
1.2 差分数组
差分数组:频繁对原始数组的某个区间的元素进行增减少
问题:输⼊⼀个数组 nums,给区间 nums[i…j] 加上val,求得变化后的nums数组
解法:
- 思路:类似前缀和,空间换时间
- 构造差分数组diff
- ,在构造函数内便计算diff,diff存储nums[i] 和nums[i-1]的差
- diff[i] += val 与 diff[j] -= val
- 反推回新的nums
1.3 双指针——链表题目
1.3.1 合并两个有序链表
问题:将两个升序链表合并成一个新的升序链表并返回
解法:
- 思路:使用双指针,分别指向两个链表,通过判定条件合并为一个链表
1.3.2 单链表的分解
问题:给一个链表的头结点head和一个特定值x,对链表进行分隔,将小于x的结点都出现在大于等于x的结点之前。并保留两个分区内每个结点的初始相对位置
解法:
- 思路:使用双指针,将链表按特定值x从头到尾分为两个链表,最后将两个链表再合并为一个链表
1.3.3 合并k个有序链表
问题:给一个链表数组,每个链表都按升序排列,将所有链表合并到一个升序链表中,返回合并后的链表
难点:逻辑类似于合并两个有序链表,难点在于,如何快速得到k个结点的最小结点
解法:
- 思路:用到优先级队列(二叉堆),把链表结点放入一个最小堆,每次获得k个结点中的最小结点
- 创建一个新的链表
- 创建一个优先级队列
- 将链表数组中的每个链表的头指针存储到优先级队列中
- 循环,将优先级队列顶部元素(最小元素)出栈,合并到链表中,将出栈元素的下一个元素(非空时)载入到优先级队列中,循环
- 得到合并后的链表
1.3.4 单链表的倒数第k个结点
问题:寻找单链表倒数第k个结点(不知道总共节点数n)
解法:
- 思路:双指针,只遍历一次数组,算出倒数第k个结点
- 让指针p1指向链表头结点head,走k步
- 然后,让指针p2指向链表头结点head
- p1和p2一起向前走,p1走到null(走n-k步)
- 此时p2停留在n-k+1个结点上,也就是倒数第k个结点
1.3.5 快慢指针
1.3.5.1 单链表的中点
问题:寻找单链表的重点元素(不知道总共节点数n)
解法:
思路:快慢指针,两个指针都指向头结点,每次向前走不同步数,通过步数关系,快指针走到nul时,慢指针指向对应元素位置
- slow指针和fast指针分别指向头结点head
- slow每次前进一步,fast每次前进两步
- 直到,fast走到链表末尾
- slow指向链表中点
- 注意:链表长度为偶数时,中点有两个,该方法返回的是靠后的那个结点
1.3.5.2 判断链表是否包含环
问题:判断链表中是否存在环
解法:
- 思路:快慢指针,慢指针前进一步,快指针前进两步,fast遇到空指针,说明链表中没有环,如果fast和slow相遇,说明链表中有环(fast超过了slow一圈)
1.3.5.3 判断链表中环的起点
问题:如果链表含有环,计算这个环的起点
解法:
- 思路:快慢指针
- 先,计算快慢指针的相遇点
- 然后,让慢指针指向头结点
- 然后,快慢指针以相同速度前进
- 再次遇到时,结点位置就是环开始的位置
1.3.6 两个链表是否相交
问题:输入两个链表的头结点headA和headB,两个链表是否相交?空间复杂度O(1),时间负责度O(n)
解法:
- 难点:关注两个链表长度不同,两个链表的结点无法对应。
- 思路:让p1和p2指针在两个链表上前进,可以同时到达相交结点c1
- 让p1遍历完A之后,开始遍历链表B;让p2遍历完B之后,开始遍历链表A
- 这样拼接,可以让p1和p2同时进入相交结点c1
- 如果相较于nul则说明没有交点,相较于c1非空,则说明有交点
1.4 双指针——数组题目
双指针:
- 左右指针:两个指针相向而行或者相背而行
- 快满指针:两个指针同向而行,一快一慢
单链表:大部分快快漫之阵
数组:把索引当作指针,使用双指针技巧
1.4.1 快慢指针——原地修改数组
数组问题快慢指针:常用来原地修改数组
1.4.1.1 删除有序数组中的重复项
问题:给定一个排序数组,在原地删除重复出现元素,使每个元素只出现依次,返回移除后数组的新长度。
- 不使用额外数组空间,O(1)额外空间点的条件下原地修改数组
解法: - 思路:快慢指针,原地修改。
- 慢指针走在后面,快指针走在前面
- fast找到一个不重复元素,让slow前进一步,并赋值给slow
- (先前进再赋值)
- 对于删除有序链表中的重复项
1.4.1.2 移除元素
问题:给一个数组nums和一个值val,移除所有数值等于val的元素,并返回移除后数组新长度。
- 不使用额外数组空间,使用O(1)额外空间,原地修改数组,元素顺序可变,不许考虑超出重新长度的元素
解法: - 思路:快慢指针,原地删除
- fast和slow均指向头结点
- 如果fast遇到val,则直接跳过,如果遇到非val,则赋值给slow,并让slow前进一步
- 最后新数组长度就是slow指
- (先赋值再前进)
1.4.2 快慢指针——滑动窗口
滑动窗⼝算法的快慢指针特性:
- left 指针在后,right 指针在前,两个指针中间的部分就是「窗⼝」,算法通过扩⼤和缩⼩「窗⼝」来解决某些问题。
- 时间复杂度O(n)
- 很多时候都是在处理字符串相关的问题
滑动窗口框架:
/* 滑动窗⼝算法框架 */
void slidingWindow(string s) {
// ⽤合适的数据结构记录窗⼝中的数据
unordered_map<char, int> window;
int left = 0, right = 0;
while (right < s.size()) {
// c 是将移⼊窗⼝的字符
char c = s[right];
window.add(c)
// 增⼤窗⼝
right++;
// 进⾏窗⼝内数据的⼀系列更新
...
/********************/
// 判断左侧窗⼝是否要收缩
while (left < right && window needs shrink) {
// d 是将移出窗⼝的字符
char d = s[left];
window.remove(d)
// 缩⼩窗⼝
left++;
// 进⾏窗⼝内数据的⼀系列更新
...
}
}
}
1.4.2.1 最小覆盖子串
问题:给一个字符串s、一个字符串t。返回s中覆盖t所有字符的最小子串。如果s中不存在涵盖t所有字符的子串,则返回空字符串“”。
解法:
-思路:滑动窗口,双指针,左右指针
- 在字符串S中使用双指针的左右指针,初始化 left = right = 0,把索引左闭右开区间[left,right)称为一个窗口
- 先不断增加right,扩大窗口,知道窗口中的字符串符合要求(包含了T中的所有字符)(寻找可行解)
- 停止right,不断增加left,缩小窗口,直到窗口中的字符串不再符合要求(不包含T中的所有字符)(优化可行解)
- 每次增加left,更新一轮结果(寻找最小子串)
- 重复2、3、4步骤,直到right到达字符串S的尽头
示例:
6. 首先,初始化 window 和 need 两个哈希表,记录窗⼝中的字符和需要凑⻬的字符:
7. 然后,使⽤ left 和 right 变量初始化窗⼝的两端,不要忘了,区间 [left, right) 是左闭右开的,所以初始情况下窗⼝没有包含任何元素。其中 valid 变量表示窗⼝中满⾜ need 条件的字符个数,如果 valid 和 need.size 的⼤⼩相同,则说明窗⼝已满⾜条件,已经完全覆盖了串 T。
string minWindow(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
// 记录最⼩覆盖⼦串的起始索引及⻓度
int start = 0, len = INT_MAX;
while (right < s.size()) {
// c 是将移⼊窗⼝的字符
char c = s[right];
// 扩⼤窗⼝
right++;
// 进⾏窗⼝内数据的⼀系列更新
if (need.count(c)) {
window[c]++;
if (window[c] == need[c])
valid++;
}
// 判断左侧窗⼝是否要收缩
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);
}
1.4.3 左右指针
1.4.3.1 二分查找
二分查找场景:寻找一个数、寻找左侧边界、寻找右侧边界
⼆分思维的精髓:通过已知信息尽可能多地收缩(折半)搜索空间,从⽽增加穷举效率,快速找到⽬标。
注意:需要注意细节问题
- 整形溢出(left + (right - left) / 2),防止left和right太大相加溢出
- 区间设置,左右都闭,统一
- 边界设置、移动设置
- mid加一还是减一
- while <= 还是 <
- 不要出现else,把所有情况用else if写清楚
- …部分就是可能出现细节问题的地方
二分查找框架:
int binarySearch(int[] nums, int target) {
int left = 0, right = ...;
while(...) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
...
} else if (nums[mid] < target) {
left = ...
} else if (nums[mid] > target) {
right = ...
}
}
return ...;
}
1.4.3.1.1 查找一个数
问题:搜索一个数,如果存在,返回其索引,否则返回-1
解法:
- 思路:二分查找
- left和right索引分别指向数组两端
- 循环判断,中间值nums[mid]是否是想要的值
- 不是的话根据值判断相关left右移还是right左移
- 找到值就返回,找不到返回-1
int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; // 注意
while(left <= right) {
int mid = left + (right - left) / 2;
if(nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1; // 注意
else if (nums[mid] > target)
right = mid - 1; // 注意
}
return -1;
}
缺陷:只能找到索引值,但是不能确定索引值是哪个位置
例子:1 2 2 2 3,得到2,为索引2的2值,还有索引1和索引3的2值
1.4.3.1.2 寻找左侧边界的二分搜索
问题:寻找一个数组中一个数的左侧边界
解法:
- 思路:在原始二分查找的基础上做一定改进
int left_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
// 搜索区间为 [left, right]
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
// 搜索区间变为 [mid+1, right]
left = mid + 1;
} else if (nums[mid] > target) {
// 搜索区间变为 [left, mid-1]
right = mid - 1;
} else if (nums[mid] == target) {
// 收缩右侧边界
right = mid - 1;
}
}
// 判断 target 是否存在于 nums 中
// 如果越界,target 肯定不存在,返回 -1
if (left < 0 || left >= nums.length) {
return -1;
}
// 判断⼀下 nums[left] 是不是 target
return nums[left] == target ? left : -1;
}
1.4.3.1.3 寻找右侧边界的二分搜索
int right_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 这⾥改成收缩左侧边界即可
left = mid + 1;
}
}
// 最后改成返回 right
if (right < 0 || right >= nums.length) {
return -1;
}
return nums[right] == target ? right : -1;
}
1.4.3.2 两数之和
问题:给一个小标从1开始的整数数组,数组按非递减顺序排列,从数组中找出满足相加之和等于目标数tarhet的两个数。假设只对应唯一答案,且不可以重复使用相同元素
解法:
- 思路:数组有序——双指针,类似于二分查找左右指针,从左右两端向中间逐渐left和right靠近,看对应和何时和target相等
1.4.3.3 反转数组
问题:给定一个字符串,将字符串反转过来
解法:
- 思路:左右指针,left和right从左右分别向中间靠近,交换left和right对应的元素,到中间后,二者便交换完毕
1.4.3.4 回文串判断
回文串:正着读和反着读都一样的字符串
问题:给定一个字符串,判断是不是回文串
解法:
- 思路:左右指针,left和right从左右分别向中间靠近,left和right对应的元素出现不同,则不是回文串,到中间后还相同,则是回文串
1.4.3.5 最长回文子串判断
问题:给一个字符串s,找到s中最长的回文子串
解法:
- 思路:
- 左右指针,left和right从中间向两端运动,left和right对应的元素出现不同,则不是回文串,到中间后还相同,则是回文串
- 中间指s的任意一个元素,遍历之后得到最长的回文子串
- 回⽂串的的⻓度可能是奇数也可能是偶数,解决该问题的核⼼是从中⼼向两端扩散的双指针技巧。
1.5 优势最大化——田忌赛马
问题:给长度相等的数组nums1和nums2,重新组织nums1中元素的位置,使得nums1的优势最大化。如果nums1[i] > nums2[i],就说nums1在索引i上对nums2[i]有优势,优势最大化就是重新组织nums1,尽可能多的让nums1[i] > nums2[i]
解法:
思路:将nums1和nums2的⻢按照战⽃⼒排序,然后按照排名⼀⼀对⽐。如果nums2的⻢能赢,那就⽐赛,如果赢不了,那就换个垫底的来送⼈头,保存实⼒。
- 借助优先级队列PriorityQueue来对nums2进行降序排列,同时存储其对应元素与数组索引的关系
- 将nums1进行升序排列
- 利用左右指针,比较PriorityQueue头部元素与nums1最大值,如果nums1最大值大,则取出存储到新数组对应PriorityQueue元素索引位置,否则找nums1中最小的来存储
1.6 链表操作的递归思维
- 递归的思想相对迭代思想,稍微有点难以理解,处理的技巧是:不要跳进递归,⽽是利⽤明确的定义来实现算法逻辑。
- 处理看起来⽐较困难的问题,可以尝试化整为零,把⼀些简单的解法进⾏修改,解决困难的问题。
- 递归操作链表并不⾼效
- 迭代解法:时间复杂度都是 O(N),空间复杂度是 O(1)
- 递归解法:时间复杂度都是 O(N),空间复杂度是 O(N)
- 所以递归操作链表可以作为对递归算法的练习或者拿去和⼩伙伴装逼,但是考虑效率的话还是使⽤迭代算法更好。
1.6.1 反转整个链表
问题:输入一个单链表头结点,将链表反转,返回新的头结点
解法:
思路:纯递归实现。
// 定义:输⼊⼀个单链表头结点,将该链表反转,返回新的头结点
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;
}
1.6.2 反转链表前n个点
问题:输入一个单链表头结点,将链表前n个点反转,返回新的头结点
解法:
- 思路:纯递归实现。
ListNode successor = null; // 后驱节点
// 反转以 head 为起点的 n 个节点,返回新的头结点
ListNode reverseN(ListNode head, int n) {
if (n == 1) {
// 记录第 n + 1 个节点
successor = head.next;
return head;
}
// 以 head.next 为起点,需要反转前 n - 1 个节点
ListNode last = reverseN(head.next, n - 1);
head.next.next = head;
// 让反转之后的 head 节点和后⾯的节点连起来
head.next = successor;
return last;
}
1.6.3 反转链表一部分
问题:输入一个单链表头结点,将链表[m, n]点反转,返回新的头结点
解法:
- 思路:纯递归实现。
- ⾸先,如果 m == 1,就相当于反转链表开头的 n 个元素嘛,也就是我们刚才实现的功能:
- 如果 m != 1 怎么办?如果我们把 head 的索引视为 1,那么我们是想从第 m 个元素开始反转对吧;如果把head.next 的索引视为 1 呢?那么相对于 head.next,反转的区间应该是从第 m - 1 个元素开始的;那么对于 head.next.next 呢……
ListNode reverseBetween(ListNode head, int m, int n) {
// base case
if (m == 1) {
return reverseN(head, n);
}
// 前进到反转的起点触发 base case
head.next = reverseBetween(head.next, m - 1, n - 1);
return head;
}
----------------队列、栈-----------------
队列和栈是操作受限的数据结构
- 队列和栈底层是数组和链表封装的
- 只暴漏了头尾操作的API
- 队列:先进先出
- 队列主要用在BFS算法
- 栈:后进先出
- 主要用在括号相关的问题
1.1 判断有效括号
1.1.1 判断有效括号
问题:输入一个字符串,其中包含[] () {} 流中括号,请你判断这个字符串组成的括号是否有效
解法
- 思路:利用栈的数据结构,遇到左括号就入栈,遇到右括号就去栈中寻找最近的左括号,看是否匹配,全部匹配则有效,出现一个不匹配则无效
bool isValid(string str) {
stack<char> left;
for (char c : str) {
if (c == '(' || c == '{' || c == '[')
left.push(c);
else { // 字符 c 是右括号
if (!left.empty() && leftOf(c) == left.top())
left.pop();
else
// 和最近的左括号不匹配
return false;
}
}
// 是否所有的左括号都被匹配了
return left.empty();
}
char leftOf(char c) {
if (c == '}') return '{';
if (c == ')') return '(';
return '[';
}
1.1.2 平衡括号串
问题:输入一个字符串s,在其中任意位置插入左括号(或者右括号),至少需要插入几次才能使得s变成一个有效的括号串
解法:
- 思路:以左括号为基准,通过维护对右括号的需求数need,来计算最小的插入次数
int minAddToMakeValid(string s) {
// res 记录插⼊次数
int res = 0;
// need 变量记录右括号的需求量
int need = 0;
for (int i = 0; i < s.size(); i++) {
if (s[i] == '(') {
// 对右括号的需求 + 1
need++;
}
if (s[i] == ')') {
// 对右括号的需求 - 1
need--;
if (need == -1) {
need = 0;
// 需插⼊⼀个左括号
res++;
}
}
}
return res + need;
}
1.1.3 平衡括号串 II
问题:输入一个字符串s,在其中任意位置插入左括号(或者右括号),至少需要插入几次才能使得s变成一个有效的括号串.(假设1个左括号需要匹配2个右括号才叫做有效的括号组合)
解法:
- 思路:以左括号为基准,通过维护对右括号的需求数need,来计算最小的插入次数(只不过need的+和-的数量有改变)
1.2 单调栈结构
- **栈(stack)**是很简单的⼀种数据结构,先进后出的逻辑顺序,符合某些问题的特点,⽐如说函数调⽤栈。
- 单调栈实际上就是栈,只是利⽤了⼀些巧妙的逻辑,使得每次新元素⼊栈后,栈内的元素都保持有序(单调递增或单调递减)
- 单调栈⽤途不太⼴泛,只处理⼀类典型的问题,⽐如「下⼀个更⼤元素」,「上⼀个更⼩元素」等
单调栈模板:
- for 循环要从后往前扫描元素,因为我们借助的是栈的结构,倒着⼊栈,其实是正着出栈。
- while 循环是把两个「个⼦⾼」元素之间的元素排除,因为他们的存在没有意义,前⾯挡着个「更⾼」的元素,所以他们不可能被作为后续进来的元素的下⼀个更⼤元素了。
- 算法的复杂度只有 O(n)
int[] nextGreaterElement(int[] nums) {
int n = nums.length;
// 存放答案的数组
int[] res = new int[n];
Stack<Integer> s = new Stack<>();
// 倒着往栈⾥放
for (int i = n - 1; i >= 0; i--) {
// 判定个⼦⾼矮
while (!s.isEmpty() && s.peek() <= nums[i]) {
// 矮个起开,反正也被挡着了。。。
s.pop();
}
// nums[i] 身后的更⼤元素
res[i] = s.isEmpty() ? -1 : s.peek();
s.push(nums[i]);
}
return res;
}
1.2.1 如何处理环形数组
问题:一个数组是环形的,计算其中每个元素的下一个更大元素
解法:
- 思路:数组长度扩大二倍,通过%运算符求模(余数),来模拟环形
1.3 单调队列结构
单调队列:⼀个「队列」,使⽤了⼀点巧妙的⽅法,使得队列中的元素全都是**单调递增(或递减)**的。
- 优先级队列⽆法满⾜标准队列结构「先进先出」的时间顺序
- 优先级队列底层利⽤⼆叉堆对元素进⾏动态排序,元素的出队顺序是元素的⼤⼩顺序,和⼊队的先后顺序完全没有关系。
- 需要⼀种新的队列结构,既能够维护队列元素「先进先出」的时间顺序,⼜能够正确维护队列中所有元素的最值,这就是**「单调队列」结构**。
- 「单调队列」这个数据结构主要⽤来辅助解决滑动窗⼝相关的问题
1.3.1 滑动窗口最大值
问题:给一个数组nums和一个正整数k,有一个大小为k的窗口在nums上从左至右滑动,输出每次窗口中k个元素的最大值
解法:
- 思路:「单调队列」的核⼼思路和「单调栈」类似,push ⽅法依然在队尾添加元素,但是要把前⾯⽐⾃⼰⼩的元素都删掉。
- 考虑如何构件MonotonicQueue类
/* 单调队列的实现 */
class MonotonicQueue {
LinkedList<Integer> maxq = new LinkedList<>();
public void push(int n) {
// 将⼩于 n 的元素全部删除
while (!maxq.isEmpty() && maxq.getLast() < n) {
maxq.pollLast();
}
// 然后将 n 加⼊尾部
maxq.addLast(n);
}
public int max() {
return maxq.getFirst();
}
public void pop(int n) {
if (n == maxq.getFirst()) {
maxq.pollFirst();
}
}
}
/* 解题函数的实现 */
int[] maxSlidingWindow(int[] nums, int k) {
MonotonicQueue window = new MonotonicQueue();
List<Integer> res = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
if (i < k - 1) {
//先填满窗⼝的前 k - 1
window.push(nums[i]);
} else {
// 窗⼝向前滑动,加⼊新数字
window.push(nums[i]);
// 记录当前窗⼝的最⼤值
res.add(window.max());
// 移出旧数字
window.pop(nums[i - k + 1]);
}
}
// 需要转成 int[] 数组再返回
int[] arr = new int[res.size()];
for (int i = 0; i < res.size(); i++) {
arr[i] = res.get(i);
}
return arr;
}
1.4 去除重复字母
问题:给一个仅包含字母的字符串,取出字符串中重复的字母,使得每个字母只出现一次,需保证返回结果的字典序最小,要求不能打乱其他字符的相对位置
- 要求⼀、要去重
- 要求⼆、去重字符串中的字符顺序不能打乱 s 中字符出现的相对顺序
- 要求三、在所有符合上⼀条要求的去重字符串中,字典序最⼩的作为最终结果
class Solution {
public:
string removeDuplicateLetters(string s) {
stack<char> stk;
// 维护一个计数器记录字符串中字符的数量
// 因为输入为 ASCII 字符,大小 256 够用了
int count[256] = {0};
for(int i = 0; i < s.size(); i++) {
count[s[i]]++;
}
bool inStack[256] = {false};
for(char c : s) {
//每遍历过一个字符,都将对应的计数减一
count[c]--;
if(inStack[c]) continue;
while(!stk.empty() && stk.top() > c) {
//若之后不存在栈顶元素了,则停止pop
if(count[stk.top()] == 0) {
break;
}
inStack[stk.top()] = false;
stk.pop();
}
stk.push(c);
inStack[c] = true;
}
string res;
while(!stk.empty()) {
res.push_back(stk.top());
stk.pop();
}
reverse(res.begin(), res.end());
return res;
}
};
2 数据类型汇总
2.1 vectro容器
#inlcude
using namespace std;
- vector
- vector<vector>
2.2 ListNode链表结构
// 单链表节点的结构
public class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
2.3 unordered_map 哈希表
2.4 map 哈希表
2.5 Arrays.sort(nums1)
2.6 new()与delete()
2.7 stack栈
2.8 Queue普通队列
2.9 MontonicQueue 单调队列
2.10 deque队列
2.11 priority_queue优先级队列(二叉堆)
2.12 reverse(res.begin(), res.end)
#include
2.13 unordered_set
2.14 upper_bound
2.15 binary_search
2.16 *max_element(dp.begin(), dp.end());
2.17 sort(matrix.begin(), matrix.end(), [](const auto& e1, const auto& e2) {
return e1[0] < e2[0] || (e1[0] == e2[0] && e1[1] > e2[1]);
});
2.18 lower_bound和upper_bound( )
2.19 memset(array, 0, sizeof(array))
3 注意事项汇总
3.1 虚拟头结点
- ⼀个链表的算法题中是很常⻅的「虚拟头结点」技巧,也就是 dummy 节点。
- 可以试试,如果不使⽤ dummy 虚拟节点,代码会复杂⼀些,需要额外处理指针 p 为空的情况。
- ⽽有了 dummy 节点这个占位符,可以避免处理空指针的情况,降低代码的复杂性。
- 当你需要创造⼀条新链表的时候,可以使⽤虚拟头结点简化边界情况的处理
3.2 delete(ptr)释放无用链表结点
- C++ 这类语⾔没有⾃动垃圾回收的机制,确实需要我们编写代码时⼿动释放掉这些节点的内存。