数据结构和算法笔记(二):位运算、栈、队列

位运算

位异或

  1. 异或的性质:两个数字异或的结果a^b是将 a 和 b 的二进制每一位进行运算,得出的数字。 运算的逻辑是如果同一位的数字相同则为 0,不同则为 1。
  2. 异或的规律:(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;
    }

面试题 05.01. 插入

    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);
    }

猜你喜欢

转载自blog.csdn.net/XindaBlack/article/details/105667442