剑指 Offer 18. 删除链表的节点

简单的双指针链表操作

第一次写:

  1. class Solution {
  2. public:
  3. ListNode* deleteNode(ListNode* head, int val) {
  4. ListNode* cur = head,pre = head;
  5. while(cur){
  6. if(cur->val == val){
  7. if(cur == head){
  8. return head->next;
  9. }
  10. pre->next = cur->next;
  11. break;
  12. }
  13. pre = cur;
  14. cur = cur->next;
  15. }
  16. return head;
  17. }
  18. };

第二次写

class Solution {
public:
    ListNode* deleteNode(ListNode* head, int val) {
        ListNode* dummy = new ListNode(0);
        dummy->next = head;
        ListNode* pre = dummy, *cur = head;
        while(cur){
            if(cur->val == val){
                pre->next = cur->next;
            }
            pre = cur;
            cur = cur->next;
        }
        return dummy->next;
    }
};

答案:

class Solution {
public:
    ListNode* deleteNode(ListNode* head, int val) {
        if(head->val == val) return head->next;
        ListNode *pre = head, *cur = head->next;
        while(cur != nullptr && cur->val != val) {
            pre = cur;
            cur = cur->next;
        }
        if(cur != nullptr) pre->next = cur->next;
        return head;
    }
};

增加前节点

直接增加一个前节点,这样就不用判断是否头节点符合条件了。

class Solution {
public:
    ListNode* deleteNode(ListNode* head, int val) {
        ListNode* pre = new ListNode(0);
        pre->next = head;
        ListNode* res = pre;
        while(pre->next->val != val){ // 不相等则直接往下遍历
            pre = pre->next;
        }
        pre->next = pre->next->next; // 找到之后直接删除
        return pre->next;
    }
};

剑指offer22链表中倒数第k个节点

这一题,用顺序遍历非常简单,但是用快慢指针属实是没想到。有点类似滑动窗口的思想,保证快节点与慢节点之间差k,等到快节点到尾部,慢节点就是倒数第k个节点的位置。

class Solution {
public:
    ListNode* getKthFromEnd(ListNode* head, int k) {
        ListNode* fast = head;
        ListNode* slow = head;

        while (fast && k > 0) {
            fast = fast->next;
            k--;
        }
        while (fast) {
            fast = fast->next;
            slow = slow->next;
        }

        return slow;
    }
};

剑指 Offer 25. 合并两个排序的链表

通过比较l1的值与l2的值来进行新的链表的构建。当l1与l2中的某一个为null时,直接把不为空的另外一个接在l3的后面即可。

class Solution {
public:
ListNode* l3;
    ListNode* mergeTwoLists(ListNode *l1, ListNode *l2) {
ListNode *dummy = new ListNode(0), *node = dummy;
while (l1 && l2) {
if (l1->val <= l2->val) {
node->next = l1;
l1 = l1->next;
} else {
node->next = l2;
l2 = l2->next;
}
node = node->next;
}
node->next = l1? l1: l2;//这里别看错了,将剩下的一个接在后面。
return dummy->next;
}
};

剑指 Offer 52. 两个链表的第一个公共节点

两个链表同时遍历,为空时直接为另外一个节点的头,当碰到时就是第一个公共节点。
这样子时间复杂度会低点,如果是从自己的头循环的话,时间复杂度会很多。

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode* a = headA,*b = headB; 
        while(a!=b){
            if(a == NULL){
                a = headA;
            } else {
                a = a ->next;
            }
            if(b == NULL){
                b = headB;
            } else {
                b = b->next;
            }
        }
        return a;
    }
};

剑指 Offer 21. 调整数组顺序使奇数位于偶数前面

一个从头找,一个从尾部找,这里需要注意溢出的问题。

class Solution {
public:
    vector<int> exchange(vector<int>& nums)
    {
        int i = 0, j = nums.size() - 1;
        while (i < j)
        {
            while(i < j && (nums[i] & 1) == 1){
                i++;
            } 
            while(i < j && (nums[j] & 1) == 0){ 
                j--;
            }
            swap(nums[i], nums[j]);
        }
        return nums;
    }
};

剑指 Offer 57. 和为s的两个数字(重要)

递增(双指针)

这一题与主站的第一题两数之和有些不同,这一题多加了一个数组是递增的。直接使用双指针,根据大小来调试的方法来获取最终结果。

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        int n = nums.size();
        for(int i = 0, j = n -1; i < j;){
            if(nums[i] + nums[j] > target) {
                j--;
            } else if(nums[i] + nums[j] < target){
                i++;
            } else {
                return{nums[i], nums[j]};
            }
        }
        return {};
    }
};

普通(哈希)

注意这里为了防止,同一个元素选取两次,先进行判断,然后再录入。
这里返回的是下标

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int, int> hashtable;
        for (int i = 0; i < nums.size(); ++i) {
            auto it = hashtable.find(target - nums[i]);
            if (it != hashtable.end()) {
                return {it->second, i};
            }
            hashtable[nums[i]] = i;
        }
        return {};
    }
};

剑指 Offer 58 - I. 翻转单词顺序

偷懒(使用stringsteam)

复杂度高的写法

主要是时间成本高 c++ string 底层实现是一个数组 在数组前面插入元素 需要将每个元素向后移动 O(n)的时间复杂度

class Solution {
public:
    string reverseWords(string s) {
        string res="",temp;
        stringstream ss(s);
        while(ss>>temp){
            res=temp+" "+res;//主要是这里的字符串相加的复杂度高
        }
        res.pop_back();
        return res;
    }
};

使用append就会好很多
class Solution {
public:
    string reverseWords(string s) {
        stack<string> stack1;
        istringstream input(s);
        string t;
        string res;
        while(input>>t) stack1.push(t);
        while(!stack1.empty()){
            res.append(stack1.top());
            res.append(" ");
            stack1.pop();
        }
        if(res.length()==0) return res;
        res.pop_back();//把最后一个空格去掉。
        return res;
    }
};

正常双指针

这里正常的想法还是从后面遍历,然后定义一个字符串重新来赋值。我一开始的想法是两个指针分别从前面和后面开始遍历,然后直接再原字符串替换,现在感觉是想多了。

class Solution {
public:
    string reverseWords(string s) {
        if (s.empty())
            return s;
        string res = "";
        int i = s.size() - 1;
        while (i >= 0){
            while (i >= 0 && s[i] == ' ')
                --i;
            int j = i;
            while (i >= 0 && s[i] != ' '){
                --i;
            }
            if (i < j){
                res += s.substr(i + 1, j - i);
                res += " ";
            }
        }
        return res.substr(0, res.size() - 1);
    }
};

剑指 Offer II 007. 数组中和为 0 的三个数(双指针+二分法)

  • 先固定一个数a,然后b、c只能从两边向中间靠拢(在a之后)
  • 去重就是看当数是否等于前一个数,如果相等则跳过当前循环
  • 复杂度为o(n2),因为第一次循环遍历了n次,第二次循环两个指针加起来遍历n次
  • 使用二分法的方法判断时,注意是while,不能是if因为在c判断完之前b应该保持不动。

    class Solution {
    public:
      vector<vector<int>> threeSum(vector<int>& nums) {
          int n = nums.size();
          sort(nums.begin(), nums.end());
          vector<vector<int>> ans;
          // 枚举 a
          for (int first = 0; first < n; ++first) {
              // 需要和上一次枚举的数不相同
              if (first > 0 && nums[first] == nums[first - 1]) {
                  continue;//避免相同
              }
              // c 对应的指针初始指向数组的最右端
              int third = n - 1;
              int target = -nums[first];
              // 枚举 b
              for (int second = first + 1; second < n; ++second) {
                  // 需要和上一次枚举的数不相同
                  if (second > first + 1 && nums[second] == nums[second - 1]) {
                      continue;
                  }
                  // 需要保证 b 的指针在 c 的指针的左侧
                  while (second < third && nums[second] + nums[third] > target) {
                      --third;
                  }
                  // 如果指针重合,随着 b 后续的增加
                  // 就不会有满足 a+b+c=0 并且 b<c 的 c 了,可以退出循环
                  if (second == third) {
                      break;
                  }
                  if (nums[second] + nums[third] == target) {
                      ans.push_back({nums[first], nums[second], nums[third]});
                  }
              }
          }
          return ans;
      }
    };
    

    滑动窗口

    剑指 Offer II 008. 和大于等于 target 的最短子数组

  • 一定要注意条件,要认真,胆大心细!!!

  • 千万注意边界条件!!!!

    class Solution {
    public:
      int minSubArrayLen(int target, vector<int>& nums) {
          int n = nums.size();
          int sum = 0;
          int minlen = INT_MAX;
          for(int i = 0, j = 0; j < n;j++){
              sum = sum + nums[j];
              while(sum >=target&&i<=j){
                  minlen = min(minlen,j-i+1);
                  sum -= nums[i];
                  i++;
              }
          }
          return minlen == INT_MAX? 0 :minlen;
      }
    };
    

    剑指 Offer II 009. 乘积小于 K 的子数组(求数组个数的滑动窗口)

    比如某次遍历符合题意的子数组为 ABCX,那么在该条件下符合条件的有X,CX,BCX,ABCX共四个(可以进行多个例子,发现个数符合right-left+1)
    我们可能会有疑问:AB,BC也算,为什么不算进去?
    记住一点我们是以最右边的X为必要条件,进行计算符合条件的子数组,否则会出现重复的!每向右移动一个窗口,即可多r-l+1个子数组。
    比如在X为右侧边界时(ABCX),我们把BC算进去了,可是我们在C为最右侧时(ABC),BC已经出现过,我们重复加了BC这个子数组两次!
    换言之,我们拆分子数组时,让num[right]存在,能避免重复计算

    class Solution {
    public:
      int numSubarrayProductLessThanK(vector<int>& nums, int k) {
          int res = 0, mul = 1, n = nums.size();
          if(k == 0||k==1){
              return 0;
          }
          for(int l = 0, r = 0; r < n; r++){
              mul = mul*nums[r];
              while(mul >= k&&l <= r){
                  mul = mul/nums[l];
                  l++;
              }
              res = res+r-l+1;
          }
          return res;
      }
    };
    

    剑指 Offer II 014. 字符串中的变位词(奇妙)

  • 这题就神奇在可以使用数组来判断两个字符串是不是变位词

  • 使用两个字符数组cnt1和cnt2,cnt1代表s1中的所有字符,cnt2为滑动窗口维护的一个与s1等长的字符数组,当cnt1等于cnt2的时候就可以了。

    class Solution {
    public:
      bool checkInclusion(string s1, string s2) {
          int n = s1.length(), m = s2.length();
          if (n > m) {
              return false;
          }
          vector<int> cnt1(26), cnt2(26);//使用数组来判断两个字符串是不是变位串
          for (int i = 0; i < n; ++i) {
              ++cnt1[s1[i] - 'a'];
              ++cnt2[s2[i] - 'a'];
          }
          if (cnt1 == cnt2) {
              return true;
          }
          for (int i = n; i < m; ++i) {
              ++cnt2[s2[i] - 'a'];
              --cnt2[s2[i - n] - 'a'];
              if (cnt1 == cnt2) {
                  return true;
              }
          }
          return false;
      }
    };
    

    剑指 Offer II 015. 字符串中的所有变位词

  • 就是上一题的变种

  • 从返回bool,变成了返回下标的集合

    剑指 Offer II 018. 有效的回文

  • 需要几个函数

    isalpha :判断一个字符是否为字母,如果是则返回true,否则返回false;
    isdigit : 判断一个字符是否表示数字,如果是则返回true,否则返回false;
    isalnum : 判断一个字符是否表示数字或者字母,如果是则返回true,否则返回false;
    islower : 判断一个字符是否为小写字母,如果是则返回true,否则返回false;
    isupper : 判断一个字符是否为大写字母,如果是则返回true,否则返回false;
    tolower : 若字符为字母则转化为小写字母;
    toupper : 若字符为字母则转化为大写字母;
    
    class Solution {
    public:
      bool isPalindrome(string s) {
          int i = 0;
          int j = s.size() - 1;
          while (i < j) {
              while (i < s.size()&&!isalnum(s[i])) {
                  i++;
              }
              while (j>=0&&!isalnum(s[j])) {
                  j--;
              }
              if(i >= j) return true;
              if (tolower(s[i]) != tolower(s[j])) {
                  return false;
              }
              else {
                  i++;
                  j--;
              }
          }
          return true;
      } 
    };
    

    剑指 Offer II 019. 最多删除一个字符得到回文

  • 就是暴力解法

  • 注意写一个函数来判断是否是回文串会更加简单

    class Solution {
    public:
      bool checkPalindrome(const string& s, int low, int high) {
          for (int i = low, j = high; i < j; ++i, --j) {
              if (s[i] != s[j]) {
                  return false;
              }
          }
          return true;
      }
    
      bool validPalindrome(string s) {
          int low = 0, high = s.size() - 1;
          while (low < high) {
              char c1 = s[low], c2 = s[high];
              if (c1 == c2) {
                  ++low;
                  --high;
              } else {
                  return checkPalindrome(s, low, high - 1) || checkPalindrome(s, low + 1, high);
              }
          }
          return true;
      }
    };
    

    剑指 Offer II 020. 回文子字符串的个数(中心扩散法)

    动态规划

    dp写法,每个i,j判断是否是回文串

    class Solution {
    public:
      int countSubstrings(string s) {
          int n = s.size(), res = 0;
          vector<vector<int>> dp(n+1, vector<int>(n+1));
          for(int i = 1; i <= n; i++){
              dp[i][i] = true;
              res++;
          }
          for(int i = n; i >= 1; i--){
              for(int j = i+1; j <= n; j++){
                  if(s[i-1]==s[j-1]){
                      if(i == j-1) {
                          dp[i][j] = true;
                      } else dp[i][j] = dp[i+1][j-1];
                  } else {
                      dp[i][j] = false;
                  }
                  if(dp[i][j]) res++;
              }
          }
          return res;
      }
    };
    

    中心扩散法

    以每个点为中心,累加以当前点为中心的回文串的数目,因为中心不同,所以回文串也一定不同,注意的是回文串的长度可能是奇数也可能偶数,也就是中心可能为1个,也可能是2个,所以需要分开计算。

    class Solution {
    public:
      int countSubstrings(string s) {
          int count = 0;
          for(int i = 0; i < s.size(); i++){
              count += ext(s, i, i);//有一个中心,也就是说长度为奇数
              count += ext(s, i, i + 1);//有两个中心,也就是说长度为偶数
          }
          return count;
      }
      int ext(string s, int l , int r){
          int count = 0;
          while(l >= 0&& r < s.size()&&s[l] == s[r]){
              --l;
              ++r;
              ++count;
          }
          return count;
      }
    };
    //有一说一想法还挺奇特,确实中点定了之后,每个子集并不相交
    //与之前的dp法相比,没有浪费额外的空间,也没有遍历多余的子集
    

    剑指 Offer II 017. 含有所有字符的最短字符串(重点)

  • 超级复杂的滑动窗口

  • 使用一个long long类型的num来判断是否满足窗口左端滑动的条件
  • 千万注意,一个常数1默认是int类型的,因此只有32位,不能进行超过32位的移位。必须要定义longlong bit = 1,然后进行移位
  • 使用两个哈希表来判断是否满足num赋值条件
  • 千万注意优先级的问题,一定要加括号,即使在复杂也是这样

    class Solution {
    public:
      string minWindow(string s, string t) {
          unordered_map<char, int> hash1;
          unordered_map<char, int> hash2;
          long long num = 0, bit = 1;
          string ansstr = "";
          int n = s.size(), m = t.size();
          for(int i = 0; i < m; i++){
              hash1[t[i]]++;
              if(t[i] >= 'a') num = num|bit << (t[i]-'a'+26);
              else num = num|bit<<(t[i]-'A');
          }
          for(int i = 0, j = 0; j < n; j++){
              hash2[s[j]]++;
              if(hash2[s[j]] >= hash1[s[j]]){
                  if(s[j] >= 'a'){
                      if(num&bit<<(s[j]-'a'+26)) {
                          num = num^bit<<(s[j]-'a'+26);
                      }
                  } else {
                      if(num&bit<<(s[j]-'A')){
                         num =  num^bit<<(s[j]-'A');
                      }
                  }
              }
              while(num == 0&&i<=j){
                  if(j-i+1 < ansstr.size()||ansstr.size()==0){
                      ansstr = s.substr(i, j-i+1);
                  }
                  hash2[s[i]]--;
                  if(hash2[s[i]] < hash1[s[i]]){
                      if(s[i] >= 'a') num = num|bit << s[i]-'a'+26;
                      else num = num|bit<<s[i]-'A';
                  }
                  i++;
              }
          }
          return ansstr;
      }
    };
    

    剑指 Offer II 057. 值和下标之差都在给定的范围内(难)

    滑动窗口+有序集合

    题目可以看作,对于序列中每一个元素x的左侧至多k个元素,如果这k个元素中存在一个元素落在区间[x-t,x+t]中,则完成条件。
    因此使用滑动窗口来维护一个长度为k的窗口,使用有序集合set来存储滑动窗口中的元素。方便进行二分查找。

    class Solution {
    public:
      bool containsNearbyAlmostDuplicate(vector<int>& nums, int k, int t) {
          int n = nums.size();
          set<int> rec;
          for (int i = 0; i < n; i++) {
              auto iter = rec.lower_bound(max(nums[i], INT_MIN + t) - t);//第一个大于等于。
              //而且INT_MIN防止越界。
              if (iter != rec.end() && *iter <= min(nums[i], INT_MAX - t) + t) {
                  return true;
              }
              rec.insert(nums[i]);
              if (i >= k) {//维护滑动窗口
                  rec.erase(nums[i - k]);
              }
          }
          return false;
      }
    };
    

    桶排序

    这题也可以使用桶排序的思想解决,按照元素大小进行分桶,维护一个滑动窗口内的元素对应的元素。
    对于元素x,影响区间为[x-t,x+t]。于是我们可以设定桶得大小为t+1.如果两个元素属于同一个桶,那么这两个元素必然符合条件。如果两个元素属于相邻桶,那么我们需要校验这两个元素得差值不超过t。否则比然不符合条件。

    class Solution {
    public:
      int getID(int x, long w) {
          return x < 0 ? (x + 1ll) / w - 1 : x / w;//返回桶的id,这里的+1ll就是为了把数据转换成longlong类型。
      }
    
      bool containsNearbyAlmostDuplicate(vector<int>& nums, int k, int t) {
          unordered_map<int, int> mp;
          int n = nums.size();
          for (int i = 0; i < n; i++) {
              long x = nums[i];
              int id = getID(x, t + 1ll);//这里的意思是桶的大小为t+1
              if (mp.count(id)) {
                  return true;
              }
    
              if (mp.count(id - 1) && abs(x - mp[id - 1]) <= t) {
                  return true;
              }
    
              if (mp.count(id + 1) && abs(x - mp[id + 1]) <= t) {
                  return true;
              }
    
              mp[id] = x;//桶里面保存前面的元素
              if (i >= k) {
                  mp.erase(getID(nums[i - k], t + 1ll));
              }
          }
          return false;
      }
    };
    

    剑指 Offer II 058. 日程表(需要看)

  • 用红黑树来完成

  • 不可能插入相同的键值,因为直接就会返回false
  • 都是先进行判断然后入队。

    class MyCalendar {
    private:
      map<int,int> calendar;
    public:
      MyCalendar() {
    
      }
      bool book(int start, int end) {
          auto iter = calendar.lower_bound(start);
          //第一个大于等于插入元素的时间段。这个时间段的起始时间不能小于插入时间的终止时间
          if(iter != calendar.end() && iter->first < end)
              return false;
          //迭代器往前走一步就是小于等于当前时间段的最大时间段。这个时间段的终止时间不能大于插入的起始时间
          if(iter != calendar.begin() && (--iter)->second > start)
              return false;
          calendar[start] = end;//不用担心插入相同键值
          return true;
      }
    };