剑指 Offer 专项突击版刷题笔记 3

第 5 天 字符串

剑指 Offer II 014. 字符串中的变位词

给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的某个变位词。

换句话说,第一个字符串的排列之一是第二个字符串的 子串 。

示例 1:

输入: s1 = “ab” s2 = “eidbaooo”
输出: True
解释: s2 包含 s1 的排列之一 (“ba”).

示例 2:

输入: s1= “ab” s2 = “eidboaoo”
输出: False

提示:

1 <= s1.length, s2.length <= 104
s1 和 s2 仅包含小写字母

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/MPnaiL
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

原本我还想着使用一个 vector<vector> array 作为前缀和呢,每一个 vector 保存的是到目前为止出现的字母的个数,vector 第 i 个位置的 int 值表示第 i 个字母出现的次数

但是想想要映射到哈希表我也不知道怎么将 26 个 int 映射成一个地址……

于是我本来想连续查找的

class Solution {
public:
    bool checkInclusion(string s1, string s2) {
        vector<int> target_count(26, 0), tmp_count(26, 0);
        int sum_count = s1.size(), tmp_sum_count = 0;

        for (char cha : s1) {
            ++target_count[cha - 'a'];
        }

        // 初始化变量
        tmp_count = target_count;
        tmp_sum_count = sum_count;

        for (char cha : s2) {
            if (tmp_count[cha - 'a'] > 0) {
                --tmp_count[cha - 'a'];

                --tmp_sum_count;
                if (tmp_sum_count == 0) return true;
            }
            // 查找连续字符串失败,恢复变量
            else {
                tmp_count = target_count;
                tmp_sum_count = sum_count;
            }
        }

        return false;
    }
};

但是遇到了连续查找的问题

输入:
"adc"
"dcda"
输出:
false
预期结果:
true

就,我一开始找 dc 的时候都是正确的,但是之后又遇到了 d,这个时候我会归为连续中断的类型,因为 target 中没有足够的字符可以取了

从这种情况来看,我应该记录第一次找到的字符的位置,并且在连续中断的时候看看是不是我第一个找到的字符,如果是第一个的话就可以撤掉第一个

class Solution {
public:
    bool checkInclusion(string s1, string s2) {
        vector<int> target_count(26, 0), tmp_count(26, 0);
        int sum_count = s1.size(), tmp_sum_count = 0, first_char_idx = -1;

        for (char cha : s1) {
            ++target_count[cha - 'a'];
        }

        // 初始化变量
        tmp_count = target_count;
        tmp_sum_count = sum_count;

        for (int i = 0; i < s2.size(); ++i) {
            if (tmp_count[s2[i] - 'a'] > 0) {
                // 如果是第一次找到的字符
                if (tmp_sum_count == s1.size()) {
                    first_char_idx = i;
                }

                --tmp_count[s2[i] - 'a'];

                --tmp_sum_count;
                if (tmp_sum_count == 0) return true;
            }
            // 查找连续字符串失败
            else {
                // 如果即将连续中断的时候,发现中断的位置的字符与第一个字符相同,那么去掉第一个字符,使得中断的位置可以被接上
                if (first_char_idx >= 0 && s2[first_char_idx] == s2[i]) {
                    ++first_char_idx;
                    continue;
                }

                // 恢复变量
                first_char_idx = -1;
                tmp_count = target_count;
                tmp_sum_count = sum_count;
            }
        }

        return false;
    }
};

后面我觉得可能是因为我某一次查找失败了之后,还要再原地再开始查找

class Solution {
public:
    bool checkInclusion(string s1, string s2) {
        vector<int> target_count(26, 0), tmp_count(26, 0);
        int sum_count = s1.size(), tmp_sum_count = 0, first_char_idx = -1;

        for (char cha : s1) {
            ++target_count[cha - 'a'];
        }

        // 初始化变量
        tmp_count = target_count;
        tmp_sum_count = sum_count;

        for (int i = 0; i < s2.size(); ++i) {
            if (tmp_count[s2[i] - 'a'] > 0) {
                // 如果是第一次找到的字符
                if (tmp_sum_count == s1.size()) {
                    first_char_idx = i;
                }

                --tmp_count[s2[i] - 'a'];

                --tmp_sum_count;
                if (tmp_sum_count == 0) return true;
            }
            // 查找连续字符串失败
            else {
                // 如果即将连续中断的时候,发现中断的位置的字符与第一个字符相同,那么去掉第一个字符,使得中断的位置可以被接上
                if (first_char_idx >= 0 && s2[first_char_idx] == s2[i]) {
                    ++first_char_idx;
                    continue;
                }

                // 如果是第一次查找失败,那么再在这个位置原地开始
                if (first_char_idx != -1) {
                    first_char_idx = -1;
                    tmp_count = target_count;
                    tmp_sum_count = sum_count;

                    --i;
                    continue;
                }
            }
        }

        return false;
    }
};

但是还是在复杂测试用例上出错了

最终还是用了滑动窗口

class Solution {
public:
    bool checkInclusion(string s1, string s2) {
        vector<int> target_count(26, 0);
        int len1 = s1.size();

        for (char cha : s1) {
            ++target_count[cha - 'a'];
        }


        for (int i = 0; i < s2.size(); ++i) {
            --target_count[s2[i] - 'a'];
            if(i >= len1)
                ++target_count[s2[i - len1] - 'a'];

            bool nz = false;
            for (int num : target_count) {
                if (num != 0) {
                    nz = true;
                    break;
                }
            }
            if (nz == false) return true;
        }

        return false;
    }
};

执行用时:4 ms, 在所有 C++ 提交中击败了95.66% 的用户
内存消耗:7 MB, 在所有 C++ 提交中击败了82.23% 的用户

傻瓜式判断,如果当前的 target 数组都为 0,说明这个滑动窗口的所有字母及其出现次数跟目标串都是一样的

之后或许还能再判空这方面优化……我懒得写了hhh

官方题解的思路是,一开始就维护两个字母出现次数的数组,目标串一个,滑动窗口一个

之后优化为 diff,两个数组之差,这样只用维护一个,这就跟我一开始想的是一样的

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

与上一题一样,就是参数稍微改了一下

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int> ans;
        vector<int> target_count(26, 0);
        int window_size = p.size();

        for (char cha : p) {
            ++target_count[cha - 'a'];
        }


        for (int i = 0; i < s.size(); ++i) {
            --target_count[s[i] - 'a'];
            if(i >= window_size)
                ++target_count[s[i - window_size] - 'a'];

            bool nz = false;
            for (int num : target_count) {
                if (num != 0) {
                    nz = true;
                    break;
                }
            }
            if (nz == false){
                ans.push_back(i - window_size + 1);
            }
        }

        return ans;
    }
};

剑指 Offer II 016. 不含重复字符的最长子字符串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长连续子字符串 的长度。

示例 1:

输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子字符串是 “abc”,所以其长度为 3。

示例 2:

输入: s = “bbbbb”
输出: 1
解释: 因为无重复字符的最长子字符串是 “b”,所以其长度为 1。

示例 3:

输入: s = “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。

示例 4:

输入: s = “”
输出: 0

提示:

0 <= s.length <= 5 * 104
s 由英文字母、数字、符号和空格组成

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/wtcaE1
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

本来想套之前的滑动窗口,后面想想还是双指针比较好

主要是因为在右指针找到重复的字符之后,左指针需要移动到右指针指向的元素相同的元素的下一位

比如 a b c d e b

一开始

a	b	c	d	e	b
p2				p1

然后 p1 移动

a	b	c	d	e	b
p2					p1

这个时候 p2 应该一直移动到 c

a	b	c	d	e	b
		p2			p1

也就是移动到 p1 指向的元素 b 相同的元素的下一位

一开始我还想用一个 int 来存是否出现过呢,后来看是我想多了

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        if(s.size() == 0) return 0;
        if(s.size() == 1) return 1;

        int p1 = 0, p2 = 0;
        int flag = 0;
        int ans = 1;
        flag |= (1 << (s[0] - 'a'));

        for(p1 = 1; p1 < s.size(); ++p1){
            // 如果没出现过
            if(((flag >> (s[p1] - 'a')) & 1) == 0){
                flag |= (1 << (s[p1] - 'a'));
                ans = max(ans, p1 - p2 + 1);
            }
            // 如果出现过
            else{
                while(s[p2] != s[p1]){
                    flag &= ~(1 << (s[p2] - 'a'));
                    ++p2;
                }
                ++p2;
            }
        }

        return ans;
    }
};

后来老老实实用哈希了

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        if(s.size() == 0) return 0;
        if(s.size() == 1) return 1;

        int p1 = 0, p2 = 0;
        int flag = 0;
        int ans = 1;
        
        unordered_set<char> cha_set;
        cha_set.insert(s[0]);

        for(p1 = 1; p1 < s.size(); ++p1){
            // 如果没出现过
            if(cha_set.find(s[p1]) == cha_set.end()){
                cha_set.insert(s[p1]);
                ans = max(ans, p1 - p2 + 1);
            }
            // 如果出现过
            else{
                while(s[p2] != s[p1]){
                    cha_set.erase(s[p2]);
                    ++p2;
                }
                ++p2;
            }
        }

        return ans;
    }
};

第 6 天 字符串

剑指 Offer II 017. 含有所有字符的最短字符串

给定两个字符串 s 和 t 。返回 s 中包含 t 的所有字符的最短子字符串。如果 s 中不存在符合条件的子字符串,则返回空字符串 “” 。

如果 s 中存在多个符合条件的子字符串,返回任意一个。

注意: 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。

示例 1:

输入:s = “ADOBECODEBANC”, t = “ABC”
输出:“BANC”
解释:最短子字符串 “BANC” 包含了字符串 t 的所有字符 ‘A’、‘B’、‘C’

示例 2:

输入:s = “a”, t = “a”
输出:“a”

示例 3:

输入:s = “a”, t = “aa”
输出:“”
解释:t 中两个字符 ‘a’ 均应包含在 s 的子串中,因此没有符合条件的子字符串,返回空字符串。

提示:

1 <= s.length, t.length <= 105
s 和 t 由英文字母组成

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/M1oyTv
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一开始我也是想的滑动窗口啊,很经典

但是我似乎没有办法协调好这两个指针

class Solution {
public:
    string minWindow(string s, string t) {
        int p1 = 0, p2 = 0;
        int nz_count = 0;
        unordered_map<int, int> t_map;

        int ans_len = INT_MAX;
        string ans;

        for(char cha : t){
            t_map[cha]++;
        }
        nz_count = t_map.size();

        while(p2 < s.size()){
            while(p1 <= p2 && p2 < s.size()){
                if(t_map.find(s[p2]) != t_map.end()){
                    // 可能减到负数
                    t_map[s[p2]]--;
                    // 刚好减到 0 才判断
                    if(t_map[s[p2]] == 0){
                        nz_count--;
                        if(nz_count == 0){
                            if(p2 - p1 + 1 < ans_len){
                                ans_len = p2 - p1 + 1;
                                ans = s.substr(p1, ans_len);
                            }
                            break;
                        }
                    }
                }
                p2++;
            }

            bool flag = false;
            while(p1 <= p2 && p2 < s.size()){
                if(t_map.find(s[p1]) != t_map.end()){
                    if(flag == true) break;
                    // 可能从负数开始增加
                    t_map[s[p2]]++;
                    // 刚好增到 1 才判断
                    if(t_map[s[p2]] == 1){
                        flag = true;
                    }
                }
                p1++;
            }
        }

        return ans;
    }
};

再改了一下,我感觉是逻辑更清晰了,还是不对

class Solution {
public:
    string minWindow(string s, string t) {
        int p1 = 0, p2 = 0;
        int nz_count = 0;
        unordered_map<int, int> t_map;

        bool first_char_matched = false;

        int ans_len = INT_MAX;
        string ans;

        for (char cha : t) {
            t_map[cha]++;
        }
        nz_count = t_map.size();

        while (p1 <= p2 && p2 < s.size()) {
            while (p1 <= p2 && p2 < s.size()) {
                if (t_map.find(s[p2]) != t_map.end()) {
                    // 可能减到负数
                    t_map[s[p2]]--;
                    // 把 p1 移动到第一个匹配的字符上
                    if (!first_char_matched) {
                        first_char_matched = true;
                        p1 = p2;
                    }
                    // 刚好减到 0 才判断
                    if (t_map[s[p2]] == 0) {
                        nz_count--;
                        if (nz_count == 0) {
                            if (p2 - p1 + 1 < ans_len) {
                                ans_len = p2 - p1 + 1;
                                ans = s.substr(p1, ans_len);
                            }
                            p2++;
                            break;
                        }
                    }
                }
                p2++;
            }

            t_map[s[p1]]++;
            if (t_map[s[p1]] == 1) nz_count++;

            p1++;

            while (p1 <= p2 && p2 < s.size()) {
                if (t_map.find(s[p1]) == t_map.end()) {
                    p1++;
                }
                else {
                    break;
                }
            }
        }

        return ans;
    }
};

思考了一下我的思路……感觉确实不太对

A	D	O	B	E	C	O	D	E	B	A	N	C
p1					p2
A	D	O	B	E	C	O	D	E	B	A	N	C
			p1			p2
A	D	O	B	E	C	O	D	E	B	A	N	C
			p1						p2
A	D	O	B	E	C	O	D	E	B	A	N	C
			p1							p2
A	D	O	B	E	C	O	D	E	B	A	N	C
					p1					p2
A	D	O	B	E	C	O	D	E	B	A	N	C
					p1							p2
A	D	O	B	E	C	O	D	E	B	A	N	C
					p1								p2

主要还是用两个 while 来控制两边确实会比较难

看了题解之后才发现别人的思路确实清晰,我的思路都是乱的

class Solution {
public:
    // ori 记录 t 中每一个字符出现的次数
    // cnt 记录滑动窗口中与 t 中字符相同的字符出现的次数
    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) {
            ++ori[c];
        }

        int l = 0, r = -1;
        int len = INT_MAX, ansL = -1, ansR = -1;

        // r 每走一步,l 就紧跟着尝试收缩
        // 对 size_t 转为 int,可以防止 r 作为 int 被转为 size_t 
        while (r < int(s.size())) {
            if (ori.find(s[++r]) != ori.end()) {
                ++cnt[s[r]];
            }
            // 在滑动窗口满足条件的情况下,收缩滑动窗口
            while (check() && l <= r) {
                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:
    unordered_map<int, int> t_map;

    bool check() {
        for (const auto &p: t_map) {
            if (p.second > 0) {
                return false;
            }
        }
        return true;
    }

    string minWindow(string s, string t) {
        for (const auto &c: t) {
            ++t_map[c];
        }

        int l = 0, r = -1;
        int len = INT_MAX, ansL = -1;

        // r 每走一步,l 就紧跟着尝试收缩
        // 对 size_t 转为 int,可以防止 r 作为 int 被转为 size_t 
        while (r < int(s.size())) {
            if (t_map.find(s[++r]) != t_map.end()) {
                --t_map[s[r]];
            }
            // 在滑动窗口满足条件的情况下,收缩滑动窗口
            while (check() && l <= r) {
                if (r - l + 1 < len) {
                    len = r - l + 1;
                    ansL = l; // 记录最小长度时的 l
                }
                // 收缩的时候更新哈希表
                if (t_map.find(s[l]) != t_map.end()) {
                    ++t_map[s[l]];
                }
                ++l;
            }
        }

        return len == INT_MAX ? string() : s.substr(ansL, len);
    }
};

写的太痛苦了……主要还是我写题习惯的问题把

上来就用两个 while 控制 l 和 r 主要因为我想到快排也是这么控制的,但是我没有预估到我的能力是这样的……

其实这样

while 在 r 不越界的情况下
	r 每走一步,l 就紧跟着尝试收缩
	while 在滑动窗口满足条件的情况下
		收缩滑动窗口,取最小长度,记录最小长度时的边界
		收缩的时候更新哈希表

这种方法就很好……就是,明显很清晰

而且在更新哈希表的时候,滑动窗口可能变成不满足条件了的,就终止 while 了,这就是,嗯,如果按照这个思路写,就会自然而然地想出来的

或者这只是我的马后炮……

剑指 Offer II 018. 有效的回文

class Solution {
private:
    bool isSameChar(const char a, const char b) {
        if (isalpha(a) && isalpha(b)) {
            if ((tolower(a) - 'a') == (tolower(b) - 'a')) return true;
        }
        else if (isdigit(a) && isdigit(b)) {
            if ((a - '0') == (b - '0')) return true;
        }

        return false;
    }
public:
    bool isPalindrome(string s) {
        string s_only_alpha_and_digit;
        for (char& cha : s) {
            if (isalpha(cha) || isdigit(cha)) {
                s_only_alpha_and_digit.append(1, cha);
            }
        }

        int len = s_only_alpha_and_digit.size();
        for (int i = 0; i < len / 2; ++i) {
            if (!isSameChar(s_only_alpha_and_digit[i], s_only_alpha_and_digit[len - i - 1])) return false;
        }

        return true;
    }
};

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

class Solution {
public:
    bool validPalindrome(string s) {
        int len = s.size();
        int p1 = 0, p2 = len - 1;

        while(p1 < p2 && s[p1] == s[p2]){
            p1++;
            p2--;
        }

        // 如果不用删除就能得到回文
        if(p1 >= p2) return true;

        int p3 = p1, p4 = p2;

        // 尝试删除右边
        p4 = p2 - 1;

        while(p3 < p4 && s[p3] == s[p4]){
            p3++;
            p4--;
        }

        if(p3 >= p4) return true;

        // 尝试删除左边
        p3 = p1 + 1;
        p4 = p2;

        while(p3 < p4 && s[p3] == s[p4]){
            p3++;
            p4--;
        }

        if(p3 >= p4) return true;

        return false;
    }
};

剑指 Offer II 020. 回文子字符串的个数

class Solution {
private:
    string str;
    bool isSymmetric(int l, int r){
        while(l < r && str[l] == str[r]){
            l++;
            r--;
        }
        if(l >= r) return true;
        else return false;
    }
public:
    int countSubstrings(string s) {
        str = s;
        int ans = 0;
        for(int i = 0; i < s.size(); ++i){
            for(int j = i; j < s.size(); ++j){
                if(isSymmetric(i, j)) ans++;
            }
        }

        return ans;
    }
};
中心拓展

计算有多少个回文子串的最朴素方法就是枚举出所有的回文子串,而枚举出所有的回文子串又有两种思路,分别是:

枚举出所有的子串,然后再判断这些子串是否是回文;
枚举每一个可能的回文中心,然后用两个指针分别向左右两边拓展,当两个指针指向的元素相同的时候就拓展,否则停止拓展。

我一开始写的中心拓展

class Solution {
public:
    int countSubstrings(string s) {
        int l = 0, r = 0;
        int ans = 0;
        for(int i = 0; i < s.size(); ++i){
            l = r = i;
            ans++;
            if(l - 1 >= 0 && r + 1 < s.size() && s[l - 1] == s[r + 1]){
                l--;
                r++;
                ans++;
            }
        }

        return ans;
    }
};

但是这个方法会少判定一些回文串

a	a	a

a
	a
		a
a	a
	a	a
a	a	a

其中

a	a
	a	a

的回文串没有判断到

这些回文串的特点是,中心在 i + 0.5 的位置

一开始我还在想,这些回文串应该归类到哪个整数的位置上……我这样的想法就复杂了

所以就是扫两遍就行了,第一遍是以整数序号为中心,第二遍是以半整数序号为中心

class Solution {
public:
    int countSubstrings(string s) {
        int l = 0, r = 0;
        int ans = 0;
        for(int i = 0; i < s.size(); ++i){
            l = r = i;
            while(l >= 0 && r < s.size() && s[l] == s[r]){
                l--;
                r++;
                ans++;
            }
        }
        for(int i = 0; i < s.size() - 1; ++i){
            l = i;
            r = i + 1;
            while(l >= 0 && r < s.size() && s[l] == s[r]){
                l--;
                r++;
                ans++;
            }
        }

        return ans;
    }
};
线性规划

假设 dp[i][j] 表示 s 的序号 i 到 j 为回文串

那么更新思路就是加一些字符上去

两边各加一个

dp[i-1][j+1] = dp[i][j] && (s[i] == s[j])

第一时间只能想到这样更新了,那其实,经历过中心拓展的坑,这里也有类似的非整数坐标中心的坑

所以其实 dp 的更新与中心拓展的坑的解决方法都是一样的

第 7 天 链表

剑指 Offer II 021. 删除链表的倒数第 n 个结点

很常见的双指针,一开始的时候一个指针先动,另一个指针不动,等到先动的指针移动了 n 步之后,两个指针再同步移动,很经典

/**
 * 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* p1 = head, *p2 = head, *prev = head;
        
        // 找到要删除的节点 p1
        for(int i = 0; i < n; ++i){
            if(p2 != nullptr) p2 = p2->next;
        }
        while(p2 != nullptr){
            prev = p1;
            p1 = p1->next;
            p2 = p2->next;
        }

        // 删除 p1

        // 如果要删除的节点是头节点
        if(p1 == head){
            prev = p1->next;
            p1->next = nullptr;
            //free(p1);
            return prev;
        }
        // 如果要删除的节点是尾结点或中间节点
        prev->next = p1->next;
        //free(p1);
        return head;
    }
};

剑指 Offer II 022. 链表中环的入口节点

/**
 * 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 *p1 = head, *p2 = head;
        if(head == nullptr) return nullptr;
        for(int i = 0; i < 2; ++i){
            if(p2->next == nullptr) return nullptr;
            else p2 = p2->next;
        }
        while(p1 != p2){
            if(p2->next == nullptr || p2->next->next == nullptr) return nullptr;

            p1 = p1->next;
            p2 = p2->next->next;
        }

        return p1;
    }
};

经典快慢指针

但是我都不知道为什么我会在这里错

输入:
[-21,10,17,8,4,26,5,35,33,-7,-16,27,-12,6,29,-12,5,9,20,14,14,2,13,-24,21,23,-21,5]
24
输出:
tail connects to node index 26
预期结果:
tail connects to node index 24

感觉应该是哪里写错了

重写了一遍,结果发现是,快慢指针相遇的地方,不一定是环连接的地方

比如有的情况下,快慢指针相遇的地方确实是环连接的地方,比如我上面那个例子

class Solution {
public:
    ListNode* detectCycle(ListNode* head) {
        ListNode* p1 = head, * p2 = head;
        if (head == nullptr) return nullptr;

        do {
            if (p2->next == nullptr || p2->next->next == nullptr) return nullptr;

            p1 = p1->next;
            p2 = p2->next->next;
        } while (p1 != p2);

        return p1;
    }
};

但是有的情况下,快慢指针相遇的地方的上一位才是环连接的地方,所以还要记录上一个位置

/**
 * 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* p1 = head, * p2 = head, * prev = head;
        if (head == nullptr) return nullptr;

        do {
            if (p2->next == nullptr || p2->next->next == nullptr) return nullptr;

            prev = p1;
            p1 = p1->next;
            p2 = p2->next->next;
        } while (p1 != p2);

        return prev;
    }
};

例如

环从尾部连接到序号 1

n0	n1	n2
p1p2

n0	n1	n2
	p1	p2

n0	n1	n2
		p1p2

还有的情况下快慢指针相遇的地方在环连接的地方之后两个 next

例如

环从尾部连接到序号 1

n0	n1	n2	n3
p1p2

n0	n1	n2	n3
	p1	p2

n0	n1	n2	n3
	p2	p1

n0	n1	n2	n3
			p1p2

这就给我整蒙了

之后看题解才知道这里还有一个数学关系

在这里插入图片描述

首先其实要知道的是,在环中,假设以入环点为序号 0,某个指针现在的序号为 i,环的长度为 len,那么如果你在环中某一个走了 n 步,你接下来的序号为 (n + i) % len

虽然这是很简单的事,但是这就意味着,如果你走的步数之中包含环长,那么从结果上来看,有没有这个环长是无所谓的

他这个意思就是,双指针停止之后,初始化指向头节点的新指针与 slow 指针同步走,slow 指针走 c + (n - 1)(b + c) 步,新指针走 a 步,这两个是一样的

又因为在环中的指针走整数倍的圈数不影响它的位置,所以新指针走 a 步走到了入环点,而 slow 指针走的 c + (n - 1)(b + c) 步,原来在 b 步的位置,现在其实就是 b + c + (n - 1)(b + c) = n(b + c),%(b + c) 得 0,也就是 slow 指针也走到了入环点

这个时候新指针和 slow 指针相遇,可以作为 while 停止条件

/**
 * 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* slow = head, * fast = head;
        if (head == nullptr) return nullptr;

        do {
            if (fast->next == nullptr || fast->next->next == nullptr) return nullptr;

            slow = slow->next;
            fast = fast->next->next;
        } while (slow != fast);
        
        // reuse fast pointer
        fast = head;
        while(fast != slow){
            slow = slow->next;
            fast = fast->next;
        }

        return slow;
    }
};

还有哈希表的做法,一遍遍历,发现在哈希表中出现过的节点,说明这个节点就是入环点,不过还要占 n 的空间

剑指 Offer II 023. 两个链表的第一个重合节点

经典双指针取间隔

一开始两个指针同步走,然后先达到终点的那个指针停下,然后给一个 tmp 赋为落后的那个指针的链表的头节点

落后的指针与 tmp 再同步走,直到落后的指针达到终点,赋落后的那个指针为 tmp,先达到终点的指针赋为其链表的头节点

这样的得到效果是,双指针距离各自的链表的尾部的距离是相同的

然后这个时候双指针再同步走,如果能相遇的话必定相遇

/**
 * 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) {
        if(headA == nullptr || headB == nullptr) return nullptr;
        
        ListNode* pA = headA, *pB = headB, *tmp;
        while(pA != nullptr && pB != nullptr){
            pA = pA->next;
            pB = pB->next;
        }
        if(pA == nullptr){
            tmp = headB;
            while(pB != nullptr){
                tmp = tmp->next;
                pB = pB->next;
            }
            pA = headA;
            pB = tmp;
        }
        else if(pB == nullptr){
            tmp = headA;
            while(pA != nullptr){
                tmp = tmp->next;
                pA = pA->next;
            }
            pB = headB;
            pA = tmp;
        }
        while(pA != pB){
            pA = pA->next;
            pB = pB->next;
        }

        return pA;
    }
};

这个题也可以用哈希表,先遍历一遍 A,然后再遍历一遍 B,遍历 B 的时候如果发现哈希表中已有节点,说明这个节点是相交的节点

还有一种对齐的思路是,把 A 的尾结点与 B 的头节点连接,B 的尾结点与 A 的头节点连接

在这里插入图片描述

得到的结果就是两个指针走过的路变相地对齐了,因为相交点之后的那一段是相同的

在这里插入图片描述

剑指 Offer II 024. 反转链表

还是比较简单的

/**
 * 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* curr = head, *prev = nullptr;
        if(head == nullptr) return nullptr;

        ListNode* tmp_next = curr->next;
        while(tmp_next != nullptr){
            curr->next = prev;
            prev = curr;
            curr = tmp_next;
            tmp_next = curr->next;
        }
        curr->next = prev;
        
        return curr;
    }
};

剑指 Offer II 025. 链表中的两数相加

如果不修改原链表,就用栈来做

/**
 * 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* addTwoNumbers(ListNode* l1, ListNode* l2) {
        stack<int> stk1, stk2, stk3;
        ListNode* p = nullptr, *head = nullptr;

        p = l1;
        while(p != nullptr){
            stk1.push(p->val);
            p = p->next;
        }

        p = l2;
        while(p != nullptr){
            stk2.push(p->val);
            p = p->next;
        }

        int sum = 0, res = 0;
        while(!stk1.empty() && !stk2.empty()){
            sum = stk1.top() + stk2.top() + res;
            if(sum >= 10){
                res = 1;
                sum = sum % 10;
            }
            else{
                res = 0;
            }

            stk1.pop();
            stk2.pop();

            stk3.push(sum);
        }

        while(!stk1.empty()){
            sum = stk1.top() + res;
            if(sum >= 10){
                res = 1;
                sum = sum % 10;
            }
            else{
                res = 0;
            }

            stk1.pop();
            stk3.push(sum);
        }

        while(!stk2.empty()){
            sum = stk2.top() + res;
            if(sum >= 10){
                res = 1;
                sum = sum % 10;
            }
            else{
                res = 0;
            }

            stk2.pop();
            stk3.push(sum);
        }

        if(res != 0) stk3.push(res);
        
        p = nullptr;
        while(!stk3.empty()){
            ListNode* node = new ListNode(stk3.top());
            stk3.pop();

            if(head == nullptr){
                head = node;
            }
            else{
                p->next = node;
            }
            p = node;
        }

        return head;
    }
};

剑指 Offer II 026. 重排链表

中等

给定一个单链表 L 的头节点 head ,单链表 L 表示为:

L0 → L1 → … → Ln-1 → Ln
请将其重新排列后变为:

L0 → Ln → L1 → Ln-1 → L2 → Ln-2 → …

不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

注意最后要把结尾的环断开

/**
 * 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:
    void reorderList(ListNode* head) {
        if (head == nullptr) return;

        stack<ListNode*> nodes;
        ListNode* p = head, * tmp = nullptr;

        while (p != nullptr) {
            nodes.push(p);
            p = p->next;
        }

        p = head;
        int len = nodes.size();
        for (int i = len / 2; i > 0; i--) {
            tmp = nodes.top();
            nodes.pop();
            tmp->next = p->next;
            p->next = tmp;
            p = tmp->next;
        }
        
		// 断开结尾的环
        if (len > 1) {
            if (len % 2 == 0)
                tmp->next = nullptr;
            else
                p->next = nullptr;
        }

    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值