剑指 offer 栈、队列、位运算题目汇总(C++版)
1、用两个栈实现队列
用两个栈实现一个队列。队列的声明如下,实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 返回 -1 )
思路:我们知道队列特点是先进先出,而栈的特点是先进后出。因此我们使用栈1来存储数据,栈2来模拟队列弹出数据。栈2如果为空,就需要把栈1的数据全都 push 过来,否则如果栈2不为空,取出栈顶元素即可。
void appendTail(int value) {
s1.push(value);
}
int deleteHead() {
if(s2.empty()) {
while(!s1.empty()) {
s2.push(s1.top());
s1.pop();
}
}
if(s2.empty()) return -1;
auto t = s2.top();
s2.pop();
return t;
}
2、包含 min 函数的栈
定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。
举例:
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.min(); 返回 -3
minStack.pop();
minStack.top(); 返回 0
minStack.min(); 返回 -2
思路:单调栈
需要维护一个单调栈,来实现返回最小值的操作。
(1) 当我们向栈中压入一个数时,如果该数 ≤ 单调栈的栈顶元素,则将该数同时压入单调栈中;否则,不压入,这是由于栈具有先进后出性质,所以在该数被弹出之前,栈中一直存在一个数比该数小,所以该数一定不会被当做最小数输出。
(2) 当我们从栈中弹出一个数时,如果该数等于单调栈的栈顶元素,则同时将单调栈的栈顶元素弹出。
(3) 单调栈由于具有单调性,所以它的栈顶元素就是当前栈中的最小数。
//sMin为最小栈
void push(int x) {
if(sMin.empty() || x <= sMin.top()) sMin.push(x);
sVal.push(x);
}
void pop() {
if(sVal.top() == sMin.top()) sMin.pop();
sVal.pop();
}
int top() {
return sVal.top();
}
int min() {
return sMin.top();
}
3、栈的压入、弹出序列
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。
举例:
输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
输出:true
解释:可以按以下顺序执行:
push(1),push(2),push(3),push(4),pop() -> 4,
push(5),pop() -> 5,pop() -> 3,pop() -> 2,pop() -> 1
思路:开一个新栈 s 来模拟实时的进出栈操作,在 for 循环里依次喂数,每 push 一个数就去检查弹出序列数组有没有能 pop 出来的。如果最后 s 为空,说明所有数都模拟完成了,一进一出刚好符合,返回true。否则返回 false。
时间复杂度:O(n) 空间复杂度:O(n)
bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {
stack<int> s;
int popId = 0;
for(auto i:pushed) {
s.push(i);
while(!s.empty() && s.top() == popped[popId]) {
s.pop();
popId++;
}
}
if(s.empty()) return true;
return false;
}
4、滑动窗口的最大值
给定一个数组 nums 和滑动窗口的大小 k,找出所有滑动窗口里的最大值。
举例:
输入:nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
思路1:暴力枚举,算法步骤如下:
枚举每个窗口的左边界 i
根据窗口左边界 i 可以计算出右边界 j
遍历当前窗口,计算出最大值存入结果集
时间复杂度:O(nk),n为数组大小,k为窗口大小
空间复杂度:O(1),存结果必须要开的数组不算
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> res;
if(!nums.size() || nums.size() < k || k < 1) return res;
for(int i = 0;i <= nums.size()-k;i++) { //左边界的范围
int j = i+k-1; //根据左边界枚举右边界
int maxV = nums[i]; //设定当前窗口最大值为最左边的数
for(int t = i;t<=j;t++) maxV = max(nums[t],maxV); //遍历当前窗口的每个数找出最大值
res.push_back(maxV); //将最大值存入结果集合
}
return res;
}
思路2:双端队列。窗口向右滑动的过程实际是将窗口的第一个数字删除,同时在窗口的末尾添加一个新数字,这就可以用双端队列来模拟,每次把尾部的数字弹出,再把新的数字压入到头部,然后找队列中最大的元素即可。
为了更快地找到最大的元素,我们可以在队列中只保留那些可能成为窗口最大元素的数字,去掉那些不可能成为窗口中最大元素的数字。考虑这样一个情况,如果队列中进来一个较大的数字,那么队列中比这个数更小的数字就不可能再成为窗口中最大的元素了,因为这个大的数字是后进来的,一定会比之前早进入窗口的小的数字要晚离开窗口,那么那些早进入且比较小的数字就“永无出头之日”,所以就可以弹出队列。
于是我们维护一个双向单调队列,队列放的是元素的下标。假设该双端队列的队头是整个队列的最大元素所在下标,队头至队尾下标代表的元素值依次减小。初始时单调队列为空,随着对数组的遍历,每次插入元素前,首先需要看队头是否还能留在队列中,如果队头下标距离 i 超过了 k,则应该出队。同时需要维护队列的单调性,如果当前遍历到的数 nums[i] 大于或等于队尾元素下标所对应的值,则当前队尾再也不可能充当某个滑动窗口的最大值了,故需要队尾出队。这样就能保证队中元素从队头到队尾始终单调递减。依次遍历一遍数组,每次队头就是每个滑动窗口的最大值的下标。
时间复杂度:O(n),其中n为数组大小
空间复杂度:O(k),k为窗口的大小
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> res;
if(!nums.size() || nums.size() < k || k < 1) return res;
deque<int> que;
for(int i = 0;i<nums.size();i++) {
// 如果当前元素的下标与队头元素下标的距离超过窗口大小,队头出队
if(que.size() && i-que.front() == k) que.pop_front();
//当前元素值大于等于队尾元素 队尾元素出队
while(que.size() && nums[i] >= nums[que.back()]) que.pop_back();
que.push_back(i); //下标i入队
//当滑动窗口首地址i大于等于k时才开始写入最大值
if(i+1 >= k) res.push_back(nums[que.front()]);
}
return res;
}
5、二进制中1的个数
输入一个整数,输出该数32位二进制表示中1的个数。其中负数用补码表示。
思路1: 将1与这个数的每一位做与操作,然后将1左移1位
时间复杂度:O(32) ,无空间复杂度
int NumberOf1(int n) {
if(!n) return n;
int cnt = 0,flag = 1;
while(flag) {
if(n & flag) cnt++;
flag <<= 1;
}
return cnt;
}
思路2:对于上一种解法如果当前位是0,还会做判断,然后一位一位的移动。可以进一步优化为利用n&(n-1) 解答。
n&(n-1)作用:将 n 的二进制表示中的最低位为1的改为0。
时间复杂度:O(n) n这个数中二进制中1的个数, 无空间复杂度
int NumberOf1(int n) {
if(!n) return n;
int cnt = 0;
while(n) {
cnt++;
n &= n-1; //n中有几个1 这个操作就执行几次
}
return cnt;
}
6、数组中数字出现的次数
一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。找出这两个只出现一次的数字。
思路1:先排序 在遍历
当下一个数和自身相同时,移动两步,当下个数与当前数不同时,则将当前数加入答案,移动一步
时间复杂度:O(nlogn) 空间复杂度:O(1)
vector<int> FindNumsAppearOnce(vector<int>& array) {
if (array.empty()) return array;
vector<int> res;
sort(array.begin(), array.end());
for(int i = 0;i<array.size();i++) {
if(i+1 == array.size() || array[i] != array[i+1]) {
res.push_back(array[i]);
}
else i++;
}
if(res[0] > res[1]) swap(res[0], res[1]);
return res;
}
思路2:利用位运算
总体思路:有一道和它类似的题,求数组中只有一个出现一次的数,解法就是从头开始不断地异或,由于相同的两个数异或值为0,因此最终的异或结果就是答案。
而本题有两个只出现一次的数 a 和 b 按异或方法最终只能得到 a 异或 b 的值,就需要思考一下这两个数异或的结果有何特点。可以发现,首先这两个数一定不同,故异或结果一定不为0,那么 a 异或 b 的结果中一定有一位为1,假设是第 x 位,那么就说明了 a 和 b 的二进制的第 x 位不同,根据这一特点,我们可以将数组分为两个集合,即第 x 位为 1 的数的集合和第 x 位为 0 的数的集合,这两部分分别异或出来的两个值就是 a 和 b 的值
另,要求二进制的最后一位1,可以用 lowbit 运算,它可以快速得到 x 的最后一位的 1
时间复杂度:O(n) 空间复杂度:O(1)
//取x最低位的1(二进制为0000 0011,最低为1的那一位是第1位,所以取出后为0000 0001)
int lowbit(int x) {
return x & -x;
}
vector<int> FindNumsAppearOnce(vector<int>& array) {
if (array.empty()) return array;
int s = 0;
for (auto x : array) s ^= x; //s是要找的两个数的异或结果
int flag = lowbit(s); //找到异或结果的二进制中最右边的1
int a = 0, b; //a和b是要求的两个数
for (auto x : array) {
if (flag & x) a ^= x; //根据flag对所有数分组,a一定是要找的数之一
}
b = s ^ a; //两个相同的数异或为0,因为s是a与b异或的结果,故b = s^a = a^b^a
vector<int> res{a,b};
if (res[0] > res[1]) swap(res[0], res[1]);
return res;
}
7、不用加减乘除做加法
写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。
思路:二进制位运算模拟加法
(1)不考虑进位对每一位相加。0加0、1加1结果都是0,0加1、1加0结果都是1。这和异或运算一样;
(2)考虑进位,0加0、0加1、1加0都不产生进位,只有1加1向前产生一个进位。可看成是先做位与运算,然后左移一位;
(3)相加过程重复前两步,直到不产生进位为止。
时间复杂度:O(1),空间复杂度:O(1)
int add(int a, int b) {
while(b) {
int temp = a ^ b;
b = (unsigned int)(a & b) << 1;//c++不支持负值左移,需要强制转换为无符号数
a = temp;
}
return a;
}