正式开始: 下面按照题目-我的解答思路和代码-代码随想录给出的讲解 的顺序写的,给出的讲解大部分是直接粘过来的,代码都是直接粘的,有的可能会加上自己的理解,当笔记记录的,欢迎讨论。
思维上一般都不难,主要考察对代码的掌控能力。
可以通过下标索引的方式获取到下标对应的数据。
正是因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。
(使用C++的话,要注意vector 和 array的区别,vector的底层实现是array,严格来讲vector是容器,不是数组。)
数组的元素是不能删的,只能覆盖。
二维数组在内存的空间地址:
C++中二维数组在地址空间上是连续的。
像Java是没有指针的,同时也不对程序员暴露其元素的地址,寻址操作完全交给虚拟机。
题目很简单,但是太久不写了,还是有很多问题:
我的最终通过代码:
class Solution {
public:
int search(vector<int>& nums, int target) {
int flag=-1;
if(sizeof(nums)==0){
return flag;
}
int a=0;
int b=nums.size()-1;
int nb = b;
if(b==0){
if(target==nums[0]){
return 0;
}
}
// if(target<nums[a] or target>nums[b]){
// b=0;
// }
while(b>=a){
int mid=(b-a)/2+a;
if(target==nums[mid]){
flag = mid;
break;
}
else if(target>nums[mid]){
a=mid+1;
}else{
b=mid-1;
}
}
return flag;
}
};
题目多了一个插入,也是有序无重复元素(符合二分法条件),也不太难
最终通过的代码:
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int flag=0;
if(nums.size()==0){
return flag;
}
int a=0;
int b=nums.size()-1;
// if(target<nums[a] or target>nums[b]){
// b=0;
// }
while(b>a){
int mid=(b-a)/2+a;
if(target==nums[mid]){
flag = mid;
return flag;
}
else if(target>nums[mid]){
a=mid+1;
}else{
b=mid-1;
}
}
if(b==a){
if(target<=nums[b]){
flag = b;
}else{
flag = b+1;
}
}
else if(b<a){
flag = a;
}
return flag;
}
};
首先第一个想法当然是暴力,有序的从头到尾来一遍就行。但是时间复杂度O(n),需要的是O(log n)。相当于先找区间所在的范围,然后分别在区间对半的两边找开始和结束,begin=end=mid:左边找开始,也就是第一个比这个数小的,小则移动left,相等则移动begin;右边找结束,也就是第一个比这个数大的,大则移动right,相等则移动end。
代码实现刚开始超出时间限制了,后来发现确实是有重复比较,因此设置两个_0来更新比较位置,已通过:
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int begin=-1;
int end=-1;
// if(nums.size()==0){
// return {begin, end};
// }
int a=0;
int b=nums.size()-1;
while(b>=a){
int mid=(b-a)/2+a;
if(target==nums[mid]){
begin=end=mid;
break;
}
else if(target>nums[mid]){
a=mid+1;
}else{
b=mid-1;
}
}
if(b==a){
return {begin, end};
}
else{
int begin0=begin-1;
int end0=end+1;
while(begin0>=a){
int mid=(begin0-a)/2+a;
if(target==nums[mid]){
begin=mid;
begin0=mid-1;
}
else{
a=mid+1;
}
}
while(b>=end0){
int mid=(b-end0)/2+end0;
if(target==nums[mid]){
end=mid;
end0=mid+1;
}else{
b=mid-1;
}
}
}
return {begin, end};
}
};
# 方法1
class Solution {
public:
int mySqrt(int x) {
int flag=2;
if(x == 0){
return 0;
}else if(x<=3){
return 1;
}
long a=2;
long b=x/2;
while(b>a){
long mid=(b-a)/2+a;
if(x == mid * mid){
flag = mid;
break;
}
else if(x > mid * mid){
flag = mid;
a=mid+1;
}else{
b=mid-1;
}
}
if(a==b){
if(x >= a * a){
flag = a; //保留可能的最小的那一个
}
}
return flag;
}
};
# 方法2
class Solution {
public:
int mySqrt(int x) {
int flag=2;
if(x == 0){
return 0;
}else if(x<=3){
return 1;
}
long a=2;
long b=x/2;
while(b>=a){
long mid=(b-a)/2+a;
if(x == mid * mid){
flag = mid;
return flag;
}
else if(x > mid * mid){
flag = mid;
a=mid+1;
}else{
b=mid-1;
}
}
flag = b;
return flag;
}
};
class Solution {
public:
bool isPerfectSquare(int num) {
if(num <= 1){
return true;
}else if(num<=3){
return false;
}
long a=2;
long b=num/2;
while(b>=a){
long mid=(b-a)/2+a;
if(num == mid * mid){
return true;
}
else if(num > mid * mid){
a=mid+1;
}else{
b=mid-1;
}
}
return false;
}
};
思路是先排序后移动,但是这样也不快,因为位置都要换;
暴力就是找到一个移动一次,也不行;
之前学过类似的,已经忘了,确实记得有指针,忘了怎么用的了,看解析想起来了。
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int a=0;
int b=nums.size();
int num=0;
for(int i=0;i<b;i++){
if(nums[i]==val){
// a=i;
}else{
num++;
nums[a]=nums[i];
a++;
}
}
return num;
}
};
就是看解析想起来的方法
双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
定义快慢指针
双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组、链表、字符串等操作的面试题,都使用双指针法。
和刚刚的方法一样,就是每一次循环换一下目标函数罢了。
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int a=1;
int b=nums.size();
int num=1;
int val=nums[0];
for(int i=1;i<b;i++){
if(nums[i]==val){
// a=i;
}else{
val=nums[i];
num++;
nums[a]=nums[i];
a++;
}
}
return num;
}
};
和之前的方法一样,就是将目标换为0,然后将后面的元素也换为0。
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int a=0;
int b=nums.size();
int num=0;
for(int i=0;i<b;i++){
if(nums[i]==0){
num++;
// a=i;
}else{
nums[a]=nums[i];
a++;
}
}
for(int i=b-1;i>=b-num;i--){
nums[i]=0;
}
}
};
class Solution {
public:
bool backspaceCompare(string s, string t) {
int num1=0;
int num2=0;
int a1=0;
int a2=0;
int b1=s.size();
int b2=t.size();
for(int i=0;i<b1;i++){
if(s[i]=='#'){
if(num1>0){
a1--;
num1--;
}
}else{
num1++;
s[a1]=s[i];
a1++;
}
}
for(int i=0;i<b2;i++){
// printf("%d,%d,%d",i,a2,num2);
if(t[i]=='#'){
if(num2>0){
a2--;
num2--;
}
}else{
num2++;
t[a2]=t[i];
a2++;
}
// printf("%d,%d,%d",i,a2,num2);
}
// printf("%d %d",num2,num1);
if(num1==num2){
for(int i=0;i<num1;i++){
if(s[i]!=t[i]){
return false;
}
}
}
else{
return false;
}
return true;
}
};
自己的方法可能导致负数里的大数字没有完全比较,看了一眼提示结合自己想的,应该在负数可能出现的范围内查找,也就是从0开始找到第一个比当前数小的停止,此前有比这个数大的就换回来,没有则这个数直接平方。
然后又思考了一下,我现在的想法是右边的一个一个往后移动,每一次放入应该的最大值,不是最大的就和最左的换,问题在于最左的换过一次后可能就不再是最大的了,因此应当在替换时替换到合适的位置。------超出时间限制。应该是插入到前面需要每次都测试一次的原因。
继续分析,后面几位比当前的值大,一定比之后的几个最右的数大,因此可以不移动而直接一同转移。------忽视了全是负数无法“掉头”的情况,分析半天,我这样的方法最终还是得排序。
不如空间换时间,直接构造一个新的数组,然后两头找大的放进去就行了。谁能想到想了这么久,是空间换时间呢
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
// // int b=0;
// int a=nums.size()-1;
// while(a>=0){
// if(abs(nums[a])>=abs(nums[0])){
// // printf("%d", nums[a]);
// nums[a] = pow(abs(nums[a]),2);
// // printf("%d", nums[a]);
// a--;
// }else{
// int temp=abs(nums[0]);
// int now=nums[a];
// nums[0]=nums[a];
// nums[a] = pow(temp,2);
// a--;
// int k=0;
// for(int b=1;b<a+1;b++){
// if(abs(now)<abs(nums[b])){
// k=b;
// }
// }
// //为了确保现在的0位就是最大的,还需要移过去k位
// for(int i=1;i<=k;i++){
// // temp=abs(nums[k]);
// // nums[k]=now;
// temp=abs(nums[i]);
// nums[i]=nums[a];
// now=nums[a];
// nums[a] = pow(temp,2);
// a--;
// //更新k值
// for(int j=k+1;j<a+1;j++){
// if(abs(now)<abs(nums[j])){
// k=j;
// }
// }
// }
// // nums[k]=now;
// }
// }
// return nums;
int n = nums.size();
vector<int>res(n);
int a=0;
int b=n-1;
for(int i=n-1;i>=0;i--){
if(abs(nums[a])<abs(nums[b])){
res[i]=pow(abs(nums[b]),2);
b--;
}else{
res[i]=pow(abs(nums[a]),2);
a++;
}
}
return res;
}
};
1.0 十大经典排序算法 | 菜鸟教程 (runoob.com)
就还是上面那道题。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int n = nums.size();
int flag=0;
int num=n;
// vector<int>res(n);
for(int i=0;i<n;i++){
int now=0;
int sum=0;
for(int j=i;j<n;j++){
if(now>=num){
break;
}
sum+=nums[j];
now++;
if(sum>=target){
flag=1;
if(now<num){
num=now;
}
break;
}
}
}
if(flag==1){
return num;
}else{
return 0;
}
}
};
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int n = nums.size();
int flag=0;
int num=n;
//初始化
int now=1;
int sum=nums[0];
if(sum>=target){
flag=1;
if(now<num){
num=now;
}
}
// 循环开始
for(int i=0;i<n;i++){
if(i!=0){ //每次更新窗户口头
sum-=nums[i-1];
now-=1;
if(sum>=target){ //多一个判断
flag=1;
if(now<num){
num=now;
}
continue;
}
}
//判断是否需要更新窗口尾
for(int j=i+now;j<n;j++){
if(now>=num){
break;
}
sum+=nums[j];
now++;
if(sum>=target){
flag=1;
if(now<num){
num=now;
}
break;
}
}
}
//判断窗口是否存在
if(flag==1){
return num;
}else{
return 0;
}
}
};
就是说不需要每次从头开始扫,只需要当前满足长度了,下一次减一,减去开头的长度,加上后面的看够不够就行,代码就是上面给的。
滑动窗口也可以理解为双指针法的一种!只不过这种解法更像是一个窗口的移动,所以叫做滑动窗口更适合一些。
在本题中实现滑动窗口,主要确定如下三点:
窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。
窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。
窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是for循环里的索引。
解题的关键在于 窗口的起始位置如何移动
发现**滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)暴力解法降为O(n)。**
不要以为for里放一个while就以为是O(n^2)啊, 主要是看每一个元素被操作的次数,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被操作两次,所以时间复杂度是 2 × n 也就是O(n)。
这个代码写的也非常好,就是一看就是理解透彻了写出来的,我自己的就是看明白了改出来的:(首先长度是计算出来的,其次for循环决定后面的长度,while决定开始的长度。)注意通过什么来控制窗口范围,这里是相加和
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX;
int sum = 0; // 滑动窗口数值之和
int i = 0; // 滑动窗口起始位置
int subLength = 0; // 滑动窗口的长度
for (int j = 0; j < nums.size(); j++) {
sum += nums[j];
// 注意这里使用while,每次更新 i(起始位置),并不断比较子序列是否符合条件
while (sum >= s) {
subLength = (j - i + 1); // 取子序列的长度
result = result < subLength ? result : subLength;
sum -= nums[i++]; // 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置)
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
};
一样的,就是要变成计算三个值,然后找其中两种和的最大值,多了一步。写了一圈错了,因为水果种类可以有无数种。直接去看题解了。
思路直接就错了,还是跟上一个一样,我是计算的长度,但是这道题里长度其实就是果子数量,因此应当保留长度,也就是通过窗口当前的果子种类,而不应当保存每种的数量去找。
下面是官方的代码,写的真好,简单明了,通过当前出现的果子类别的数量来控制滑动的窗口大小。等回来自己重写一下试试,主要是c++的其他数据结构怎么用早忘完了。
class Solution {
public:
int totalFruit(vector<int>& fruits) {
int n = fruits.size();
unordered_map<int, int> cnt;
int left = 0, ans = 0;
for (int right = 0; right < n; ++right) {
++cnt[fruits[right]];
while (cnt.size() > 2) {
auto it = cnt.find(fruits[left]);
--it->second;
if (it->second == 0) {
cnt.erase(it);
}
++left;
}
ans = max(ans, right - left + 1);
}
return ans;
}
};
class Solution {
public:
unordered_map <char, int> ori, cnt;
bool check() { //比较是否完全包含
for (const auto &p: ori) {
if (cnt[p.first] < p.second) {
return false;
}
}
return true;
}
string minWindow(string s, string t) {
for (const auto &c: t) { //初始化,记录t中都有什么有多长
++ori[c];
}
int l = 0, r = -1;
int len = INT_MAX, ansL = -1, ansR = -1;
while (r < int(s.size())) {
if (ori.find(s[++r]) != ori.end()) { //find()在字符串中找对应的元素,没有则返回end()
++cnt[s[r]];
}
while (check() && l <= r) { //猜测没有数值时,check也可能返回true,因此需要有后面的条件?
if (r - l + 1 < len) { //更新
len = r - l + 1;
ansL = l;
}
if (ori.find(s[l]) != ori.end()) {
--cnt[s[l]];
} // 缩小窗口
++l;
}
}
return ansL == -1 ? string() : s.substr(ansL, len);
}
};
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
int ni=n,nj=n;
int ni0=0,nj0=0;
int num=1;
int i=0,j=0;
vector<vector<int>> res(n, vector<int>(n));
while(num<=n*n){
i=ni0;
j=nj0;
for(;j<nj;j++){
if(num>n*n){
break;
}
res[i][j]=num;
num++;
printf("%d %d %d",i,j,num);
}
j--;
ni0++;
for(i++;i<ni;i++){
if(num>n*n){
break;
}
res[i][j]=num;
num++;
printf("%d %d %d",i,j,num);
}
i--;
nj--;
for(j--;j>=nj0;j--){
if(num>n*n){
break;
}
res[i][j]=num;
num++;
printf("%d %d %d",i,j,num);
}
j++;
ni--;
for(i--;i>=ni0;i--){
if(num>n*n){
break;
}
res[i][j]=num;
num++;
printf("%d %d %d",i,j,num);
}
i++;
nj0++;
}
return res;
}
};
很简单,上面代码复制过来就好。
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
int m=matrix.size(),n=matrix[0].size();
vector<int> res(m*n);
int now=0;
int ni=m,nj=n;
int ni0=0,nj0=0;
int i=0,j=0;
while(now<n*m){
i=ni0;
j=nj0;
for(;j<nj;j++){
if(now>=n*m){
break;
}
res[now++]=matrix[i][j];
// printf("%d %d %d",i,j,num);
}
j--;
ni0++;
for(i++;i<ni;i++){
if(now>=n*m){
break;
}
res[now++]=matrix[i][j];
// printf("%d %d %d",i,j,num);
}
i--;
nj--;
for(j--;j>=nj0;j--){
if(now>=n*m){
break;
}
res[now++]=matrix[i][j];
// printf("%d %d %d",i,j,num);
}
j++;
ni--;
for(i--;i>=ni0;i--){
if(now>=n*m){
break;
}
res[now++]=matrix[i][j];
// printf("%d %d %d",i,j,num);
}
i++;
nj0++;
}
return res;
}
};
LCR 146. 螺旋遍历二维数组 - 力扣(LeetCode)
和上题是一道题,区别在于存在0输入。
class Solution {
public:
vector<int> spiralArray(vector<vector<int>>& array) {
int m=array.size();
if(m==0){
vector<int> res(0);
return res;
}
int n=array[0].size();
vector<int> res(m*n);
int now=0;
int ni=m,nj=n;
int ni0=0,nj0=0;
int i=0,j=0;
while(now<n*m){
i=ni0;
j=nj0;
for(;j<nj;j++){
if(now>=n*m){
break;
}
res[now++]=array[i][j];
// printf("%d %d %d",i,j,num);
}
j--;
ni0++;
for(i++;i<ni;i++){
if(now>=n*m){
break;
}
res[now++]=array[i][j];
// printf("%d %d %d",i,j,num);
}
i--;
nj--;
for(j--;j>=nj0;j--){
if(now>=n*m){
break;
}
res[now++]=array[i][j];
// printf("%d %d %d",i,j,num);
}
j++;
ni--;
for(i--;i>=ni0;i--){
if(now>=n*m){
break;
}
res[now++]=array[i][j];
// printf("%d %d %d",i,j,num);
}
i++;
nj0++;
}
return res;
}
};
数组是非常基础的数据结构,在面试中,是必考的基础数据结构,考察数组的题目一般在思维上都不难,主要是考察对代码的掌控能力。数组的题目在思想上一般比较简单的,但是如果想高效,并不容易。
数组是存放在连续内存空间上的相同类型数据的集合。
需要两点注意的是
正是因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。
使用C++的话,要注意vector 和 array的区别,vector的底层实现是array,严格来讲vector是容器,不是数组。
数组的元素是不能删的,只能覆盖。
Java的二维数组在内存中不是 3*4 的连续地址空间,而是四条连续的地址空间组成!
二分法:可以使用暴力解法,通过这道题目,如果追求更优的算法,建议试一试用二分法,来解决这道题目
在这道题目中我们讲到了循环不变量原则,只有在循环中坚持对区间的定义,才能清楚的把握循环中的各种细节。
二分法是算法面试中的常考题,建议通过这道题目,锻炼自己手撕二分的能力。
双指针法:(快慢指针法):通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
这道题目迷惑了不少同学,纠结于数组中的元素为什么不能删除,主要是因为以下两点:
双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法。
滑动窗口:数组操作中的另一个重要思想
本题中,主要要理解滑动窗口如何移动 窗口起始位置,达到动态更新窗口大小的,从而得出长度最小的符合条件的长度。
滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。
如果没有接触过这一类的方法,很难想到类似的解题思路,滑动窗口方法还是很巧妙的。
模拟行为:模拟类的题目在数组中很常见,不涉及到什么算法,就是单纯的模拟,十分考察大家对代码的掌控能力。
在这道题目中,我们再一次介绍到了循环不变量原则,其实这也是写程序中的重要原则。
真正解决题目的代码都是简洁的,或者有原则性的,大家可以在这道题目中体会到这一点。
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链表的入口节点称为链表的头结点也就是head。
单链表中的指针域只能指向节点的下一个节点。
双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
双链表 既可以向前查询也可以向后查询。
循环链表,顾名思义,就是链表首尾相连。可以用来解决约瑟夫环问题。
数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。
链表是通过指针域的指针链接在内存中各个节点。
所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
链表的定义:
// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
//不定义构造函数,C++默认生成一个构造函数,这个构造函数不会初始化任何成员变量(因为无参)
删除节点:next指针 指向别的节点就可以了,在C++里最好是再手动释放这个节点,释放这块内存,其他语言例如Java、Python,就有自己的内存回收机制,就不用自己手动释放了。
要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。
添加节点:链表的增添和删除都是O(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* removeElements(ListNode* head, int val) {
ListNode* head_res = new ListNode(0);
do{ //首先找到第一个不等于val的节点
if(head == NULL){
return NULL;
}
if(head->val != val){
head_res->next=head;
break;
}
head=head->next;
}while(head_res->next==NULL);
while(head->next!=NULL){ //删除后面的元素
if(head->next->val==val){
head->next = head->next->next;
}else{
head=head->next;
}
}
return head_res->next;
}
};
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
// 删除头结点
while (head != NULL && head->val == val) { // 注意这里不是if
ListNode* tmp = head;
head = head->next;
delete tmp;
}
// 删除非头结点
ListNode* cur = head;
while (cur != NULL && cur->next!= NULL) {
if (cur->next->val == val) {
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
} else {
cur = cur->next;
}
}
return head;
}
};
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
dummyHead->next = head; // 将虚拟头结点指向head,这样方便后面做删除操作
ListNode* cur = dummyHead;
while (cur->next != NULL) {
if(cur->next->val == val) {
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
} else {
cur = cur->next;
}
}
head = dummyHead->next;
delete dummyHead;
return head;
}
};
没啥可说的,我的代码不如官方给的。
感觉很容易算错的就是现在应当是第几个,因为有头(虚拟的,只是提供一个指向),这样有时候不需要插入操作的需要额外判断(尤其注意方法:addAtIndex)。
class MyLinkedList {
public:
// 定义链表节点结构体
struct LinkedNode {
int val;
LinkedNode* next;
LinkedNode(int val):val(val), next(nullptr){}
};
// 初始化链表
MyLinkedList() {
_dummyHead = new LinkedNode(0); // 这里定义的头结点 是一个虚拟头结点,而不是真正的链表头结点
}
int get(int index) {
LinkedNode* cur= _dummyHead;
for(int i=0;i<=index;i++){
if(cur->next==NULL){
return -1;
}
cur=cur->next;
}
return cur->val;
}
void addAtHead(int val) {
LinkedNode* newNode=new LinkedNode(val);
newNode->next=_dummyHead->next;
_dummyHead->next=newNode;
}
void addAtTail(int val) {
LinkedNode* newNode=new LinkedNode(val);
LinkedNode* cur=_dummyHead;
while(cur->next!=NULL){
cur=cur->next;
}
cur->next=newNode;
}
void addAtIndex(int index, int val) {
LinkedNode* newNode=new LinkedNode(val);
LinkedNode* cur=_dummyHead;
int i=0;
for(;i<index;i++){
if(cur->next==NULL){
break;
}
cur=cur->next;
}
if(i==index){ //需要注意不是全部时候都需要插入操作的!
newNode->next=cur->next;
cur->next=newNode;
}
}
void deleteAtIndex(int index) {
LinkedNode* cur=_dummyHead;
for(int i=0;i<index;i++){
if(cur->next==NULL){
break;
}
cur=cur->next;
}
LinkedNode* de=cur->next;
if(cur->next!=NULL){
cur->next=cur->next->next;
}
delete de;
}
private:
LinkedNode* _dummyHead;
};
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList* obj = new MyLinkedList();
* int param_1 = obj->get(index);
* obj->addAtHead(val);
* obj->addAtTail(val);
* obj->addAtIndex(index,val);
* obj->deleteAtIndex(index);
*/
直接看代码:
class MyLinkedList {
public:
// 定义链表节点结构体
struct LinkedNode {
int val;
LinkedNode* next;
LinkedNode(int val):val(val), next(nullptr){}
};
// 初始化链表
MyLinkedList() {
_dummyHead = new LinkedNode(0); // 这里定义的头结点 是一个虚拟头结点,而不是真正的链表头结点
_size = 0;
}
// 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点
int get(int index) {
if (index > (_size - 1) || index < 0) {
return -1;
}
LinkedNode* cur = _dummyHead->next;
while(index--){ // 如果--index 就会陷入死循环
cur = cur->next;
}
return cur->val;
}
// 在链表最前面插入一个节点,插入完成后,新插入的节点为链表的新的头结点
void addAtHead(int val) {
LinkedNode* newNode = new LinkedNode(val);
newNode->next = _dummyHead->next;
_dummyHead->next = newNode;
_size++;
}
// 在链表最后面添加一个节点
void addAtTail(int val) {
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
while(cur->next != nullptr){
cur = cur->next;
}
cur->next = newNode;
_size++;
}
// 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果index大于链表的长度,则返回空
// 如果index小于0,则在头部插入节点
void addAtIndex(int index, int val) {
if(index > _size) return;
if(index < 0) index = 0;
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
while(index--) {
cur = cur->next;
}
newNode->next = cur->next;
cur->next = newNode;
_size++;
}
// 删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的
void deleteAtIndex(int index) {
if (index >= _size || index < 0) {
return;
}
LinkedNode* cur = _dummyHead;
while(index--) {
cur = cur ->next;
}
LinkedNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
//delete命令指示释放了tmp指针原本所指的那部分内存,
//被delete后的指针tmp的值(地址)并非就是NULL,而是随机值。也就是被delete后,
//如果不再加上一句tmp=nullptr,tmp会成为乱指的野指针
//如果之后的程序不小心使用了tmp,会指向难以预想的内存空间
tmp=nullptr;
_size--;
}
// 打印链表
void printLinkedList() {
LinkedNode* cur = _dummyHead;
while (cur->next != nullptr) {
cout << cur->next->val << " ";
cur = cur->next;
}
cout << endl;
}
private:
int _size;
LinkedNode* _dummyHead;
};
/**
* 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* reverseList(ListNode* head) {
ListNode* head_re=new ListNode();
head_re->next=head;
if(head==NULL){ //需要先判断当前head是否为NULL才能判断->next是否为NULL
return head_re->next;
}
while(head->next!=NULL){
ListNode* cur=head->next;
head->next=head->next->next;
cur->next=head_re->next;
head_re->next=cur;
}
return head_re->next;
}
};
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* temp; // 保存cur的下一个节点
ListNode* cur = head;
ListNode* pre = NULL;
while(cur) {
temp = cur->next; // 保存一下 cur的下一个节点,因为接下来要改变cur->next
cur->next = pre; // 翻转操作
// 更新pre 和 cur指针
pre = cur;
cur = temp;
}
return pre;
}
};
class Solution {
public:
ListNode* reverse(ListNode* pre,ListNode* cur){
if(cur == NULL) return pre;
ListNode* temp = cur->next;
cur->next = pre;
// 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步
// pre = cur;
// cur = temp;
return reverse(cur,temp);
}
ListNode* reverseList(ListNode* head) {
// 和双指针法初始化是一样的逻辑
// ListNode* cur = head;
// ListNode* pre = NULL;
return reverse(NULL, 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* swapPairs(ListNode* head) {
if(head==NULL or head->next==NULL){
return head;
}
ListNode* head_re= new ListNode();//处理头节点
head_re->next=head;
ListNode* save= head_re;
while(head!=NULL and head->next!=NULL){//处理之后的
ListNode* pre=head;
ListNode* cur=head->next;
head=cur->next;
cur->next=pre;
pre->next=head;
save->next=cur;
save=pre;
}
return head_re->next;
}
};
19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)
/**
* 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* head_re=new ListNode();
head_re->next=head;
ListNode* len=head;
int num=0;
head=head_re;
while(len!=NULL){
len=len->next;
num++;
}
n = num-n;
for(int i=0;i<n;i++){
if(head->next==NULL){
return head_re->next;
}
head=head->next;
}
if(head->next==NULL){
return head_re->next;
}
ListNode* cur=head->next;
head->next=head->next->next;
delete cur;
return head_re->next;
}
};
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* slow = dummyHead;
ListNode* fast = dummyHead;
while(n-- && fast != NULL) {
fast = fast->next;
}
fast = fast->next; // fast再提前走一步,因为需要让slow指向删除节点的上一个节点
while (fast != NULL) {
fast = fast->next;
slow = slow->next;
}
slow->next = slow->next->next;
// ListNode *tmp = slow->next; C++释放内存的逻辑
// slow->next = tmp->next;
// delete nth;
return dummyHead->next;
}
};
面试题 02.07. 链表相交 - 力扣(LeetCode)
/**
* 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* node=new ListNode();
// node->next=NULL;
while(headA!=NULL and headB!=NULL){
ListNode* cur=headB;
while(cur!=NULL){
if(headA == cur){
return headA;
}
cur=cur->next;
}
headA=headA->next;
}
return NULL;
}
};
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* curA = headA;
ListNode* curB = headB;
int lenA = 0, lenB = 0;
while (curA != NULL) { // 求链表A的长度
lenA++;
curA = curA->next;
}
while (curB != NULL) { // 求链表B的长度
lenB++;
curB = curB->next;
}
curA = headA;
curB = headB;
// 让curA为最长链表的头,lenA为其长度
if (lenB > lenA) {
swap (lenA, lenB);
swap (curA, curB);
}
// 求长度差
int gap = lenA - lenB;
// 让curA和curB在同一起点上(末尾位置对齐)
while (gap--) {
curA = curA->next;
}
// 遍历curA 和 curB,遇到相同则直接返回
while (curA != NULL) {
if (curA == curB) {
return curA;
}
curA = curA->next;
curB = curB->next;
}
return NULL;
}
};
此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。
假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。
因为就按照一起进入环计算,慢的走一半的时候,快的肯定走完一圈了,那么慢的另外一半的时候,快的肯定还能再走一圈,因此如果有环那一定在慢的还没有走完第一圈时就能相遇。
也不可能跳过,跳过一定是fast在环内slow的后面,在后面两个时,下一步fast会走到当前slow的位置,然后slow向前走一步,fast变成在slow后面一个位置上,向前走两步,跨过slow一个,但是下一步slow向前走,两者正好遇上。(文章中解释的是相对于slow,fast是每次走一个位置,感觉不太好理解,简单模拟一下就好了)
现在就可以考虑计算环的入口了:
相遇时: slow指针走过的节点数为: x + y
, fast指针走过的节点数:x + y + n (y + z)
,n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。
因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 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的时候,公式就化解为 x = z
,这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。
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;
do{
for(int i=0;i<2;i++){
if(fast==NULL){
return NULL;
}
fast=fast->next;
}
slow=slow->next;
}while(fast!=slow);
//找入口
slow=head;
while(slow!=fast){
slow=slow->next;
fast=fast->next;
}
return fast;
}
};
//代码随想录
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head;
ListNode* slow = head;
while(fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
// 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇
if (slow == fast) {
ListNode* index1 = fast;
ListNode* index2 = head;
while (index1 != index2) {
index1 = index1->next;
index2 = index2->next;
}
return index2; // 返回环的入口
}
}
return NULL;
}
};
虚拟头节点:链表操作中一个非常总要的技巧:虚拟头节点。链表的一大问题就是操作当前节点必须要找前一个节点才能操作。这就造成了,头结点的尴尬,因为头结点没有前一个节点了。每次对应头结点的情况都要单独处理,所以使用虚拟头结点的技巧,就可以解决这个问题。
链表的基本操作:设计链表把链表常见的五个操作练习了一遍,是练习链表基础操作的非常好的一道题目,可以说把这道题目做了,链表基本操作就OK了,再也不用担心链表增删改查整不明白了。
反转链表:两种反转的方式,迭代法和递归法。
建议大家先学透迭代法,然后再看递归法,因为递归法比较绕,如果迭代还写不明白,递归基本也写不明白了。
可以先通过迭代法,彻底弄清楚链表反转的过程!
删除倒数第N个节点:结合虚拟头结点 和 双指针法来移除链表倒数第N个节点
链表相交:用双指针来找到两个链表的交点(引用完全相同,即:内存地址完全相同的交点)
环形链表:在链表如何找环,以及如何找环的入口位置
哈希表是根据关键码的值而直接进行访问的数据结构。其实直白来讲其实数组就是一张哈希表。
哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素
哈希表能解决什么问题呢,一般哈希表都是用来快速判断一个元素是否出现集合里,枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
将元素映射到哈希表上就涉及到了hash function ,也就是哈希函数。
哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
如果hashCode得到的数值大于 哈希表的大小了,也就是大于tableSize了,怎么办呢?
此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,这样我们就保证了学生姓名一定可以映射到哈希表上了。
此时问题又来了,哈希表我们刚刚说过,就是一个数组。如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下标的位置。小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞。一般哈希碰撞有两种解决方法, 拉链法和线性探测法。
拉链法:发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了。(数据规模是dataSize, 哈希表的大小为tableSize)
其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
线性探测法:使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 否 | 否 | O(log n) | O(log n) |
std::multiset | 红黑树 | 有序 | 是 | 否 | O(logn) | O(logn) |
std::unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1 |
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
那么再来看一下map ,在map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。
映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::map | 红黑树 | key有序 | key不可重复 | key不可修改 | O(logn) | O(logn) |
std::multimap | 红黑树 | key有序 | key可重复 | key不可修改 | O(log n) | O(log n) |
std::unordered_map | 哈希表 | key无序 | key不可重复 | key不可修改 | O(1) | O(1) |
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。
其他语言例如:java里的HashMap ,TreeMap 都是一样的原理。可以灵活贯通。
class Solution {
public:
bool isAnagram(string s, string t) {
vector<int> s_(26);
vector<int> t_(26);
for(int i=0;i<26;i++){
s_[i]=t_[i]=0;
}
for(int i=0;i<s.size();i++){
s_[s[i]-'a']++;
}
for(int i=0;i<t.size();i++){
t_[t[i]-'a']++;
}
for(int i=0;i<26;i++){
if(s_[i]!=t_[i]){
return false;
}
}
return true;
}
};
暴力的解法,两层for循环,同时还要记录字符是否重复出现,很明显时间复杂度是 O(n^2)。
数组其实就是一个简单哈希表,而且这道题目中字符串只有小写字符,那么就可以定义一个数组,来记录字符串s里字符出现的次数。
定义一个数组叫做record用来上记录字符串s里字符出现的次数。
需要把字符映射到数组也就是哈希表的索引下标上,因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25。
那看一下如何检查字符串t中是否出现了这些字符,同样在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作(这样更优,因为只需要定义一个哈希表)。
那么最后检查一下,record数组如果有的元素不为零0,说明字符串s和t一定是谁多了字符或者谁少了字符,return false。
时间复杂度为O(n),空间上因为定义是的一个常量大小的辅助数组,所以空间复杂度为O(1)。
没思路,看的题解:
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> mp;
for (string& str: strs) {
string key = str;
sort(key.begin(), key.end());
mp[key].emplace_back(str);//vector 容器中添加了新的方法:emplace_back() ,和 push_back() 一样的是都是在容器末尾添加一个新的元素进去,不同的是 emplace_back() 在效率上相比较于 push_back() 有了一定的提升。
}
vector<vector<string>> ans;
for (auto it = mp.begin(); it != mp.end(); ++it) {
ans.emplace_back(it->second);
}
return ans;
}
};
代码注释:C++中push_back和emplace_back的区别 - 知乎 (zhihu.com)
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
// 自定义对 array<int, 26> 类型的哈希函数
auto arrayHash = [fn = hash<int>{}] (const array<int, 26>& arr) -> size_t {
return accumulate(arr.begin(), arr.end(), 0u, [&](size_t acc, int num) {
return (acc << 1) ^ fn(num);
});
};
unordered_map<array<int, 26>, vector<string>, decltype(arrayHash)> mp(0, arrayHash);
for (string& str: strs) {
array<int, 26> counts{};
int length = str.length();
for (int i = 0; i < length; ++i) {
counts[str[i] - 'a'] ++;
}
mp[counts].emplace_back(str);
}
vector<vector<string>> ans;
for (auto it = mp.begin(); it != mp.end(); ++it) {
ans.emplace_back(it->second);
}
return ans;
}
};
438. 找到字符串中所有字母异位词 - 力扣(LeetCode)
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int sLen = s.size(), pLen = p.size();
if (sLen < pLen) {
return vector<int>();
}
vector<int> ans;
vector<int> sCount(26);
vector<int> pCount(26);
for (int i = 0; i < pLen; ++i) {
++sCount[s[i] - 'a'];
++pCount[p[i] - 'a'];
}
if (sCount == pCount) {
ans.emplace_back(0);
}
for (int i = 0; i < sLen - pLen; ++i) {
--sCount[s[i] - 'a'];
++sCount[s[i + pLen] - 'a'];
if (sCount == pCount) {
ans.emplace_back(i + 1);
}
}
return ans;
}
};
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int sLen = s.size(), pLen = p.size();
if (sLen < pLen) {
return vector<int>();
}
vector<int> ans;
vector<int> count(26);
for (int i = 0; i < pLen; ++i) {
++count[s[i] - 'a'];
--count[p[i] - 'a'];
}
int differ = 0;
for (int j = 0; j < 26; ++j) {
if (count[j] != 0) {//这里的物理含义并非有几个不相同,而是一共有多少不同,因为两个字符串中的字符都会被算上
++differ;
}
}
if (differ == 0) {
ans.emplace_back(0);
}
for (int i = 0; i < sLen - pLen; ++i) {
if (count[s[i] - 'a'] == 1) { // 窗口中字母 s[i] 的数量与字符串 p 中的数量从不同变得相同
--differ;
} else if (count[s[i] - 'a'] == 0) { // 窗口中字母 s[i] 的数量与字符串 p 中的数量从相同变得不同
++differ;
}
--count[s[i] - 'a'];
if (count[s[i + pLen] - 'a'] == -1) { // 窗口中字母 s[i+pLen] 的数量与字符串 p 中的数量从不同变得相同
--differ;
} else if (count[s[i + pLen] - 'a'] == 0) { // 窗口中字母 s[i+pLen] 的数量与字符串 p 中的数量从相同变得不同
++differ;
}
++count[s[i + pLen] - 'a'];
if (differ == 0) {
ans.emplace_back(i + 1);
}
}
return ans;
}
};
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_map<int, int> mp;
for(int i=0;i<nums1.size();i++){
mp[nums1[i]]=1;
}
vector<int> ans;
for(int i=0;i<nums2.size();i++){
if(mp.find(nums2[i]) != mp.end()){ //存在
if(--mp[nums2[i]]==0){
// printf("%d",nums2[i]);
ans.emplace_back(nums2[i]);
}
}
}
return ans;
}
};
这道题用暴力的解法时间复杂度是O(n^2),那来看看使用哈希法进一步优化。
用数组来做哈希表也是不错的选择,使用数组来做哈希的题目,是因为题目都限制了数值的大小。
而这道题目没有限制数值的大小,就无法使用数组来做哈希表了。
而且如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。
此时就要使用另一种结构体了,set ,关于set,C++ 给提供了如下三种可用的数据结构:
那有同学可能问了,遇到哈希问题我直接都用set不就得了,用什么数组啊。
直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的。
不要小瞧 这个耗时,在数据量大的情况,差距是很明显的。
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set; // 存放结果,之所以用set是为了给结果集去重
unordered_set<int> nums_set(nums1.begin(), nums1.end());//更为简洁的代码
for (int num : nums2) {
// 发现nums2的元素 在nums_set里又出现过
if (nums_set.find(num) != nums_set.end()) {
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};
350. 两个数组的交集 II - 力扣(LeetCode)
class Solution {
public:
vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
unordered_map<int, int> mp;
for(int i=0;i<nums1.size();i++){
if(mp.find(nums1[i]) == mp.end()){
mp[nums1[i]]=1;
}else{
mp[nums1[i]]++;
// printf("%d %d",nums1[i], mp[nums1[i]]);
}
}
vector<int> ans;
for(int i=0;i<nums2.size();i++){
if(mp.find(nums2[i]) != mp.end()){ //存在
if(--mp[nums2[i]]>=0){
ans.emplace_back(nums2[i]);
}
}
}
return ans;
}
};
class Solution {
public:
bool isHappy(int n) {
unordered_map<int, int> mp;
mp[n]=1;
int res=n;
while(n!=1){
int n1=0;
int k;
while(n!=0){//计算下一个结果
k=n%10;
n=int(n/10);
n1+=k*k;
}
// printf("%d", n1);
n=n1;
if(mp.find(n) != mp.end()){
return false;
}else{
mp[n]=1;
}
}
return true;
}
};
官方代码:
class Solution {
public:
// 取数值各个位上的单数之和
int getSum(int n) {
int sum = 0;
while (n) {
sum += (n % 10) * (n % 10);
n /= 10;
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> set;
while(1) {
int sum = getSum(n);
if (sum == 1) {
return true;
}
// 如果这个sum曾经出现过,说明已经陷入了无限循环了,立刻return false
if (set.find(sum) != set.end()) {
return false;
} else {
set.insert(sum);
}
n = sum;
}
}
};
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> mp;
for(int i=0;i<nums.size();i++){
mp[nums[i]]=i;
}
vector<int> ans(2);
for(int i=0;i<nums.size();i++){
if(mp.find(target - nums[i])!=mp.end() and mp[target - nums[i]]!=i){
ans[0]=i;
ans[1]=mp[target - nums[i]];
return ans;
}
}
return ans;
}
};
再强调一下 什么时候使用哈希法,当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。
本题呢,我就需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,某元素是否遍历过,也就是 是否出现在这个集合。
不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适。
使用数组和set来做哈希法的局限。
此时就要选择另一种数据结构:map ,map是一种key value的存储结构,可以用key保存数值,用value再保存数值所在的下标。
这道题目中并不需要key有序,选择std::unordered_map 效率更高! 使用其他语言的录友注意了解一下自己所用语言的数据结构就行。
接下来需要明确两点:
map目的用来存放我们访问过的元素,因为遍历数组的时候,需要记录我们之前遍历过哪些元素和对应的下标,这样才能找到与当前元素相匹配的(也就是相加等于target)
接下来是map中key和value分别表示什么。
这道题 我们需要 给出一个元素,判断这个元素是否出现过,如果出现过,返回这个元素的下标。
那么判断元素是否出现,这个元素就要作为key,所以数组中的元素作为key,有key对应的就是value,value用来存下标。
所以 map中的存储结构为 {key:数据元素,value:数组元素对应的下标}。
在遍历数组的时候,只需要向map去查询是否有和目前遍历元素匹配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中,因为map存放的就是我们访问过的元素。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
std::unordered_map <int,int> map;
for(int i = 0; i < nums.size(); i++) {
// 遍历当前元素,并在map中寻找是否有匹配的key
auto iter = map.find(target - nums[i]);
if(iter != map.end()) {
return {iter->second, i};
}
// 如果没找到匹配对,就把访问过的元素和下标加入到map中
map.insert(pair<int, int>(nums[i], i));
}
return {};
}
};
本题其实有四个重点:
把这四点想清楚了,本题才算是理解透彻了。
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int, int> mp;
for(int i =0;i<nums1.size();i++){
for(int j=0;j<nums2.size();j++){
mp[nums1[i]+nums2[j]]++;//不需要先判断再加入
}
}
int count=0;
for(int i =0;i<nums3.size();i++){
for(int j=0;j<nums4.size();j++){
if (mp.find(0-(nums3[i]+nums4[j])) != mp.end()) {
count+=mp[0-(nums3[i]+nums4[j])];
}
}
}
return count;
}
};
上面的代码就是我们参考这里给的思路,反思写上面了,这里不放了。
本题是使用哈希法的经典题目,这道题目是四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑有重复的四个元素相加等于0的情况。
如果本题想难度升级:就是给出一个数组(而不是四个数组),在这里找出四个元素相加等于0,答案中不可以包含重复的四元组,大家可以思考一下,后续的文章我也会讲到的。
本题解题步骤:
给的代码总是更简洁:
class Solution {
public:
int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) {
unordered_map<int, int> umap; //key:a+b的数值,value:a+b数值出现的次数
// 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中
for (int a : A) {
for (int b : B) {
umap[a + b]++;
}
}
int count = 0; // 统计a+b+c+d = 0 出现的次数
// 在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就把map中key对应的value也就是出现次数统计出来。
for (int c : C) {
for (int d : D) {
if (umap.find(0 - (c + d)) != umap.end()) {
count += umap[0 - (c + d)];
}
}
}
return count;
}
};
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
vector<int> mp(26);
for(int i=0;i<26;i++){
mp[i]=0;
}
for(int i=0;i<magazine.size();i++){
mp[magazine[i]-'a']++;
}
for(int i=0;i<ransomNote.size();i++){
if(--mp[ransomNote[i]-'a']<0){
return false;
}
}
return true;
}
};
这道题目和242.有效的字母异位词 (opens new window)很像,242.有效的字母异位词 (opens new window)相当于求 字符串a 和 字符串b 是否可以相互组成 ,而这道题目是求 字符串a能否组成字符串b,而不用管字符串b 能不能组成字符串a。
本题判断第一个字符串ransom能不能由第二个字符串magazines里面的字符构成,但是这里需要注意两点。
暴力解法就忽视了,两层for还需要擦除动作。
题目说只有小写字母,那可以采用空间换取时间的哈希策略,用一个长度为26的数组来记录magazine里字母出现的次数。
然后再用ransomNote去验证这个数组是否包含了ransomNote所需要的所有字母。
一些同学可能想,用数组干啥,都用map完事了,其实在本题的情况下,使用map的空间消耗要比数组大一些的,因为map要维护红黑树或者哈希表,而且还要做哈希函数,是费时的!数据量大的话就能体现出来差别了。 所以数组更加简单直接有效!
学习一下简洁的代码:
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int record[26] = {0};
//add
if (ransomNote.size() > magazine.size()) {
return false;
}
for (int i = 0; i < magazine.length(); i++) {
// 通过record数据记录 magazine里各个字符出现次数
record[magazine[i]-'a'] ++;
}
for (int j = 0; j < ransomNote.length(); j++) {
// 遍历ransomNote,在record里对应的字符个数做--操作
record[ransomNote[j]-'a']--;
// 如果小于零说明ransomNote里出现的字符,magazine没有
if(record[ransomNote[j]-'a'] < 0) {
return false;
}
}
return true;
}
};
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
// 找出a + b + c = 0
// a = nums[i], b = nums[j], c = -(a + b)
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零,那么不可能凑成三元组
if (nums[i] > 0) {
break;
}
if (i > 0 && nums[i] == nums[i - 1]) { //三元组元素a去重
continue;
}
unordered_set<int> set;
for (int j = i + 1; j < nums.size(); j++) {
if (j > i + 2
&& nums[j] == nums[j-1]
&& nums[j-1] == nums[j-2]) { // 三元组元素b去重
continue;
}
int c = 0 - (nums[i] + nums[j]);
if (set.find(c) != set.end()) {
result.push_back({nums[i], nums[j], c});
set.erase(c);// 三元组元素c去重
} else {
set.insert(nums[j]);
}
}
}
return result;
}
};
双指针: 其实这道题目使用哈希法并不十分合适,因为在去重的操作中有很多细节需要注意,在面试中很难直接写出没有bug的代码。而且使用哈希法 在使用两层for循环的时候,能做的剪枝操作很有限,虽然时间复杂度是O(n^2),也是可以在leetcode上通过,但是程序的执行时间依然比较长 。
接下来我来介绍另一个解法:双指针法,这道题目使用双指针法 要比哈希法高效一些,那么来讲解一下具体实现的思路。
首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。相当于 a = nums[i],b = nums[left],c = nums[right]。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
// 找出a + b + c = 0
// a = nums[i], b = nums[left], c = nums[right]
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
if (nums[i] > 0) {
return result;
}
// 错误去重a方法,将会漏掉-1,-1,2 这种情况
/*
if (nums[i] == nums[i + 1]) {
continue;
}
*/
// 正确去重a方法
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.size() - 1;
while (right > left) {
// 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left 了,从而漏掉了 0,0,0 这种三元组
/*
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
*/
if (nums[i] + nums[left] + nums[right] > 0) right--;
else if (nums[i] + nums[left] + nums[right] < 0) left++;
else {
result.push_back(vector<int>{nums[i], nums[left], nums[right]});
// 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
// 找到答案时,双指针同时收缩
right--;
left++;
}
}
}
return result;
}
};
补充:两数之和 就不能使用双指针法,因为两数之和要求返回的是索引下标, 而双指针法一定要排序,一旦排序之后原数组的索引就被改变了。
如果两数之和要求返回的是数值的话,就可以使用双指针法了。
去重:
a的去重:a 如果重复了怎么办,a是nums里遍历的元素,那么应该直接跳过去。
但这里有一个问题,是判断 nums[i] 与 nums[i + 1]是否相同,还是判断 nums[i] 与 nums[i-1] 是否相同。都是和 nums[i]进行比较,是比较它的前一个,还是比较它的后一个。
比较它的后一个:就把 三元组中出现重复元素的情况直接pass掉了。 例如{-1, -1 ,2} 这组数据,当遍历到第一个-1 的时候,判断 下一个也是-1,那这组数据就pass了。
我们要做的是 不能有重复的三元组,但三元组内的元素是可以重复的!
所以这里是有两个重复的维度,比较它的前一个:就是当前使用 nums[i],我们判断前一位是不是一样的元素,在看 {-1, -1 ,2} 这组数据,当遍历到 第一个 -1 的时候,只要前一位没有-1,那么 {-1, -1 ,2} 这组数据一样可以收录到 结果集里。
这是一个非常细节的思考过程。
b c的去重:去重的逻辑多加了 对right 和left 的去重:
while (right > left) {
if (nums[i] + nums[left] + nums[right] > 0) {
right--;
// 去重 right
while (left < right && nums[right] == nums[right + 1]) right--;
} else if (nums[i] + nums[left] + nums[right] < 0) {
left++;
// 去重 left
while (left < right && nums[left] == nums[left - 1]) left++;
} else {
}
}
这种去重其实对提升程序运行效率是没有帮助的。
拿right去重为例,即使不加这个去重逻辑,依然根据 while (right > left)
和 if (nums[i] + nums[left] + nums[right] > 0)
去完成right-- 的操作。
多加了 while (left < right && nums[right] == nums[right + 1]) right--;
这一行代码,其实就是把 需要执行的逻辑提前执行了,但并没有减少 判断的逻辑。
最直白的思考过程,就是right还是一个数一个数的减下去的,所以在哪里减的都是一样的。
所以这种去重 是可以不加的。 仅仅是 把去重的逻辑提前了而已。
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
sort(nums.begin(),nums.end());
vector<vector<int>> result;
if(nums.size()<4){
return result;
}
for(int i=0;i<nums.size()-3;i++){
// printf("%d %d",i, nums.size()-3);
//不能再加下面的判断了,因为不是每一个数都是正整数,因此当是负数时出错
// if(nums[i]>target){
// return result;
// }
if(nums[i]>=0 and nums[i]>target){
continue;
}
while(i>0 and nums[i]==nums[i-1]){
i++;
}
for(int j=i+1;j<nums.size()-2;j++){
while(j>i+1 and j<nums.size()-2 and nums[j]==nums[j-1]){
j++;
}
int left=j+1;
int right=nums.size()-1;
while(right>left){
// if(nums[i]+nums[j]+nums[left]+nums[right])>long(INT_MAX)){
// continue;
// }
if((long) nums[i]+nums[j]+nums[left]+nums[right]==target){
result.push_back(vector<int>{nums[i],nums[j], nums[left], nums[right]});
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
right--;
left++;
}else if((long) nums[i]+nums[j]+nums[left]+nums[right]>target){
right--;
}else if((long) nums[i]+nums[j]+nums[left]+nums[right]<target){
left++;
}
}
}
}
return result;
}
};
四数之和,和三数之和是一个思路,都是使用双指针法, 基本解法就是在三数之和 的基础上再套一层for循环
但是有一些细节需要注意,例如: 不要判断nums[k] > target
就返回了,三数之和 可以通过 nums[i] > 0
就返回了,因为 0 已经是确定的数了,四数之和这道题目 target是任意值。比如:数组是[-4, -3, -2, -1]
,target
是-10
,不能因为-4 > -10
而跳过。但是我们依旧可以去做剪枝,逻辑变成nums[i] > target && (nums[i] >=0 || target >= 0)
就可以了。
四数之和的双指针解法是两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下标作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况,三数之和的时间复杂度是O(n^2),四数之和的时间复杂度是O(n^3) 。四数之和的双指针解法就是将原本暴力O(n^4)的解法,降为O(n^3)的解法。
那么一样的道理,五数之和、六数之和等等都采用这种解法。
之前我们讲过哈希表的经典题目:454.四数相加II (opens new window),相对于本题简单很多,因为本题是要求在一个集合中找出四个数相加等于target,同时四元组不能重复。
而四数相加II是四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑有重复的四个元素相加等于0的情况,所以相对于本题还是简单了不少!
下面的代码还有很多需要学习的细节:比如二级剪枝;一级剪枝的方法;防止溢出的方法。
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for (int k = 0; k < nums.size(); k++) {
// 剪枝处理
if (nums[k] > target && nums[k] >= 0) {
break; // 这里使用break,统一通过最后的return返回
}
// 对nums[k]去重
if (k > 0 && nums[k] == nums[k - 1]) {
continue;
}
for (int i = k + 1; i < nums.size(); i++) {
// 2级剪枝处理
if (nums[k] + nums[i] > target && nums[k] + nums[i] >= 0) {
break;
}
// 对nums[i]去重
if (i > k + 1 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.size() - 1;
while (right > left) {
// nums[k] + nums[i] + nums[left] + nums[right] > target 会溢出
if ((long) nums[k] + nums[i] + nums[left] + nums[right] > target) {
right--;
// nums[k] + nums[i] + nums[left] + nums[right] < target 会溢出
} else if ((long) nums[k] + nums[i] + nums[left] + nums[right] < target) {
left++;
} else {
result.push_back(vector<int>{nums[k], nums[i], nums[left], nums[right]});
// 对nums[left]和nums[right]去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
// 找到答案时,双指针同时收缩
right--;
left++;
}
}
}
}
return result;
}
};
补充:二级剪枝的部分:
if (nums[k] + nums[i] > target && nums[k] + nums[i] >= 0) {
break;
}
可以优化为:
if (nums[k] + nums[i] > target && nums[i] >= 0) {
break;
}
因为只要 nums[k] + nums[i] > target,那么 nums[i] 后面的数都是正数的话,就一定 不符合条件了。
不过这种剪枝 其实有点 小绕,大家能够理解 文章给的完整代码的剪枝 就够了。
本篇我们从哈希表的理论基础到数组、set和map的经典应用,把哈希表的整个全貌完整的呈现给大家。
同时也强调虽然map是万能的,详细介绍了什么时候用数组,什么时候用set。
一般来说哈希表都是用来快速判断一个元素是否出现集合里。
对于哈希表,要知道哈希函数和哈希碰撞在哈希表中的作用。
哈希函数是把传入的key映射到符号表的索引上。
哈希碰撞处理有多个key映射到相同索引上时的情景,处理碰撞的普遍方式是拉链法和线性探测法。
接下来是常见的三种哈希结构:
在C++语言中,set 和 map 都分别提供了三种数据结构,每种数据结构的底层实现和用途都有所不同,在关于哈希表,你该了解这些! (opens new window)中我给出了详细分析,这一知识点很重要!
例如什么时候用std::set,什么时候用std::multiset,什么时候用std::unordered_set,都是很有考究的。
只有对这些数据结构的底层实现很熟悉,才能灵活使用,否则很容易写出效率低下的程序。
数组作为哈希表:数组就是简单的哈希表,但是数组的大小是受限的。
set作为哈希表:题目没有限制数值的大小,就无法使用数组来做哈希表了。主要因为如下两点:
数组的大小是有限的,受到系统栈空间(不是数据结构的栈)的限制。
如果数组空间够大,但哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。
做映射的话,就可以使用set了。关于set,C++ 给提供了如下三种可用的数据结构:(详情请看关于哈希表,你该了解这些! (opens new window))
std::set和std::multiset底层实现都是红黑树,std::unordered_set的底层实现是哈希, 使用unordered_set 读写效率是最高的,本题并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set。
map作为哈希表:map是一种<key, value>
的结构,本题可以用key保存数值,用value在保存数值所在的下标。所以使用map最为合适。使用数组和set来做哈希法的局限:
数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。
C++提供如下三种map:(详情请看关于哈希表,你该了解这些! (opens new window))
std::unordered_map 底层实现为哈希,std::map 和std::multimap 的底层实现是红黑树。
同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解),1.两数之和 (opens new window)中并不需要key有序,选择std::unordered_map 效率更高!
在454.四数相加 (opens new window)中我们提到了其实需要哈希的地方都能找到map的身影。
本题咋眼一看好像和18. 四数之和 (opens new window),15.三数之和 (opens new window)差不多,其实差很多!**关键差别是本题为四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑重复问题,而18. 四数之和 (opens new window),15.三数之和 (opens new window)是一个数组(集合)里找到和为0的组合,可就难很多了!**用哈希法解决了两数之和,很多同学会感觉用哈希法也可以解决三数之和,四数之和。其实是可以解决,但是非常麻烦,需要去重导致代码效率很低。
在15.三数之和 (opens new window)中我给出了哈希法和双指针两个解法,大家就可以体会到,使用哈希法还是比较麻烦的。所以18. 四数之和,15.三数之和都推荐使用双指针法!
class Solution {
public:
void reverseString(vector<char>& s) {
char temp;
for(int i=0,j=s.size()-1;i<j;i++,j--){
temp=s[i];
s[i]=s[j];
s[j]=temp;
}
return ;
}
};
用C++里的一个库函数 reverse,调一下直接完事了, 相信每一门编程语言都有这样的库函数。
**如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数。**毕竟面试官一定不是考察你对库函数的熟悉程度, 如果使用python和java 的同学更需要注意这一点,因为python、java提供的库函数十分丰富。
如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数。
字符串也是一种数组,所以元素在内存中是连续分布,这就决定了反转链表和反转字符串方式上还是有所差异的。
循环里只要做交换s[i] 和s[j]操作就可以了,那么我这里使用了swap 这个库函数。大家可以使用。swap可以有两种实现:
一种就是常见的交换数值:
int tmp = s[i];
s[i] = s[j];
s[j] = tmp;
s[i] ^= s[j];
s[j] ^= s[i];
s[i] ^= s[j];
class Solution {
public:
string reverseStr(string s, int k) {
int i=0;
while(i<s.size()){
if(i+k<s.size()){
reverse(s.begin()+i,s.begin()+i+k);
}else{
reverse(s.begin()+i,s.end());
}
i=i+k;
if(i+k<s.size()){
i+=k;
}else{
break;
}
}
return s;
}
};
class Solution {
public:
void reverse(string& s, int start, int end) {
for (int i = start, j = end; i < j; i++, j--) {
swap(s[i], s[j]);
}
}
string reverseStr(string s, int k) {
for (int i = 0; i < s.size(); i += (2 * k)) {
// 1. 每隔 2k 个字符的前 k 个字符进行反转
// 2. 剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符
if (i + k <= s.size()) {
reverse(s, i, i + k - 1);
continue;
}
// 3. 剩余字符少于 k 个,则将剩余字符全部反转。
reverse(s, i, s.size() - 1);
}
return s;
}
};
class Solution {
public:
string reverseStr(string s, int k) {
int n = s.size(),pos = 0;
while(pos < n){
//剩余字符串大于等于k的情况
if(pos + k < n) reverse(s.begin() + pos, s.begin() + pos + k);
//剩余字符串不足k的情况
else reverse(s.begin() + pos,s.end());
pos += 2 * k;
}
return s;
}
};
#include<iostream>
using namespace std;
int main(){
string s;
cin>>s;
int count=0;
int nows=s.size();
for(int i=0;i<nows;i++){
if(s[i]>='0' and s[i]<='9'){//别忘了等于
count++;
}
}
// if(count==0){ //小心这个输出格式会重复输出,输完要直接return
// cout<<s<<endl;
// }
s.resize(nows+count*5);//因为本身就有一个大小,只需要再扩大5个
int news=s.size();
for(int i=nows-1,j=news-1;j>i;j--,i--){//别忘了是size()-1
if(s[i]>='0' and s[i]<='9'){
s[j] = 'r';
s[j - 1] = 'e';
s[j - 2] = 'b';
s[j - 3] = 'm';
s[j - 4] = 'u';
s[j - 5] = 'n';
j -= 5;
}else{
s[j]=s[i];
}
}
cout<<s<<endl;
}
首先扩充数组到每个数字字符替换成 “number” 之后的大小,从后向前填充,也就是双指针法,过程如下:i指向新长度的末尾,j指向旧长度的末尾。
从前向后填充就是O(n^2)的算法了,因为每次添加元素都要将添加元素之后的所有元素整体向后移动。其实很多数组填充类的问题,其做法都是先预先给数组扩容带填充后的大小,然后在从后向前进行操作。
这么做有两个好处:
字符串是若干字符组成的有限序列,也可以理解为是一个字符数组,但是很多语言对字符串做了特殊的规定,在C语言中,把一个字符串存入一个数组时,也把结束符 ‘\0’存入数组,并以此作为该字符串是否结束的标志;在C++中,提供一个string类,string类会提供 size接口,可以用来判断string类字符串是否结束,就不用’\0’来判断是否结束。
那么vector< char > 和 string 又有什么区别呢?其实在基本操作上没有区别,但是 string提供更多的字符串处理的相关接口,例如string 重载了+,而vector却没有。所以想处理字符串,我们还是会定义一个string类型。
不知道为啥会用一个while输入,上面测试了去掉也能通过:
#include<iostream>
using namespace std;
int main() {
string s;
while (cin >> s) {
int count = 0; // 统计数字的个数
int sOldSize = s.size();
for (int i = 0; i < s.size(); i++) {
if (s[i] >= '0' && s[i] <= '9') {
count++;
}
}
// 扩充字符串s的大小,也就是每个空格替换成"number"之后的大小
s.resize(s.size() + count * 5);
int sNewSize = s.size();
// 从后先前将空格替换为"number"
for (int i = sNewSize - 1, j = sOldSize - 1; j < i; i--, j--) {
if (s[j] > '9' || s[j] < '0') {
s[i] = s[j];
} else {
s[i] = 'r';
s[i - 1] = 'e';
s[i - 2] = 'b';
s[i - 3] = 'm';
s[i - 4] = 'u';
s[i - 5] = 'n';
i -= 5;
}
}
cout << s << endl;
}
}
class Solution {
public:
string reverseWords(string s) {
if(s.size()==0){
return s;
}
int i=0,j=0;
if(s[i]==' '){ //处理开头空格
while(j<s.size() and s[j]==' '){
j++;
}
s[i]=s[j++];
}
for(;j<s.size();j++){ //处理中间空格
if(i!=j and (s[i]!=' ' or s[j]!=' ')){
s[++i]=s[j];
}
}
if(s[i]==' '){ //处理结尾空格
i--;
}
printf("%s",s.c_str());
s.resize(i+1);
for(int j=0,k=i;j<k;j++,k--){
s[k]^=s[j];
s[j]^=s[k];
s[k]^=s[j];
}
for(int j=0;j<=i;j++){
int m,k;
if(s[j]!=' '){
m=j;
while(j<=i and s[j]!=' '){
j++;
}
k=j-1;
for(;m<k;m++,k--){
s[k]^=s[m];
s[m]^=s[k];
s[k]^=s[m];
}
}
}
return s;
}
};
综合考察了字符串的多种操作。
一些同学会使用split库函数,分隔单词,然后定义一个新的string字符串,最后再把单词倒序相加,那么这道题题目就是一道水题了,失去了它的意义。
所以这里我还是提高一下本题的难度:**不要使用辅助空间,空间复杂度要求为O(1)。**不能使用辅助空间之后,那么只能在原字符串上下功夫了。想一下,我们将整个字符串都反转过来,那么单词的顺序指定是倒序了,只不过单词本身也倒序了,那么再把单词反转一下,单词不就正过来了。
所以解题思路如下:
遇到空格了就erase。如果不仔细琢磨一下erase的时间复杂度,还以为以上的代码是O(n)的时间复杂度呢。想一下真正的时间复杂度是多少,一个erase本来就是O(n)的操作。erase操作上面还套了一个for循环,那么以上代码移除冗余空格的代码时间复杂度为O(n^2)。那么使用双指针法来去移除空格,最后resize(重新设置)一下字符串的大小,就可以做到O(n)的时间复杂度。
有的同学可能发现用erase来移除空格,在leetcode上性能也还行。主要是以下几点;:
移除多余空格:这部分和27.移除元素 (opens new window)的逻辑是一样一样的,本题是移除空格,而 27.移除元素 就是移除元素。
将每个单词反转:这个实现我们分别在344.反转字符串 (opens new window)和541.反转字符串II (opens new window)里已经讲过了。
class Solution {
public:
void reverse(string& s, int start, int end){ //翻转,区间写法:左闭右闭 []
for (int i = start, j = end; i < j; i++, j--) {
swap(s[i], s[j]);
}
}
void removeExtraSpaces(string& s) {//去除所有空格并在相邻单词之间添加空格, 快慢指针。
int slow = 0; //整体思想参考https://programmercarl.com/0027.移除元素.html
for (int i = 0; i < s.size(); ++i) { //
if (s[i] != ' ') { //遇到非空格就处理,即删除所有空格。
if (slow != 0) s[slow++] = ' '; //手动控制空格,给单词之间添加空格。slow != 0说明不是第一个单词,需要在单词前添加空格。
while (i < s.size() && s[i] != ' ') { //补上该单词,遇到空格说明单词结束。
s[slow++] = s[i++];
}
}
}
s.resize(slow); //slow的大小即为去除多余空格后的大小。
}
string reverseWords(string s) {
removeExtraSpaces(s); //去除多余空格,保证单词之间之只有一个空格,且字符串首尾没空格。
reverse(s, 0, s.size() - 1);
int start = 0; //removeExtraSpaces后保证第一个单词的开始下标一定是0。
for (int i = 0; i <= s.size(); ++i) {
if (i == s.size() || s[i] == ' ') { //到达空格或者串尾,说明一个单词结束。进行翻转。
reverse(s, start, i - 1); //翻转,注意是左闭右闭 []的翻转。
start = i + 1; //更新下一个单词的开始下标start
}
}
return s;
}
};
#include<iostream>
using namespace std;
int main(){
string s;
int k;
cin>>k;
cin>>s;
for(int i=0,j=s.size()-1;i<j;j--,i++){
s[i]^=s[j];
s[j]^=s[i];
s[i]^=s[j];
}
for(int i=0,j=k-1;i<j;i++,j--){ //注意前半段需要反转的范围才是原来在后面的需要旋转的范围
s[i]^=s[j];
s[j]^=s[i];
s[i]^=s[j];
}
for(int i=k,j=s.size()-1;i<j;i++,j--){
s[i]^=s[j];
s[j]^=s[i];
s[i]^=s[j];
}
cout<<s<<endl;
return 0;
}
// 版本一
#include<iostream>
#include<algorithm>
using namespace std;
int main() {
int n;
string s;
cin >> n;
cin >> s;
int len = s.size(); //获取长度
reverse(s.begin(), s.end()); // 整体反转
reverse(s.begin(), s.begin() + n); // 先反转前一段,长度n
reverse(s.begin() + n, s.end()); // 再反转后一段
cout << s << endl;
}
// 版本二
#include<iostream>
#include<algorithm>
using namespace std;
int main() {
int n;
string s;
cin >> n;
cin >> s;
int len = s.size(); //获取长度
reverse(s.begin(), s.begin() + len - n); // 先反转前一段,长度len-n ,注意这里是和版本一的区别
reverse(s.begin() + len - n, s.end()); // 再反转后一段
reverse(s.begin(), s.end()); // 整体反转
cout << s << endl;
}
28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode)
class Solution {
public:
void get_next(int* next, string needle){
int j=0;
next[0]=0;
for(int i=1;i<needle.size();i++){
while(j>0 and needle[j]!=needle[i]){
j=next[j-1];
}
if(needle[j]==needle[i]){
j++;
}
next[i]=j;
}
}
int strStr(string haystack, string needle) {
// int flag=-1;
int next[needle.size()];
get_next(next, needle);
int i=0,j=0;
for(;i<haystack.size();i++){//不能用while因为这样可能导致i不会向后推移,最终陷入死循环
while(j>0 and needle[j]!=haystack[i]){
j=next[j-1];
}
if(needle[j]==haystack[i]){
j++;
if(j==needle.size()){
return i-j+1;
}
}
}
return -1;
}
};
KMP的经典思想就是:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。
把KMP的每一个细微的细节都扣了出来
class Solution {
public:
void getNext(int* next, const string& s) {
int j = 0;
next[0] = 0;
for(int i = 1; i < s.size(); i++) {
while (j > 0 && s[i] != s[j]) {
j = next[j - 1];
}
if (s[i] == s[j]) {
j++;
}
next[i] = j;
}
}
int strStr(string haystack, string needle) {
if (needle.size() == 0) {
return 0;
}
int next[needle.size()];
getNext(next, needle);
int j = 0;
for (int i = 0; i < haystack.size(); i++) {
while(j > 0 && haystack[i] != needle[j]) {
j = next[j - 1];
}
if (haystack[i] == needle[j]) {
j++;
}
if (j == needle.size() ) {
return (i - needle.size() + 1);
}
}
return -1;
}
};
视频版学习笔记放到另一篇中了。
class Solution {
public:
void get_next(int* next, string needle){
int j=0;
next[0]=0;
for(int i=1;i<needle.size();i++){
while(j>0 and needle[j]!=needle[i]){
j=next[j-1];
}
if(needle[j]==needle[i]){
j++;
}
next[i]=j;
}
}
bool repeatedSubstringPattern(string s) {
int next[s.size()];
get_next(next, s);
int i=s.size()-next[s.size()-1];
// int i=0;
// for(;i<s.size();i++){
// if(next[i]!=0){
// break;
// }
// }
if(i==0 or s.size()%i!=0){//表示没有重复的或者长度不是子串的倍数
return false;
}
for(int k=1,j=(k+1)*i-1;j<s.size();k++,j=(k+1)*i-1){
if(next[j]!=k*i){
return false;
}
}
return true;
}
};
暴力的解法, 就是一个for循环获取 子串的终止位置, 然后判断子串是否能重复构成字符串,又嵌套一个for循环,所以是O(n^2)的时间复杂度。我们只需要判断,以第一个字母为开始的子串就可以,所以一个for循环获取子串的终止位置就行了。 而且遍历的时候 都不用遍历结束,只需要遍历到中间位置,因为子串结束位置大于中间位置的话,一定不能重复组成字符串。
移动匹配法:如果是子串,从中间开始一定是一样的(不是指偶数情况,这里指的是将整个字符串看成一个S,然后在S+S(掐头去尾后)中查找S,出现则证明有重复,因为S+S中表示S的尾串也可以作为头串,但是下面的例子给的不太恰当吧,但是很好理解)。
当一个字符串s:abcabc,内部由重复的子串组成,那么这个字符串的结构由前后相同的子串组成。
既然前面有相同的子串,后面有相同的子串,用 s + s,这样组成的字符串中,后面的子串做前串,前面的子串做后串,就一定还能组成一个s,
我们在判断 s + s 拼接的字符串里是否出现一个s的的时候,要刨除 s + s 的首字符和尾字符,这样避免在s+s中搜索出原来的s,我们要搜索的是中间拼接出来的s。
不过这种解法还有一个问题,就是 我们最终还是要判断 一个字符串(s + s)是否出现过 s 的过程,大家可能直接用contains,find 之类的库函数。 却忽略了实现这些函数的时间复杂度(暴力解法是m * n,一般库函数实现为 O(m + n))。
如果我们做过 28.实现strStr (opens new window)题目的话,其实就知道,实现一个 高效的算法来判断 一个字符串中是否出现另一个字符串是很复杂的,这里就涉及到了KMP算法。
class Solution {
public:
bool repeatedSubstringPattern(string s) {
string t = s + s;
t.erase(t.begin()); t.erase(t.end() - 1); // 掐头去尾
if (t.find(s) != std::string::npos) return true; // r
return false;
}
};
KMP法:在一个串中查找是否出现过另一个串,这是KMP的看家本领。那么寻找重复子串怎么也涉及到KMP算法了呢?
KMP算法中next数组为什么遇到字符不匹配的时候可以找到上一个匹配过的位置继续匹配,靠的是有计算好的前缀表。 前缀表里,统计了各个位置为终点字符串的最长相同前后缀的长度。
那么 最长相同前后缀和重复子串的关系又有什么关系呢。
在由重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串,这里拿字符串s:abababab 来举例,ab就是最小重复单位,为啥一定是开头的ab呢。 其实最关键还是要理解 最长相等前后缀:
步骤一:因为 这是相等的前缀和后缀,t[0] 与 k[0]相同, t[1] 与 k[1]相同,所以 s[0] 一定和 s[2]相同,s[1] 一定和 s[3]相同,即:,s[0]s[1]与s[2]s[3]相同 。
步骤二: 因为在同一个字符串位置,所以 t[2] 与 k[0]相同,t[3] 与 k[1]相同。
步骤三: 因为 这是相等的前缀和后缀,t[2] 与 k[2]相同 ,t[3]与k[3] 相同,所以,s[2]一定和s[4]相同,s[3]一定和s[5]相同,即:s[2]s[3] 与 s[4]s[5]相同。
步骤四:循环往复。
所以字符串s,s[0]s[1]与s[2]s[3]相同, s[2]s[3] 与 s[4]s[5]相同,s[4]s[5] 与 s[6]s[7] 相同。
正是因为 最长相等前后缀的规则,当一个字符串由重复子串组成的,最长相等前后缀不包含的子串就是最小重复子串(也就是长度减去最大值剩下的部分)。
数学推理:
强烈建议大家把next数组打印出来,看看next数组里的规律,有助于理解KMP算法
class Solution {
public:
void getNext (int* next, const string& s){
next[0] = 0;
int j = 0;
for(int i = 1;i < s.size(); i++){
while(j > 0 && s[i] != s[j]) {
j = next[j - 1];
}
if(s[i] == s[j]) {
j++;
}
next[i] = j;
}
}
bool repeatedSubstringPattern (string s) {
if (s.size() == 0) {
return false;
}
int next[s.size()];
getNext(next, s);
int len = s.size();
if (next[len - 1] != 0 && len % (len - (next[len - 1] )) == 0) {
return true;
}
return false;
}
};
字符串类类型的题目,往往想法比较简单,但是实现起来并不容易,复杂的字符串题目非常考验对代码的掌控能力。字符串是若干字符组成的有限序列,也可以理解为是一个字符数组,但是很多语言对字符串做了特殊的规定:
强调了打基础的时候,不要太迷恋于库函数。
**双指针法在数组,链表和字符串中很常用。**双指针法是字符串处理的常客。
反转:
在反转上还可以在加一些玩法,其实考察的是对代码的掌控能力。
541. 反转字符串II (opens new window)中,一些同学可能为了处理逻辑:每隔2k个字符的前k的字符,写了一堆逻辑代码或者再搞一个计数器,来统计2k,再统计前k个字符。
其实当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章。要找的也就是每2 * k 区间的起点,这样写程序会高效很多。
在151.翻转字符串里的单词 (opens new window)中要求翻转字符串里的单词,这道题目可以说是综合考察了字符串的多种操作。是考察字符串的好题。
这道题目通过 先整体反转再局部反转,实现了反转字符串里的单词。
后来发现反转字符串还有一个牛逼的用处,就是达到左旋的效果。
KMP算法是字符串查找最重要的算法,但彻底理解KMP并不容易,我们已经写了五篇KMP的文章,不断总结和完善,最终才把KMP讲清楚。KMP的主要思想是**当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。**KMP的精髓所在就是前缀表,在KMP精讲 (opens new window)中提到了,什么是KMP,什么是前缀表,以及为什么要用前缀表。前缀表:起始位置到下标i之前(包括i)的子串中,有多大长度的相同前缀后缀。
其实题目已经全部写过了,因此这里只是再总结一下
19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)
面试题 02.07. 链表相交 - 力扣(LeetCode)
https://leetcode.cn/problems/reverse-linked-list/description/
https://leetcode.cn/problems/reverse-linked-list/description/
https://leetcode.cn/problems/reverse-linked-list/description/
在数组:就移除个元素很难么? (opens new window)中,原地移除数组上的元素,我们说到了数组上的元素,不能真正的删除,只能覆盖。
一些同学可能会写出如下代码(伪代码):
for (int i = 0; i < array.size(); i++) {
if (array[i] == target) {
array.erase(i);
}
}
这个代码看上去好像是O(n)的时间复杂度,其实是O(n^2)的时间复杂度,因为erase操作也是O(n)的操作。
所以此时使用双指针法才展现出效率的优势:通过两个指针在一个for循环下完成两个for循环的工作。
在字符串:这道题目,使用库函数一行代码搞定 (opens new window)中讲解了反转字符串,注意这里强调要原地反转,要不然就失去了题目的意义。
使用双指针法,定义两个指针(也可以说是索引下标),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。,时间复杂度是O(n)。
在替换空格 (opens new window)中介绍使用双指针填充字符串的方法,如果想把这道题目做到极致,就不要只用额外的辅助空间了!
思路就是首先扩充数组到每个空格替换成"%20"之后的大小。然后双指针从后向前替换空格。
有同学问了,为什么要从后向前填充,从前向后填充不行么?
从前向后填充就是O(n^2)的算法了,因为每次添加元素都要将添加元素之后的所有元素向后移动。
其实很多数组(字符串)填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。
那么在字符串:花式反转还不够! (opens new window)中,我们使用双指针法,用O(n)的时间复杂度完成字符串删除类的操作,因为题目要删除冗余空格。
在删除冗余空格的过程中,如果不注意代码效率,很容易写成了O(n^2)的时间复杂度。其实使用双指针法O(n)就可以搞定。
主要还是大家用erase用的比较随意,一定要注意for循环下用erase的情况,一般可以用双指针写效率更高!
翻转链表是现场面试,白纸写代码的好题,考察了候选者对链表以及指针的熟悉程度,而且代码也不长,适合在白纸上写。
在链表:听说过两天反转链表又写不出来了? (opens new window)中,讲如何使用双指针法来翻转链表,只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表。
思路还是很简单的,代码也不长,但是想在白纸上一次性写出bugfree的代码,并不是容易的事情。
在链表中求环,应该是双指针在链表里最经典的应用,在链表:环找到了,那入口呢? (opens new window)中讲解了如何通过双指针判断是否有环,而且还要找到环的入口。
使用快慢指针(双指针法),分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
那么找到环的入口,其实需要点简单的数学推理,我在文章中把找环的入口清清楚楚的推理的一遍,如果对找环入口不够清楚的同学建议自己看一看链表:环找到了,那入口呢? (opens new window)。
在哈希表:解决了两数之和,那么能解决三数之和么? (opens new window)中,讲到使用哈希法可以解决1.两数之和的问题
其实使用双指针也可以解决1.两数之和的问题,只不过1.两数之和求的是两个元素的下标,没法用双指针,如果改成求具体两个元素的数值就可以了。
使用了哈希法解决了两数之和,但是哈希法并不使用于三数之和!
使用哈希法的过程中要把符合条件的三元组放进vector中,然后在去去重,这样是非常费时的,很容易超时,也是三数之和通过率如此之低的根源所在。去重的过程不好处理,有很多小细节,如果在面试中很难想到位。时间复杂度可以做到O(n^2),但还是比较费时的,因为不好做剪枝操作。
所以这道题目使用双指针法才是最为合适的,用双指针做这道题目才能就能真正体会到,通过前后两个指针不算向中间逼近,在一个for循环下完成两个for循环的工作。
只用双指针法时间复杂度为O(n^2),但比哈希法的O(n^2)效率高得多,哈希法在使用两层for循环的时候,能做的剪枝操作很有限。
在双指针法:一样的道理,能解决四数之和 (opens new window)中,讲到了四数之和,其实思路是一样的,**在三数之和的基础上再套一层for循环,依然是使用双指针法。**对于三数之和使用双指针法就是将原本暴力O(n^3)的解法,降为O(n^2)的解法,四数之和的双指针解法就是将原本暴力O(n^4)的解法,降为O(n^3)的解法。同样的道理,五数之和,n数之和都是在这个基础上累加。
队列是先进先出,栈是先进后出。
四个关于栈的问题,以下是以C++为例,使用其他编程语言的同学也对应思考一下,自己使用的编程语言里栈和队列是什么样的。
栈和队列是STL(C++标准库)里面的两个数据结构。C++标准库是有多个版本的,要知道我们使用的STL是哪个版本,才能知道对应的栈和队列的实现原理。那么来介绍一下,三个最为普遍的STL版本:
接下来介绍的栈和队列也是SGI STL里面的数据结构, 知道了使用版本,才知道对应的底层实现。
栈先进后出,栈提供push 和 pop 等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素。栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。所以STL中栈往往不被归类为容器,而被归类为container adapter(容器适配器)。
那么STL 中栈是用什么容器实现的?从下图中可以看出,栈的内部结构,栈的底层实现可以是vector,deque,list 都是可以的, 主要就是数组和链表的底层实现。我们常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的底层结构。deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。
**SGI STL中 队列底层实现缺省情况下一样使用deque实现的。**我们也可以指定vector为栈的底层实现,初始化语句如下:
std::stack<int, std::vector<int> > third; // 使用vector为底层容器的栈
对应的队列的情况是一样的。队列中先进先出的数据结构,同样不允许有遍历行为,不提供迭代器, **SGI STL中队列一样是以deque为缺省情况下的底部结构。**也可以指定list 为起底层实现,初始化queue的语句如下:
std::queue<int, std::list<int>> third; // 定义以list为底层容器的队列
所以STL 队列也不被归类为容器,而被归类为container adapter( 容器适配器)。
我这里讲的都是C++ 语言中的情况, 使用其他语言的同学也要思考栈与队列的底层实现问题, 不要对数据结构的使用浅尝辄止,而要深挖其内部原理,才能夯实基础。
class MyQueue {
public:
stack<int> A;
stack<int> B;
int flag=0;
MyQueue() {
}
void push(int x) {
if(flag==0){
A.push(x);
}else{
while(!B.empty()){
int y=B.top();
B.pop();
A.push(y);
}
flag=0;
A.push(x);
}
}
int pop() {
if(flag==0){
while(!A.empty()){
int y=A.top();
A.pop();
B.push(y);
}
flag=1;
}
int y=B.top();
B.pop();
return y;
}
int peek() {
if(flag==0){
while(!A.empty()){
int y=A.top();
A.pop();
B.push(y);
}
flag=1;
}
int y=B.top();
return y;
}
bool empty() {
return A.empty() && B.empty();
}
};
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue* obj = new MyQueue();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->peek();
* bool param_4 = obj->empty();
*/
模拟题,不涉及到具体算法,考察的就是对栈和队列的掌握程度。
使用栈来模式队列的行为,如果仅仅用一个栈,是一定不行的,所以需要两个栈一个输入栈,一个输出栈,这里要注意输入栈和输出栈的关系。
在push数据的时候,只要数据放进输入栈就好,但在pop的时候,操作就复杂一些,输出栈如果为空(相比我们的方法这样可以减少栈间的移动,相当于将队列分开存放了),就把进栈数据全部导入进来(注意是全部导入),再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。
最后如何判断队列为空呢?如果进栈和出栈都为空的话,说明模拟的队列为空了。
代码非常漂亮:
class MyQueue {
public:
stack<int> stIn;
stack<int> stOut;
/** Initialize your data structure here. */
MyQueue() {
}
/** Push element x to the back of queue. */
void push(int x) {
stIn.push(x);
}
/** Removes the element from in front of queue and returns that element. */
int pop() {
// 只有当stOut为空的时候,再从stIn里导入数据(导入stIn全部数据)
if (stOut.empty()) {
// 从stIn导入数据直到stIn为空
while(!stIn.empty()) {
stOut.push(stIn.top());
stIn.pop();
}
}
int result = stOut.top();
stOut.pop();
return result;
}
/** Get the front element. */
int peek() {
int res = this->pop(); // 直接使用已有的pop函数
stOut.push(res); // 因为pop函数弹出了元素res,所以再添加回去
return res;
}
/** Returns whether the queue is empty. */
bool empty() {
return stIn.empty() && stOut.empty();
}
};
可以看出peek()的实现,直接复用了pop(), 要不然,对stOut判空的逻辑又要重写一遍。再多说一些代码开发上的习惯问题,在工业级别代码开发中,最忌讳的就是 实现一个类似的函数,直接把代码粘过来改一改就完事了。这样的项目代码会越来越乱,一定要懂得复用,功能相近的函数要抽象出来,不要大量的复制粘贴,很容易出问题!(踩过坑的人自然懂)
工作中如果发现某一个功能自己要经常用,同事们可能也会用到,自己就花点时间把这个功能抽象成一个好用的函数或者工具类,不仅自己方便,也方便了同事们。
class MyStack {
public:
queue<int> A;
queue<int> B;
int flag=0;
MyStack() {
}
void push(int x) {
if(flag==0){
B.push(x);
while(!A.empty()){
int y=A.front();
A.pop();
B.push(y);
}
flag=1;
}else{
A.push(x);
while(!B.empty()){
int y=B.front();
B.pop();
A.push(y);
}
flag=0;
}
}
int pop() {
int y;
if(flag==0 and !A.empty()){
y=A.front();
A.pop();
}else if(flag==1 and !B.empty()){
y=B.front();
B.pop();
}
return y;
}
int top() {
int y;
if(flag==0 and !A.empty()){
y=A.front();
}else if(flag==1 and !B.empty()){
y=B.front();
}
return y;
}
bool empty() {
return B.empty() and A.empty();
}
};
/**
* Your MyStack object will be instantiated and called as such:
* MyStack* obj = new MyStack();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->top();
* bool param_4 = obj->empty();
*/
class MyStack {
public:
queue<int> que;
/** Initialize your data structure here. */
MyStack() {
}
/** Push element x onto stack. */
void push(int x) {
que.push(x);
}
/** Removes the element on top of the stack and returns that element. */
int pop() {
int size = que.size();
size--;
while (size--) { // 将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部
que.push(que.front());
que.pop();
}
int result = que.front(); // 此时弹出的元素顺序就是栈的顺序了
que.pop();
return result;
}
/** Get the top element. */
int top() {
return que.back();
}
/** Returns whether the stack is empty. */
bool empty() {
return que.empty();
}
};
class Solution {
public:
bool isValid(string s) {
int n=s.size();
stack<int> ss;
for(int i=0;i<n;i++){
if(s[i]=='(' or s[i]=='[' or s[i]=='{'){
ss.push(s[i]);
}else{
if(s[i]==')'){
if(!ss.empty() and ss.top()=='('){
ss.pop();
}else{
return false;
}
}else if(s[i]==']'){
if(!ss.empty() and ss.top()=='['){
ss.pop();
}else{
return false;
}
}else if(s[i]=='}'){
if(!ss.empty() and ss.top()=='{'){
ss.pop();
}else{
return false;
}
}
}
}
if(!ss.empty()){
return false;
}
return true;
}
};
扩展:**括号匹配是使用栈解决的经典问题。**题意其实就像我们在写代码的过程中,要求括号的顺序是一样的,有左括号,相应的位置必须要有右括号。如果还记得编译原理的话,编译器在 词法分析的过程中处理括号、花括号等这个符号的逻辑,也是使用了栈这种数据结构。再举个例子,linux系统中,cd这个进入目录的命令我们应该再熟悉不过了。
cd a/b/c/../../
这个命令最后进入a目录,系统是如何知道进入了a目录呢 ,这就是栈的应用(其实可以出一道相应的面试题了)。所以栈在计算机领域中应用是非常广泛的。所以数据结构与算法的应用往往隐藏在我们看不到的地方!
由于栈结构的特殊性,非常适合做对称匹配类的题目。首先要弄清楚,字符串里的括号不匹配有几种情况。一些同学,在面试中看到这种题目上来就开始写代码,然后就越写越乱。建议在写代码之前要分析好有哪几种不匹配的情况,如果不在动手之前分析好,写出的代码也会有很多问题。
先来分析一下 这里有三种不匹配的情况,我们的代码只要覆盖了这三种不匹配的情况,就不会出问题,可以看出 动手之前分析好题目的重要性。
字符串遍历完之后,栈是空的,就说明全都匹配了。分析完之后,代码其实就比较好写了,但还有一些技巧,在匹配左括号的时候,右括号先入栈,就只需要比较当前元素和栈顶相不相等就可以了,比左括号先入栈代码实现要简单的多了!
class Solution {
public:
bool isValid(string s) {
if (s.size() % 2 != 0) return false; // 如果s的长度为奇数,一定不符合要求
stack<char> st;
for (int i = 0; i < s.size(); i++) {
if (s[i] == '(') st.push(')');
else if (s[i] == '{') st.push('}');
else if (s[i] == '[') st.push(']');
// 第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号 return false
// 第二种情况:遍历字符串匹配的过程中,发现栈里没有我们要匹配的字符。所以return false
else if (st.empty() || st.top() != s[i]) return false;
else st.pop(); // st.top() 与 s[i]相等,栈弹出元素
}
// 第一种情况:此时我们已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false,否则就return true
return st.empty();
}
};
1047. 删除字符串中的所有相邻重复项 - 力扣(LeetCode)
class Solution {
public:
string removeDuplicates(string s) {
stack<char> st;
int length=0;
if(s.size()==0){
return s;
}
st.push(s[0]);
length++;
for(int i=1;i<s.size();i++){
if(!st.empty() and s[i]==st.top()){
st.pop();
length--;
}else{
st.push(s[i]);
length++;
}
}
s.resize(length);
for(int i=s.size()-1;i>=0;i--){
s[i]=st.top();
st.pop();
}
return s;
}
};
class Solution {
public:
string removeDuplicates(string S) {
string result;
for(char s : S) {
if(result.empty() || result.back() != s) {
result.push_back(s);
}
else {
result.pop_back();
}
}
return result;
}
};
Segmentation fault
(当然不是所有的Segmentation fault
都是栈溢出导致的) ,如果你使用了递归,就要想一想是不是无限递归了,那么系统调用栈就会溢出。class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<long long> st;
for(int i=0;i<tokens.size();i++){
if(tokens[i]=="+"){
long long a=st.top();
st.pop();
long long b=st.top();
st.pop();
a=a+b;
// printf("%ld+%ld=%ld",a,b,a);
st.push(a);
}else if(tokens[i]=="-"){
long long a=st.top();
st.pop();
long long b=st.top();
st.pop();
a=b-a;
// printf("%ld-%ld=%ld",a,b,a);
st.push(a);
}else if(tokens[i]=="*"){
long long a=st.top();
st.pop();
long long b=st.top();
st.pop();
a=a*b;
// printf("%ld*%ld=%ld",a,b,a);
st.push(a);
}else if(tokens[i]=="/"){
long long a=st.top();
st.pop();
long long b=st.top();
st.pop();
a=b/a;//注意a b之间的顺序
// printf("%ld/%ld=%ld",a,b,a);
st.push(a);
}else{
st.push(stoll(tokens[i]));
}
}
int a=st.top();
st.pop();
return a;
}
};
class Solution {
public:
int evalRPN(vector<string>& tokens) {
// 力扣修改了后台测试数据,需要用longlong
stack<long long> st;
for (int i = 0; i < tokens.size(); i++) {
if (tokens[i] == "+" || tokens[i] == "-" || tokens[i] == "*" || tokens[i] == "/") {
long long num1 = st.top();
st.pop();
long long num2 = st.top();
st.pop();
if (tokens[i] == "+") st.push(num2 + num1);
if (tokens[i] == "-") st.push(num2 - num1);
if (tokens[i] == "*") st.push(num2 * num1);
if (tokens[i] == "/") st.push(num2 / num1);
} else {
st.push(stoll(tokens[i]));
}
}
int result = st.top();
st.pop(); // 把栈里最后一个元素弹出(其实不弹出也没事)
return result;
}
};
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int max=nums[0];
queue<int> que;
int n=nums.size()-k+1;
vector<int> max_nums(n);
int num_max=0;
for(int i=0;i<k;i++){
que.push(nums[i]);
if(nums[i]>max){
max=nums[i];
}
}
max_nums[num_max++]=max;
for(int i=k;i<nums.size();i++){
int nn=que.front();
printf("%d: %d ",i, nn);
que.pop();//front会直接pop出来
que.push(nums[i]);
if(nums[i]>max){
max=nums[i];
}else if(max==nn){
max=que.front();
que.pop();
printf(" max:%d ", max);
que.push(max);
for(int j=0;j<k-1;j++){
int nx=que.front();
if(max<nx){
max=nx;
}
que.pop();
que.push(nx);
}
}
max_nums[num_max++]=max;
}
return max_nums;
}
};
class Solution {
private:
class MyQueue { //单调队列(从大到小)
public:
deque<int> que; // 使用deque来实现单调队列
void pop() {
if (!que.empty()) {
que.pop_front();
}
}
void push(int value) {
que.push_back(value);
}
int front() {
return que.front();
}
int back() {
return que.back();
}
void pop_back() {
que.pop_back();
}
bool empty(){
return que.empty();
}
};
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int max=nums[0];
MyQueue que;
int n=nums.size()-k+1;
vector<int> max_nums(n);
int num_max=0;
for(int i=0;i<k;i++){
while(!que.empty() and nums[i]>que.back()){
que.pop_back();
}
que.push(nums[i]);
if(nums[i]>max){
max=nums[i];
}
}
max_nums[num_max++]=max;
for(int i=k;i<nums.size();i++){
// int nn=que.front();
// printf("%d: %d ",i, nn);
// que.pop();//front会直接pop出来
if(nums[i-k]==que.front()){
que.pop();
}
while(!que.empty() and nums[i]>que.back()){
que.pop_back();
}
que.push(nums[i]);
if(nums[i]>max){
max=nums[i];
}else if(max==nums[i-k]){
max=que.front();
// que.pop();
// printf(" max:%d ", max);
// que.push(max);
// for(int j=0;j<k-1;j++){
// int nx=que.front();
// if(max<nx){
// max=nx;
// }
// que.pop();
// que.push(nx);
// }
}
max_nums[num_max++]=max;
}
return max_nums;
}
};
这是使用单调队列的经典题目。
暴力方法,遍历一遍的过程中每次从窗口中再找到最大的数值,这样很明显是O(n × k)的算法。有的同学可能会想用一个大顶堆(优先级队列)来存放这个窗口里的k个数字,这样就可以知道最大的最大值是多少了, 但是问题是这个窗口是移动的,而大顶堆每次只能弹出最大值,我们无法移除其他数值,这样就造成大顶堆维护的不是滑动窗口里面的数值了。所以不能用大顶堆。
此时我们需要一个队列,这个队列呢,放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。
其实在C++中,可以使用 multiset 来模拟这个过程,文末提供这个解法仅针对C++,以下讲解我们还是靠自己来实现这个单调队列。
队列里的元素一定是要排序的,而且要最大值放在出队口,要不然怎么知道最大值呢。但如果把窗口里的元素都放进队列里,窗口移动的时候,队列需要弹出元素。那么问题来了,已经排序之后的队列 怎么能把窗口要移除的元素(这个元素可不一定是最大值)弹出呢。
其实队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。
那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。C++中没有直接支持单调队列,需要我们自己来实现一个单调队列
不要以为实现的单调队列就是 对窗口里面的数进行排序,如果排序的话,那和优先级队列又有什么区别了呢。
对于窗口里的元素{2, 3, 5, 1 ,4},单调队列里只维护{5, 4} 就够了,保持单调队列里单调递减,此时队列出口元素就是窗口里最大元素。此时大家应该怀疑单调队列里维护着{5, 4} 怎么配合窗口进行滑动呢?
设计单调队列的时候,pop,和push操作要保持如下规则:
保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。
那么我们用什么数据结构来实现这个单调队列呢?
使用deque最为合适,在文章栈与队列:来看看栈和队列不为人知的一面 (opens new window)中,我们就提到了常用的queue在没有指定容器的情况下,deque就是默认底层容器。
相当于队列并不是为了记录当前的窗口,当前的窗口直接在数组中看就行,没必要记录,需要记录到队列中的是当前最大的数也就是在一个数(未被弹出)前,前面所有小于这个数的都被弹出。
再来看一下时间复杂度,使用单调队列的时间复杂度是 O(n)。
有的同学可能想了,在队列中 push元素的过程中,还有pop操作呢,感觉不是纯粹的O(n)。
其实,大家可以自己观察一下单调队列的实现,nums 中的每个元素最多也就被 push_back 和 pop_back 各一次,没有任何多余操作,所以整体的复杂度还是 O(n)。
空间复杂度因为我们定义一个辅助队列,所以是O(k)
题解中单调队列里的pop和push接口,仅适用于本题哈。单调队列不是一成不变的,而是不同场景不同写法,总之要保证队列里单调递减或递增的原则,所以叫做单调队列。 不要以为本题中的单调队列实现就是固定的写法哈。
大家貌似对deque也有一些疑惑,C++中deque是stack和queue默认的底层实现容器(这个我们之前已经讲过啦),deque是可以两边扩展的,而且deque里元素并不是严格的连续分布的。
注意deque需要自己进行实现:
class Solution {
private:
class MyQueue { //单调队列(从大到小)
public:
deque<int> que; // 使用deque来实现单调队列
// 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
// 同时pop之前判断队列当前是否为空。
void pop(int value) {
if (!que.empty() && value == que.front()) {
que.pop_front();
}
}
// 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
// 这样就保持了队列里的数值是单调从大到小的了。
void push(int value) {
while (!que.empty() && value > que.back()) {
que.pop_back();
}
que.push_back(value);
}
// 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
int front() {
return que.front();
}
};
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MyQueue que;
vector<int> result;
for (int i = 0; i < k; i++) { // 先将前k的元素放进队列
que.push(nums[i]);
}
result.push_back(que.front()); // result 记录前k的元素的最大值
for (int i = k; i < nums.size(); i++) {
que.pop(nums[i - k]); // 滑动窗口移除最前面元素
que.push(nums[i]); // 滑动窗口前加入最后面的元素
result.push_back(que.front()); // 记录对应的最大值
}
return result;
}
};
class Solution {
public:
// 小顶堆
class mycomparison {
public:
bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
return lhs.second > rhs.second;
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
// 要统计元素出现频率
unordered_map<int, int> map; // map<nums[i],对应出现的次数>
for (int i = 0; i < nums.size(); i++) {
map[nums[i]]++;
}
// 对频率排序
// 定义一个小顶堆,大小为k
priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;
// 用固定大小为k的小顶堆,扫面所有频率的数值
for (unordered_map<int, int>::iterator it = map.begin(); it != map.end(); it++) {
pri_que.push(*it);
if (pri_que.size() > k) { // 如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
pri_que.pop();
}
}
// 找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
vector<int> result(k);
for (int i = k - 1; i >= 0; i--) {
result[i] = pri_que.top().first;
pri_que.pop();
}
return result;
}
};
对这个比较运算在建堆时是如何应用的,为什么左大于右就会建立小顶堆,反而建立大顶堆比较困惑。
确实 例如我们在写快排的cmp函数的时候,return left>right
就是从大到小,return left<right
就是从小到大。
优先级队列的定义正好反过来了,可能和优先级队列的源码实现有关(我没有仔细研究),我估计是底层实现上优先队列队首指向后面,队尾指向最前面的缘故!
在栈与队列系列中,我们强调栈与队列的基础,也是很多同学容易忽视的点。使用抽象程度越高的语言,越容易忽视其底层实现,而C++相对来说是比较接近底层的语言。
灵魂四问:
栈与队列是我们熟悉的不能再熟悉的数据结构,但它们的底层实现,很多同学都比较模糊,这其实就是基础所在。可以出一道面试题:栈里面的元素在内存中是连续分布的么?这个问题有两个陷阱:
一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时在去弹出元素就是栈的顺序了。
**栈在系统中的应用:**如果还记得编译原理的话,编译器在词法分析的过程中处理括号、花括号等这个符号的逻辑,就是使用了栈这种数据结构。递归的实现是栈:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。数据结构与算法的应用往往隐藏在我们看不到的地方!
**括号匹配是使用栈解决的经典问题。**建议要写代码之前要分析好有哪几种不匹配的情况,如果不动手之前分析好,写出的代码也会有很多问题。这里还有一些技巧,在匹配左括号的时候,右括号先入栈,就只需要比较当前元素和栈顶相不相等就可以了,比左括号先入栈代码实现要简单的多了!
除字符串中的所有相邻重复项:思路就是可以把字符串顺序放到一个栈中,然后如果相同的话 栈就弹出,这样最后栈里剩下的元素都是相邻不相同的元素了。
求逆波兰表达式。本题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,那么这岂不就是一个相邻字符串消除的过程,和栈与队列:匹配问题都是栈的强项 (opens new window)中的对对碰游戏是不是就非常像了。
滑动窗口最大值问题:讲解了一种数据结构:单调队列。这道题目还是比较绕的,如果第一次遇到这种题目,需要反复琢磨。主要思想是队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。C++中没有直接支持单调队列,需要我们自己来一个单调队列。而且**不要以为实现的单调队列就是 对窗口里面的数进行排序,如果排序的话,那和优先级队列又有什么区别了呢。**设计单调队列的时候,pop,和push操作要保持如下规则:
保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。一些同学还会对单调队列都有一些困惑,首先要明确的是,题解中单调队列里的pop和push接口,仅适用于本题。单调队列不是一成不变的,而是不同场景不同写法,总之要保证队列里单调递减或递增的原则,所以叫做单调队列。**不要以为本地中的单调队列实现就是固定的写法。**我们用deque作为单调队列的底层数据结构,C++中deque是stack和queue默认的底层实现容器(这个我们之前已经讲过),deque是可以两边扩展的,而且deque里元素并不是严格的连续分布的。
求前 K 个高频元素:通过求前 K 个高频元素,引出另一种队列就是优先级队列。就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。而且优先级队列内部元素是自动依照元素的权值排列。那么它是如何有序排列的呢?
缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)。本题就要使用优先级队列来对部分频率进行排序。 注意这里是对部分数据进行排序而不需要对所有数据排序!所以排序的过程的时间复杂度是 O ( log ? k ) O(\log k) O(logk),整个算法的时间复杂度是 O ( n log ? k ) O(n\log k) O(nlogk)。
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
stack<int> ss;
vector<int> answer(temperatures.size());
for(int i=temperatures.size()-1;i>=0;i--){
if(ss.empty()){
answer[i]=0;
ss.push(temperatures[i]);
}else{
// int top=ss.top();
if(ss.top()>temperatures[i]){
answer[i]=1;
ss.push(temperatures[i]);
}else{
int tag=answer[i+1]+1;
ss.pop();
while(!ss.empty() and temperatures[i]>=ss.top()){
ss.pop();
tag+=answer[i+tag];
}
if(ss.empty()){
answer[i]=0;
ss.push(temperatures[i]);
}else{
ss.push(temperatures[i]);
answer[i]=tag;
}
}
}
}
return answer;
}
};
首先想到的当然是暴力解法,两层for循环,把至少需要等待的天数就搜出来了。时间复杂度是O(n^2)
我怎么能想到用单调栈呢? 什么时候用单调栈呢?通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。时间复杂度为O(n)。
那么单调栈的原理是什么呢?为什么时间复杂度是O(n)就可以找到每一个元素的右边第一个比它大的元素位置呢?单调栈的本质是空间换时间,因为在遍历的过程中需要用一个栈来记录右边第一个比当前元素高的元素,优点是整个数组只需要遍历一次。更直白来说,就是用一个栈来记录我们遍历过的元素,因为我们遍历数组的时候,我们不知道之前都遍历了哪些元素,以至于遍历一个元素找不到是不是之前遍历过一个更小的,所以我们需要用一个容器(这里用单调栈)来记录我们遍历过的元素。
在使用单调栈的时候首先要明确如下几点:
使用单调栈主要有三个判断条件。
把这三种情况分析清楚了,也就理解透彻了。
定义result数组的时候,就应该直接初始化为0,如果result没有更新,说明这个元素右面没有更大的了,也就是为0。通过以上过程,大家可以自己再模拟一遍,就会发现:只有单调栈递增(从栈口到栈底顺序),就是求右边第一个比自己大的,单调栈递减的话,就是求右边第一个比自己小的。(这里的方法是正向放入数据的,感觉更难理解欸:就是正向放入(比前面的数小则直接放入),当需要弹出时更新result值(更新为当前准备压入的值,也就是当前值更大的意思),最后未被弹出的直接赋值为0(因为后面没有值能将其弹出)。)
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& T) {
stack<int> st; // 递增栈
vector<int> result(T.size(), 0);
for (int i = 0; i < T.size(); i++) {
while (!st.empty() && T[i] > T[st.top()]) { // 注意栈不能为空
result[st.top()] = i - st.top();
st.pop();
}
st.push(i);
}
return result;
}
};
class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
vector<int> result(nums1.size(), -1);
stack<int> ss;
unordered_map<int, int> st;
for(int i=0;i<nums1.size();i++){
st[nums1[i]]=i;
}
for(int i=0;i<nums2.size();i++){
while(!ss.empty() and nums2[i]>ss.top()){
if(st.find(ss.top()) != st.end()){
result[st[ss.top()]]=nums2[i];
}
ss.pop();
}
ss.push(nums2[i]);
}
return result;
}
};
本题则是说nums1 是 nums2的子集,找nums1中的元素在nums2中下一个比当前元素大的元素。从题目示例中我们可以看出最后是要求nums1的每个元素在nums2中下一个比当前元素大的元素,那么就要定义一个和nums1一样大小的数组result来存放结果。题目说如果不存在对应位置就输出 -1 ,所以result数组如果某位置没有被赋值,那么就应该是是-1,所以就初始化为-1。
在遍历nums2的过程中,我们要判断nums2[i]是否在nums1中出现过,因为最后是要根据nums1元素的下标来更新result数组。注意题目中说是两个没有重复元素 的数组 nums1 和 nums2。没有重复元素,我们就可以用map来做映射了。根据数值快速找到下标,还可以判断nums2[i]是否在nums1中出现过。
C++中,当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的。我在关于哈希表,你该了解这些! (opens new window)中也做了详细的解释。
使用单调栈,首先要想单调栈是从大到小还是从小到大。栈头到栈底的顺序,要从小到大,也就是保持栈里的元素为递增顺序。只要保持递增,才能找到右边第一个比自己大的元素。其实递减栈就是求右边第一个比自己小的元素了。
接下来就要分析如下三种情况,一定要分析清楚。
此时满足递增栈(栈头到栈底的顺序),所以直接入栈。
如果相等的话,依然直接入栈,因为我们要求的是右边第一个比自己大的元素,而不是大于等于!
此时如果入栈就不满足递增栈了,这也是找到右边第一个比自己大的元素的时候。
判断栈顶元素是否在nums1里出现过,(注意栈里的元素是nums2的元素),如果出现过,开始记录结果。记录结果这块逻辑有一点小绕,要清楚,此时栈顶元素在nums2数组中右面第一个大的元素是nums2[i](即当前遍历元素)。(这道题和nums 2的下标又没什么关系为什么要记录下标啊?我的代码更好理解一点吧)
class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
stack<int> st;
vector<int> result(nums1.size(), -1);
if (nums1.size() == 0) return result;
unordered_map<int, int> umap; // key:下标元素,value:下标
for (int i = 0; i < nums1.size(); i++) {
umap[nums1[i]] = i;
}
st.push(0);
for (int i = 1; i < nums2.size(); i++) {
while (!st.empty() && nums2[i] > nums2[st.top()]) {
if (umap.count(nums2[st.top()]) > 0) { // 看map里是否存在这个元素
int index = umap[nums2[st.top()]]; // 根据map找到nums2[st.top()] 在 nums1中的下标
result[index] = nums2[i];
}
st.pop();
}
st.push(i);
}
return result;
}
};
503. 下一个更大元素 II - 力扣(LeetCode)
class Solution {
public:
vector<int> nextGreaterElements(vector<int>& nums) {
vector<int> result(nums.size(), -1);
stack<int> ss;
for(int i=0;i<nums.size();i++){
while(!ss.empty() and nums[i]>nums[ss.top()]){
result[ss.top()]=nums[i];
ss.pop();
}
ss.push(i);
}
// while(!ss.empty()){
// ss.pop();
// }
for(int i=0;i<nums.size();i++){
while(!ss.empty() and nums[i]>nums[ss.top()]){
if(result[ss.top()]==-1){
result[ss.top()]=nums[i];
}
ss.pop();
}
ss.push(i);
}
return result;
}
};
class Solution {
public:
vector<int> nextGreaterElements(vector<int>& nums) {
vector<int> result(nums.size(), -1);
if (nums.size() == 0) return result;
stack<int> st;
for (int i = 0; i < nums.size() * 2; i++) {
// 模拟遍历两边nums,注意一下都是用i % nums.size()来操作
while (!st.empty() && nums[i % nums.size()] > nums[st.top()]) {
result[st.top()] = nums[i % nums.size()];
st.pop();
}
st.push(i % nums.size());
}
return result;
}
};
class Solution {
public:
int trap(vector<int>& height) {
vector<int> result(height.size(), 0);
stack<int> ss;
int sum=0;
for(int i=0;i<height.size();i++){
while(!ss.empty() and height[i]>=height[ss.top()]){
result[ss.top()]=i;
ss.pop();
}
ss.push(i);
}
while(!ss.empty()){
ss.pop();
}
for(int i=height.size()-1;i>=0;i--){
int flag=0;
if(result[i]==0){
flag=1;
}
while(!ss.empty() and height[i]>height[ss.top()]){
if(flag){
result[i]=ss.top();//保证这是能弹出的后面的值中未被弹出的最大的
}
ss.pop();
}
ss.push(i);
}
for(int i=0;i<height.size();i++){
printf("%d ", result[i]);
}
for(int i=0;i<height.size();i++){
int high;
if(result[i]>i){
if(height[i]>height[result[i]]){
high=height[result[i]];
}else{
high=height[i];
}
for(int j=i+1;j<result[i];j++){
if(high>height[j]){
sum+=high-height[j];
}
}
i=result[i]-1; //避免重复计算
}
}
return sum;
}
};
暴力:本题暴力解法也是也是使用双指针。首先要明确计算面积是要按照行来计算,还是按照列来计算。按照列来计算,比较容易理解:**如果按照列来计算的话,宽度一定是1了,我们再把每一列的雨水的高度求出来就可以了。**每一列雨水的高度,取决于,该列 左侧最高的柱子和右侧最高的柱子中最矮的那个柱子的高度。
class Solution {
public:
int trap(vector<int>& height) {
int sum = 0;
for (int i = 0; i < height.size(); i++) {
// 第一个柱子和最后一个柱子不接雨水
if (i == 0 || i == height.size() - 1) continue;
int rHeight = height[i]; // 记录右边柱子的最高高度
int lHeight = height[i]; // 记录左边柱子的最高高度
for (int r = i + 1; r < height.size(); r++) {
if (height[r] > rHeight) rHeight = height[r];
}
for (int l = i - 1; l >= 0; l--) {
if (height[l] > lHeight) lHeight = height[l];
}
int h = min(lHeight, rHeight) - height[i];
if (h > 0) sum += h;
}
return sum;
}
};
双指针优化:为了得到两边的最高高度,使用了双指针来遍历,每到一个柱子都向两边遍历一遍,这其实是有重复计算的。我们把每一个位置的左边最高高度记录在一个数组上(maxLeft),右边最高高度记录在一个数组上(maxRight),这样就避免了重复计算。当前位置,左边的最高高度是前一个位置的左边最高高度和本高度的最大值。
class Solution {
public:
int trap(vector<int>& height) {
if (height.size() <= 2) return 0;
vector<int> maxLeft(height.size(), 0);
vector<int> maxRight(height.size(), 0);
int size = maxRight.size();
// 记录每个柱子左边柱子最大高度
maxLeft[0] = height[0];
for (int i = 1; i < size; i++) {
maxLeft[i] = max(height[i], maxLeft[i - 1]);
}
// 记录每个柱子右边柱子最大高度
maxRight[size - 1] = height[size - 1];
for (int i = size - 2; i >= 0; i--) {
maxRight[i] = max(height[i], maxRight[i + 1]);
}
// 求和
int sum = 0;
for (int i = 0; i < size; i++) {
int count = min(maxLeft[i], maxRight[i]) - height[i];
if (count > 0) sum += count;
}
return sum;
}
};
单调栈:单调栈就是保持栈内元素有序。和栈与队列:单调队列 (opens new window)一样,需要我们自己维持顺序,没有现成的容器可以用。通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。而接雨水这道题目,我们正需要寻找一个元素,右边最大元素以及左边最大元素,来计算雨水面积。
那么本题使用单调栈有如下几个问题:
首先单调栈是按照行方向来计算雨水
使用单调栈内元素的顺序,从大到小还是从小到大呢?从栈头(元素从栈头弹出)到栈底的顺序应该是从小到大的顺序。因为一旦发现添加的柱子高度大于栈头元素了,此时就出现凹槽了,栈头元素就是凹槽底部的柱子,栈头第二个元素就是凹槽左边的柱子,而添加的元素就是凹槽右边的柱子。关于单调栈的顺序给大家一个总结: 739. 每日温度 (opens new window)中求一个元素右边第一个更大元素,单调栈就是递增的,84.柱状图中最大的矩形 (opens new window)求一个元素右边第一个更小元素,单调栈就是递减的。
遇到相同高度的柱子怎么办。遇到相同的元素,更新栈内下标,就是将栈里元素(旧下标)弹出,将新元素(新下标)加入栈中。因为我们要求宽度的时候 如果遇到相同高度的柱子,需要使用最右边的柱子来计算宽度。
栈里要保存什么数值:使用单调栈,也是通过 长 * 宽 来计算雨水面积的。长就是通过柱子的高度来计算,宽是通过柱子之间的下标来计算,那么栈里有没有必要存一个pair<int, int>类型的元素,保存柱子的高度和下标呢。其实不用,栈里就存放下标就行,想要知道对应的高度,通过height[stack.top()] 就知道弹出的下标对应的高度了。
以下逻辑主要就是三种情况(和我的方法思路不同,这里是在对每一个凹点找两边,我的是在找每一个凸点的另一边)
int h = min(height[st.top()], height[i]) - height[mid];
雨水的宽度是 凹槽右边的下标 - 凹槽左边的下标 - 1(因为只求中间宽度),代码为:int w = i - st.top() - 1 ;
当前凹槽雨水的体积就是:h * w
。 class Solution {
public:
int trap(vector<int>& height) {
stack<int> st;
st.push(0);
int sum = 0;
for (int i = 1; i < height.size(); i++) {
while (!st.empty() && height[i] > height[st.top()]) {
int mid = st.top();
st.pop();
if (!st.empty()) {
int h = min(height[st.top()], height[i]) - height[mid];
int w = i - st.top() - 1;
sum += h * w;
}
}
st.push(i);
}
return sum;
}
};
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
stack<int> st;
st.push(0);
int max=heights[0];
for(int i=1;i<heights.size();i++){
while(!st.empty() and heights[i]<heights[st.top()]){
int high=st.top();
st.pop();
int length;
if(!st.empty()){
length=high-st.top();
}else{
length=high+1;
}
int now=heights[high] * length;
if(now>max){
max=now;
}
if(!st.empty() and heights[i]<heights[st.top()]){
int x=st.top();
st.pop();
st.push(high);
heights[high]=heights[x];
}
}
if(!st.empty() and heights[i]==heights[st.top()]){
st.pop();
}
st.push(i);
}
while(!st.empty()){
int high=st.top();
st.pop();
int length;
if(!st.empty()){
length=high-st.top();
}else{
length=high+1;
}
int now=heights[high] * length;
if(now>max){
max=now;
}
if(!st.empty()){
int x=st.top();
st.pop();
st.push(high);
heights[high]=heights[x];
}
}
return max;
}
};
本题和42. 接雨水 (opens new window),是遥相呼应的两道题目,建议都要仔细做一做,原理上有很多相同的地方,但细节上又有差异,更可以加深对单调栈的理解!
暴力解法不能通过会超时。(很神奇的是暴力解法好像让我更明白了,就是只需要找两边小于该值的就行,因为最小值也会去两边找,不用像我之前想的分那么多的情况(上面的代码已经是我简化后的,刚开始想复杂了),因此是不会错过什么可能能取到的值的。)
双指针的写法:整体思路和42. 接雨水 (opens new window)是一致的,但要比42. 接雨水 (opens new window)难一些。难就难在本题要记录记录每个柱子 左边第一个小于该柱子的下标,而不是左边第一个小于该柱子的高度。所以需要循环查找,也就是下面在寻找的过程中使用了while。
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
vector<int> minLeftIndex(heights.size());
vector<int> minRightIndex(heights.size());
int size = heights.size();
// 记录每个柱子 左边第一个小于该柱子的下标
minLeftIndex[0] = -1; // 注意这里初始化,防止下面while死循环
for (int i = 1; i < size; i++) {
int t = i - 1;
// 这里不是用if,而是不断向左寻找的过程
while (t >= 0 && heights[t] >= heights[i]) t = minLeftIndex[t];
minLeftIndex[i] = t;
}
// 记录每个柱子 右边第一个小于该柱子的下标
minRightIndex[size - 1] = size; // 注意这里初始化,防止下面while死循环
for (int i = size - 2; i >= 0; i--) {
int t = i + 1;
// 这里不是用if,而是不断向右寻找的过程
while (t < size && heights[t] >= heights[i]) t = minRightIndex[t];
minRightIndex[i] = t;
}
// 求和
int result = 0;
for (int i = 0; i < size; i++) {
int sum = heights[i] * (minRightIndex[i] - minLeftIndex[i] - 1);
result = max(sum, result);
}
return result;
}
};
单调栈:解法和接雨水的题目是遥相呼应的。本题是找每个柱子左右两边第一个小于该柱子的柱子。这里就涉及到了单调栈很重要的性质,就是单调栈里的顺序,是从小到大还是从大到小。因为本题是要找每个柱子左右两边第一个小于该柱子的柱子,所以从栈头(元素从栈头弹出)到栈底的顺序应该是从大到小的顺序!只有栈里从大到小的顺序,才能保证栈顶元素找到左右两边第一个小于栈顶元素的柱子。此时大家应该可以发现其实就是栈顶和栈顶的下一个元素以及要入栈的三个元素组成了我们要求最大面积的高度和宽度。主要就是分析清楚如下三种情况:
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
stack<int> st;
heights.insert(heights.begin(), 0); // 数组头部加入元素0
heights.push_back(0); // 数组尾部加入元素0
st.push(0);
int result = 0;
for (int i = 1; i < heights.size(); i++) {
while (heights[i] < heights[st.top()]) {
int mid = st.top();
st.pop();
int w = i - st.top() - 1;
int h = heights[mid];
result = max(result, w * h);
}
st.push(i);
}
return result;
}
};
sum = 0;
for (int i = 1; i < height.size(); i++) {
while (!st.empty() && height[i] > height[st.top()]) {
int mid = st.top();
st.pop();
if (!st.empty()) {
int h = min(height[st.top()], height[i]) - height[mid];
int w = i - st.top() - 1;
sum += h * w;
}
}
st.push(i);
}
return sum;
}
};
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
stack<int> st;
st.push(0);
int max=heights[0];
for(int i=1;i<heights.size();i++){
while(!st.empty() and heights[i]<heights[st.top()]){
int high=st.top();
st.pop();
int length;
if(!st.empty()){
length=high-st.top();
}else{
length=high+1;
}
int now=heights[high] * length;
if(now>max){
max=now;
}
if(!st.empty() and heights[i]<heights[st.top()]){
int x=st.top();
st.pop();
st.push(high);
heights[high]=heights[x];
}
}
if(!st.empty() and heights[i]==heights[st.top()]){
st.pop();
}
st.push(i);
}
while(!st.empty()){
int high=st.top();
st.pop();
int length;
if(!st.empty()){
length=high-st.top();
}else{
length=high+1;
}
int now=heights[high] * length;
if(now>max){
max=now;
}
if(!st.empty()){
int x=st.top();
st.pop();
st.push(high);
heights[high]=heights[x];
}
}
return max;
}
};
本题和42. 接雨水 (opens new window),是遥相呼应的两道题目,建议都要仔细做一做,原理上有很多相同的地方,但细节上又有差异,更可以加深对单调栈的理解!
暴力解法不能通过会超时。(很神奇的是暴力解法好像让我更明白了,就是只需要找两边小于该值的就行,因为最小值也会去两边找,不用像我之前想的分那么多的情况(上面的代码已经是我简化后的,刚开始想复杂了),因此是不会错过什么可能能取到的值的。)
双指针的写法:整体思路和42. 接雨水 (opens new window)是一致的,但要比42. 接雨水 (opens new window)难一些。难就难在本题要记录记录每个柱子 左边第一个小于该柱子的下标,而不是左边第一个小于该柱子的高度。所以需要循环查找,也就是下面在寻找的过程中使用了while。
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
vector<int> minLeftIndex(heights.size());
vector<int> minRightIndex(heights.size());
int size = heights.size();
// 记录每个柱子 左边第一个小于该柱子的下标
minLeftIndex[0] = -1; // 注意这里初始化,防止下面while死循环
for (int i = 1; i < size; i++) {
int t = i - 1;
// 这里不是用if,而是不断向左寻找的过程
while (t >= 0 && heights[t] >= heights[i]) t = minLeftIndex[t];
minLeftIndex[i] = t;
}
// 记录每个柱子 右边第一个小于该柱子的下标
minRightIndex[size - 1] = size; // 注意这里初始化,防止下面while死循环
for (int i = size - 2; i >= 0; i--) {
int t = i + 1;
// 这里不是用if,而是不断向右寻找的过程
while (t < size && heights[t] >= heights[i]) t = minRightIndex[t];
minRightIndex[i] = t;
}
// 求和
int result = 0;
for (int i = 0; i < size; i++) {
int sum = heights[i] * (minRightIndex[i] - minLeftIndex[i] - 1);
result = max(sum, result);
}
return result;
}
};
单调栈:解法和接雨水的题目是遥相呼应的。本题是找每个柱子左右两边第一个小于该柱子的柱子。这里就涉及到了单调栈很重要的性质,就是单调栈里的顺序,是从小到大还是从大到小。因为本题是要找每个柱子左右两边第一个小于该柱子的柱子,所以从栈头(元素从栈头弹出)到栈底的顺序应该是从大到小的顺序!只有栈里从大到小的顺序,才能保证栈顶元素找到左右两边第一个小于栈顶元素的柱子。此时大家应该可以发现其实就是栈顶和栈顶的下一个元素以及要入栈的三个元素组成了我们要求最大面积的高度和宽度。主要就是分析清楚如下三种情况:
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
stack<int> st;
heights.insert(heights.begin(), 0); // 数组头部加入元素0
heights.push_back(0); // 数组尾部加入元素0
st.push(0);
int result = 0;
for (int i = 1; i < heights.size(); i++) {
while (heights[i] < heights[st.top()]) {
int mid = st.top();
st.pop();
int w = i - st.top() - 1;
int h = heights[mid];
result = max(result, w * h);
}
st.push(i);
}
return result;
}
};