位运算
位异或
- 异或的性质:两个数字异或的结果a^b是将 a 和 b 的二进制每一位进行运算,得出的数字。 运算的逻辑是如果同一位的数字相同则为 0,不同则为 1。
- 异或的规律:(1)任何数和本身异或则为0;(2)任何数和 0 异或是本身;(3)异或满足交换律。 即 a ^ b ^ c ,等价于 a ^ c ^ b。
只出现一次的数字(其余都出现两次) -> (1)可以用unordered_map记录数组元素出现的次数;(2)更快的方式是直接对所有元素进行异或操作,由于其他元素都出现两次(任何数和本身异或则为0),而且异或满足交换律,所以其他元素异或的结果应该为0,最后结果为这个只出现一次的数。
int singleNumber(vector<int>& nums) {
int k = 0; // 性质2,任何数和0异或是本身,所以初始值取0
for(int i = 0; i < nums.size(); i++){
k^=nums[i];
}
return k;
}
vector<int> singleNumbers(vector<int>& nums) {
int k = 0;
for(int num:nums){
k^=num;
}
// 假设这两个数字为a和b,此时k=a^b
// 将nums里的数字分两组,目标是a和b在不同的组里
int pos = 0, a = 0, b = 0;
for(int i = 0; i < 32;i++){
if(k & 1 == 1){ // 找到k中为1的位,说明a和b在该位是不同的
pos = i;
break;
}
k = k >> 1;
}
// 根据找到的pos进行分组,a和b在不同组
for(int num:nums){
if((num >> pos) & 1 == 1){
a^=num;
}else{
b^=num;
}
}
vector<int> v{a,b};
return v;
}
找到数组中只出现一次的数(其余都出现三次) -> (1) 哈希表存储和查找,使用unordered_map;(2) 如果某个数字出现3次,那么这个3个数字的和肯定能被3整除,则其对应二进制位的每一位的和也能被3整除;统计数组中每个数字的二进制中每一位的和,判断该和是否能被3整除。若可以,则只出现一次的数字的二进制数中那一位为0,否则为1。参考
int singleNumber(vector<int>& nums) {
int k = 0;
for(int i = 0; i < 32; i++){
int count = 0;
for(int num:nums){
if((num>>i)&1 == 1){
count++;
}
}
// 出现三次的所有数字的每一位上的和都能被3整除,如果count%3==1表明这个只出现一次的数在这一位为1,否则为0
if(count%3==1){
k += 1<<i;
}
}
return k;
}
相关练习
二进制中1的个数
不用加减乘除做加法 -> 位异或操作可以模拟二进制无进位的加法,位与操作可以模拟二进制的进位(位与的结果需要左移1位)。a可以当做无进位的和,b当做进位,然后不断重复之前的操作,直到进位b为0,a便是和。
int add(int a, int b) {
int sum, carry;
while(b!=0){
sum = a^b;
carry = (unsigned int)(a&b) << 1; // C++负数无法左移,需要转换成无符号整型
a = sum;
b = carry;
}
return a;
}
面试题 01.01. 判定字符是否唯一 -> 如果不能使用额外的数据结构,考虑位运算
// 题目默认字母是小写,只有'a'-'z',字母可以用数组来代替map或者set
bool isUnique(string astr) {
int mark = 0; // 32位, 使用一个int类型的变量来代替长度为26的bool数组
for(char c : astr){
int move_bits = c - 'a';
if((mark & (1 << move_bits)) == (1 << move_bits)){ // 注意运算符优先级,移位(<<、>>) > ==或!= > 逐位与(&)
return false;
}else{
mark |= (1 << move_bits);
}
}
return true;
}
int insertBits(int N, int M, int i, int j) {
// 把N的[i,j]位置0
for(int k = i; k <= j; k++){
if((N & (1 << k)) != 0){
N -= 1<<k;
}
}
return (M<<i) + N;
}
栈、队列
单调队列
概念
单调队列及其应用:
单调队列,就是指队列中的元素是单调的。如:{a1,a2,a3,a4……an}满足a1<=a2<=a3……<=an,a序列便是单调递增序列。同理递减队列也是存在的。
单调队列的出现可以简化问题,队首元素便是最大(小)值,这样,选取最大(小)值的复杂度便为o(1),由于队列的性质,每个元素入队一次,出队一次,维护队列的复杂度均摊下来便是o(1)。
如何维护单调队列呢,以单调递增序列为例:(要特别注意头指针和尾指针的应用)
1、如果队列的长度一定,先判断队首元素是否在规定范围内,如果超范围则将队首移出队列。
2、每次加入元素时和队尾比较,如果当前元素小于队尾且队列非空,则队尾元素依次出队,直到满足队列的单调性为止。
总结:需要求某个队列元素的最大(小)值时(队列元素不断地在进出),可以用一个辅助的单调队列动态地记录最大(小)值。由于所有元素最多进出一次队列,这样均摊下来每次求队列最大(小)值的时间复杂度O(1)。如滑动窗口,可以看成一个元素不断地在进出的队列。
相关练习
滑动窗口的最大值 -> 滑动窗口移动过程相当于一个队列头部元素不断出队列,尾部不断有元素进入。因此,用一个辅助非严格递减的双端队列保存滑动窗口内的元素(由于对队列头有出列操作,队列尾有入列、出列操作,因此用双端队列),队列头部就是滑动窗口内的最大元素,使求滑动窗口的最大值由O(k)变成均摊O(1)。
理解为什么要一个单调队列:如果滑动窗口内的元素为
[
8
,
1
,
2
,
3
,
4
,
1
,
2
,
4
]
[8,1,2,3,4,1,2,4]
[8,1,2,3,4,1,2,4],则辅助的非严格递减的队列为
[
8
,
4
,
4
]
[8,4,4]
[8,4,4]。当元素8出队列,由于1,2,3都小于4,所以4为8移出后队列的最大值(暂不考虑进入队列的元素),因此才有了维护单调队列第二步:每次加入元素时和队尾比较,如果当前元素大于队尾且队列非空,则队尾元素依次出队,直到满足队列的单调性为止。
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
// 如何用O(1)的时间复杂度求出滑动窗口内最大值 -> 一个辅助的双端队列
vector<int> all;
deque<int> q; // 非严格递减,且队列头部始终是滑动窗口内最大的元素
if(nums.size() == 0){
return all;
}
q.push_back(nums[0]);
for(int i = 1; i < k; i++){
while(!q.empty() && nums[i] >= q.back()){
q.pop_back();
}
q.push_back(nums[i]);
}
all.push_back(q.front());
// O(2n)
for(int i = 1, j = i+k-1; j < nums.size(); i++,j++){
if(nums[i-1]==q.front()){ // nums[i-1]为滑动窗口要移除的元素
q.pop_front();
}
// 每个元素最多一次入队列,一次出队列,因此操作队列q的总体时间复杂度O(n)(不是单次循环)
while(!q.empty() && nums[j] > q.back()){
q.pop_back();
}
q.push_back(nums[j]);
all.push_back(q.front());
}
return all;
}
队列的最大值 -> 与上一题类似。
包含min函数的栈 -> 由于栈是先进后出,因此这里用一个辅助的栈存储栈的非严格递减元素(PS:非严格是指连续的元素可以相等),保证栈顶是当前栈中最小的元素。
滑动窗口
LeetCode 滑动窗口(Sliding Window)类问题总结
这类题目脱离不开主串(主数组)和子串(子数组)的关系,要求的时间复杂度往往是 O(n),空间复杂度往往是常数级的。之所以是滑动窗口,是因为,遍历的时候,两个指针一前一后夹着的子串(子数组)类似一个窗口,这个窗口大小和范围会随着前后指针的移动发生变化。
小技巧:当需要对字符建立hashmap的时候,用数组(桶)代替hashmap,直接字符作为key,字符在字符串中索引或者统计字符在字符串中出现的次数作为value,vector哈希散列表。
相关练习
和为s的两个数字 -> 由于是递增排序的数组,利用双指针i、j,从数组两端向中间移动。和小于s,则i++;和大于s,则j–。
和为s的连续正数序列 -> 连续的整数序列相当于不同大小的窗口,利用两个指针i、j指示窗口的左右边界。
无重复字符的最长子串 -> 注意子串是连续的,序列可以不连续
// 用数组(桶)代替hashmap,直接字符作为索引/键key,索引作为值,vector哈希散列表
int lengthOfLongestSubstring(string s) {
vector<int> pos(256,-1); // key:字符,value:该字符在s中的下标
int i = 0, j = 0; // [i,j]
int len = 0;
while(j < s.size()){
if(pos[s[j]] >= i){ // 说明在[i,j)范围内有与s[j]一样的字符
len = max(len,j-i);
i = pos[s[j]]+1; // i位置的确定需要保证[i,j]区间内不包含重复的字符
}
pos[s[j]] = j; //覆盖之前的值,记录当前asscii码出现在字符串中的最大下标
j++; // j一直向后移动
}
len = max(len,j-i); // 出循环还需要再判断一次!
return len;
}
找到字符串中所有字母异位词 -> 除了滑动窗口外,还有两点有助于字母异位子串的判断:1. 对于字符作为key,由于字符的范围是0-255,整型数组直接访问代替map;2. 如果vector里面的元素类型是简单类型(内置类型),可以直接使用“==”或者“!=”进行比较。(PS:甚至可以使用“<=” “<” “>=” ">"比较两个vector大小:按照字典序排列)
vector<int> findAnagrams(string s, string p) {
vector<int> pos;
// 对于字符作为key,由于字符的范围是0-255,整型数组直接访问代替map
vector<int> need(256,0); // 统计p的不同字符的个数
vector<int> count(256,0); // 统计滑动窗口内不同字符的个数
for(int i = 0; i < p.size(); i++){
need[p[i]]++;
if(i < s.size()){ // 可能存在p的长度大于s的情况
count[s[i]]++;
}
}
//如果vector里面的元素类型是简单类型(内置类型),可以直接使用“==”或者“!=”进行比较
if(need==count){
pos.push_back(0);
}
// 固定长度的滑动窗口,利用双指针i,j
for(int i = 1, j = p.size(); j < s.size(); i++,j++){
count[s[i-1]]--;
count[s[j]]++;
if(need==count){
pos.push_back(i);
}
}
return pos;
}
最小覆盖子串 -> 此题是判断滑动窗口内的子字符串是否包含字符串t的所有字母,滑动窗口的大小会改变,同时包含关系两个比较字符串的长度不一定一样长,所以不能像上一题直接need==count就可以判断两个是字母异位的子串。
这里借助一个cnt变量记录滑动窗口内字符串与字符串t的重叠字符数量。关键在于判断时count[s[r]] < need[s[r]]
和count[s[l]] <= need[s[l]]
。对于count[s[r]] < need[s[r]]
,如"aab"与"abc"匹配个数为2,而count[‘a’]=2,count[‘b’]=1,count[‘c’]=0,计算某个字符匹配个数最大不能超过字符串p中该字符的个数;对于count[s[l]] <= need[s[l]]
,如"aab"与"abc",若count[‘a’]>need[‘a’],将最左边的a移出,匹配的字符个数并不会减少。
string minWindow(string s, string t) {
vector<int> need(128,0);
vector<int> count(128,0);
for(int i = 0; i < t.size(); i++){
need[t[i]]++;
}
int l = 0,r = 0; // [l,r]
int start = 0, len = INT_MAX, cnt = 0;
// 外层循环,r向右移动,直到滑动窗口内字符串包含t
while( r < s.size()){
if(need[s[r]]!=0 && count[s[r]] < need[s[r]]){
cnt++;
}
count[s[r]]++;
r++;
// 内层循环,加入尾部一个字符后(或者删除头部一个字符),窗口内字符串包含t,则l向右移动,
// 直到子串包含t,且不能进一步缩小为止。
while(cnt==t.size()){
if(r-l < len){
start = l;
len = r-l;
}
if(need[s[l]]!=0 && count[s[l]] <= need[s[l]]){
cnt--;
}
count[s[l]]--;
l++;
}
}
return len == INT_MAX ? "" : s.substr(start,len);
}