【leetcode】算法题记录(201-210)

数字范围按位与

给你两个整数 left 和 right ,表示区间 [left, right] ,返回此区间内所有数字 按位与 的结果(包含 left 、right 端点)。

示例 1:

输入:left = 5, right = 7
输出:4

示例 2:

输入:left = 0, right = 0
输出:0

示例 3:

输入:left = 1, right = 2147483647
输出:0


提示:

    0 <= left <= right <= 2^31 - 1

解题思路:最开始的办法就是从 left 开始逐个求与,如果结果为 0 则返回结果。但是发现竟然案例全部通过超时。。。 后面想到一个提前返回的条件,就是当 right 大于 left 的两倍时,结果肯定是 0 的,但是这里需要注意 int 越界的问题。

后面发现其实这道题是找公共前缀问题,可以利用 Brian Kernighan 算法,用于清除二进制串中最右边的 1, 即 n & (n-1);

即这样运算的话,则 right 最终会小于或者等于 left,如果 right = 0 则表明不存在公共前缀,若大于 0 则表明为公共前缀。

class Solution {
public:
    int rangeBitwiseAnd(int left, int right) {
        if((long)right > ((long)left << 1)) return 0;
        int res = left;
        while(left != right) {
            ++left;
            res &= left;
            if(res == 0) break;
        }
        return res;
    } 
};

class Solution {
public:
    int rangeBitwiseAnd(int left, int right) {
        while(right > left) {
            right = right & (right - 1);
        }
        return right;
    }
};

快乐数

编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」定义为:

  • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
  • 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
  • 如果 可以变为 1,那么这个数就是快乐数。

如果 n 是快乐数就返回 true ;不是,则返回 false 。

示例 1:

输入:19
输出:true
解释:
1^2 + 9^2 = 82
8^2 + 2^2 = 68
6^2 + 8^2 = 100
1^2 + 0^2 + 0^2 = 1

示例 2:

输入:n = 2
输出:false

提示:

    1 <= n <= 2^31 - 1

解题思路:利用 set 存储每次计算的结果,根据提示重复计算的过程只有两种结果,要不得到 1 ,要不无限循环。

所以思路就在与重复计算平方和,得到 1 返回 true,进入循环返回 false。

class Solution {
public:
    bool isHappy(int n) {
        set<int> MidRes;
        bool res = false;
        int mulSum = 0;
        while(true) {
            while(n > 0) {
                int temp = n % 10;
                mulSum += (temp * temp);
                n /= 10; 
            }
            if(MidRes.find(mulSum) == MidRes.end()) {
                MidRes.insert(mulSum);
                n = mulSum;
                mulSum = 0;
            }else {
                if(mulSum == 1) res = true;
                break;
            }
        }
        return res;
    }
};

移除链表元素

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

示例 1:

输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

示例 2:

输入:head = [], val = 1
输出:[]

示例 3:

输入:head = [7,7,7,7], val = 7
输出:[]


提示:
    列表中的节点数目在范围 [0, 10^4] 内
    1 <= Node.val <= 50
    0 <= val <= 50

解题思路:遍历链表,删除节点。

/**
 * 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* removeElements(ListNode* head, int val) {
        ListNode* res = new ListNode(-1, head);
        ListNode* pre = res, *curr = head;
        while(curr != nullptr) {
            if(curr->val == val) {
                pre->next = curr->next;
                curr = curr->next;
            }else {
                pre = curr;
                curr = curr->next;
            }
        }
        return res->next;
    }
};

计数质数

统计所有小于非负整数 n 的质数的数量。

示例 1:

输入:n = 10
输出:4
解释:小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 。

示例 2:

输入:n = 0
输出:0

示例 3:

输入:n = 1
输出:0

提示:

    0 <= n <= 5 * 10^6

解题思路:想着是一道简单题,应该暴力没有问题,但是还是超时了。。

可以提前将非质数提前标记出来,可以知道质数的倍数肯定就不是质数了,所以利用这一点,可以提前将非质数标记出来。

这样的话可以发现都不用去判断一个数是否是质数了,当遍历到这个数的时候,它还没有被标记,就说明它就是个质数。

但是这样标记的话,会发现有一些数会被重复标记,也会浪费一点时间,比如 6 既会被 2 标记,又会被 3 标记。

如果想要只标记一次,可以采用 线性筛 的方法,即标记的时候,对当前整数进行操作,

  • 即 x * primes[0], x * primes[1], …,且在发现 x mod primes[i] ​= 0 的时候结束当前标记
  • 因为 x mod primes[i] ​= 0,则表明 x * primes[i+1] 这个数一定会被 primes[i] 这个质数整除,保证每个非质数只会被其最小的质因数标记
  • 所以也就保证了每个非质数只会被标记一次
class Solution {
public:
    int countPrimes(int n) {
        int res = 0;
        vector<int> isPrime(n, 1);
        for(int i = 2; i < n; ++i) {
            if(isPrime[i]) {
                res += 1;
                if ((long long)i * i < n) {
                    for (int j = i * i; j < n; j += i) {
                        isPrime[j] = 0;
                    }
                }
            }
        }
        return res;
    }
};

同构字符串

给定两个字符串 s 和 t,判断它们是否是同构的。

如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。

每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。

示例 1:

输入:s = "egg", t = "add"
输出:true

示例 2:

输入:s = "foo", t = "bar"
输出:false

示例 3:

输入:s = "paper", t = "title"
输出:true

提示:

    可以假设 s 和 t 长度相同。

解题思路:既然是映射关系,那么就建立映射表,根据映射表来判断两个字符串是否是同构的。需要注意的是因为是强对应关系,需要管理两个映射表。

class Solution {
public:
    bool isIsomorphic(string s, string t) {
        if(s.size() != t.size()) return false;
        unordered_map<char, char> sTt;
        unordered_map<char, char> tTs;
        for(int i = 0; i < s.size(); ++i) {
            if(sTt.find(s[i]) == sTt.end() && tTs.find(t[i]) == tTs.end() ) {
                sTt[s[i]] = t[i];
                tTs[t[i]] = s[i];
            }else {
                if(s[i] != tTs[t[i]] || t[i] != sTt[s[i]]) return false;
            }
        }
        return true;
    }
};

反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例 1:

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

示例 2:

输入:head = [1,2]
输出:[2,1]

示例 3:

输入:head = []
输出:[]

提示:

    链表中节点的数目范围是 [0, 5000]
    -5000 <= Node.val <= 5000


进阶:链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?

解题思路:递归和迭代,两种方法。迭代是从前面开始反转,借助头前节点来完成反转。递归是从后面开始反转,直接记录最后一个节点为反转后的头结点。

/**
 * 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* ret = nullptr;

    void reverse(ListNode* node) {
        if(!node->next) {
            ret = node;
            return;
        }
        reverse(node->next);
        node->next->next = node;
        node->next = nullptr;
    }

    ListNode* reverseList(ListNode* head) {
        if(head != nullptr) reverse(head);
        return ret;
    }

    // 迭代版本
    ListNode* reverseList(ListNode* head) {
        ListNode* res = new ListNode(-1);
        ListNode* curr = head;
        while(curr) {
            ListNode* next = curr->next;
            curr->next = res->next;
            res->next = curr;
            curr = next;
        }
        return res->next;
    }
};

课程表

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则必须先学习课程 bi 。

例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。

请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。

示例 1:

输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。

示例 2:

输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成​课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。

 

提示:

    1 <= numCourses <= 10^5
    0 <= prerequisites.length <= 5000
    prerequisites[i].length == 2
    0 <= ai, bi < numCourses
    prerequisites[i] 中的所有课程对 互不相同

解题思路:利用 set 记录已经学习的课程。利用 map 记录每个课程的全部前置课程。

class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
        //说明没有限制
        if(prerequisites.size() == 0) return true;
        set<int> started;
        for(int i = 0; i < numCourses; ++i) started.insert(i);
        map<int, set<int>> miv;
        for(auto s : prerequisites) {
            int temp = s[0];
            int preTemp = s[1];
            started.erase(temp);
            auto it = miv.find(temp);
            if(it == miv.end()) {
                set<int> vi = {preTemp};
                miv[temp] = vi;
            }else {
                miv[temp].insert(preTemp);
            }
        }
        // 说明每个课程都有前置,那么肯定不能实现
        if(numCourses == miv.size()) return false;

        while(started.size() < numCourses) {
            int pre = started.size();
            for(auto it = miv.begin(); it != miv.end();) {
                for(auto s = it->second.begin(); s != it->second.end();) {
                    if(started.find(*s) != started.end()) {
                        s = it->second.erase(s);
                    }else {
                        ++s;
                    }
                }
                if(it->second.empty()) {
                    started.insert(it->first);
                    it = miv.erase(it);
                }else {
                    ++it;
                }
            }
            if(pre == started.size()) break;
        }

        return started.size() >= numCourses;
    }
};

实现 Trie (前缀树)

Trie(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。

请你实现 Trie 类:

  • Trie() 初始化前缀树对象。
  • void insert(String word) 向前缀树中插入字符串 word 。
  • boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
  • boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。
示例:

输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出
[null, null, true, false, true, null, true]

解释
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple");   // 返回 True
trie.search("app");     // 返回 False
trie.startsWith("app"); // 返回 True
trie.insert("app");
trie.search("app");     // 返回 True

提示:
    1 <= word.length, prefix.length <= 2000
    word 和 prefix 仅由小写英文字母组成
    insert、search 和 startsWith 调用次数 总计 不超过 3 * 10^4 次

解题思路:自己想到的办法就是利用两个 set 分别记录单词和前缀的集合,然后查找。在做的时候就想到这样肯定是不正确的,想着应该超时的,没想到竟然 ac 了。

看到题解得知正确的做法应该是要符合这道题的名字,建立前缀树,利用前缀树去查找。

/**
 * Your Trie object will be instantiated and called as such:
 * Trie* obj = new Trie();
 * obj->insert(word);
 * bool param_2 = obj->search(word);
 * bool param_3 = obj->startsWith(prefix);
 */
//利用 set
class Trie {
public:
    unordered_set<string> allWord;
    unordered_set<string> allPrefix;

    /** Initialize your data structure here. */
    Trie() {
        allWord.clear();
        allPrefix.clear();
    }
    
    /** Inserts a word into the trie. */
    void insert(string word) {
        allWord.insert(word);
        for(int i = 1; i <= word.size(); ++i) {
            allPrefix.insert(word.substr(0,i));
        }
    }
    
    /** Returns if the word is in the trie. */
    bool search(string word) {
        return allWord.find(word) != allWord.end();
    }
    
    /** Returns if there is any word in the trie that starts with the given prefix. */
    bool startsWith(string prefix) {
        return allPrefix.find(prefix) != allPrefix.end();
    }
};

//利用前缀树
class Trie {
public:
    vector<Trie*> children;
    bool isEnd;

    Trie* searchPrefix(string prefix) {
        Trie* node = this;
        for (char ch : prefix) {
            ch -= 'a';
            if (node->children[ch] == nullptr) {
                return nullptr;
            }
            node = node->children[ch];
        }
        return node;
    }

    /** Initialize your data structure here. */
    Trie() {
        children = vector<Trie* >(26, 0);
        isEnd = false;
    }
    
    /** Inserts a word into the trie. */
    void insert(string word) {
        Trie* node = this;
        for (char ch : word) {
            ch -= 'a';
            if (node->children[ch] == nullptr) {
                node->children[ch] = new Trie();
            }
            node = node->children[ch];
        }
        node->isEnd = true;
    }
    
    /** Returns if the word is in the trie. */
    bool search(string word) {
        Trie* node = this->searchPrefix(word);
        return node != nullptr && node->isEnd;
    }
    
    /** Returns if there is any word in the trie that starts with the given prefix. */
    bool startsWith(string prefix) {
        return this->searchPrefix(prefix) != nullptr;
    }
};

长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

示例 2:

输入:target = 4, nums = [1,4,4]
输出:1

示例 3:

输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0

提示:

    1 <= target <= 10^9
    1 <= nums.length <= 10^5
    1 <= nums[i] <= 10^5

进阶:

    如果你已经实现 O(n) 时间复杂度的解法, 请尝试设计一个 O(n log(n)) 时间复杂度的解法。

解题思路:依次遍历,得到从每个数字开始达到 target 的最短长度,然后得到最终答案。

优化:

  • 因为都是正整数,所以可以记录前缀和,直接利用二分法找到每个数字的最短长度。
  • 利用滑动窗口,start 和 end 记录窗口的起始位置,若窗口总和小于 target,end 往后移,直到大于 target,更新长度;并且使 start 左移,直至小于 target,更新长度。
class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int res = INT_MAX;
        for(int i = 0; i < nums.size(); ++i) {
            int sum = 0;
            for (int j = i; j < nums.size(); j++) {
                sum += nums[j];
                if (sum >= target) {
                    res = min(res, j - i + 1);
                    break;
                }
            }
        }
        return res == INT_MAX ? 0 : res;
    }
};

课程表 II

现在你总共有 n 门课需要选,记为 0 到 n-1。

在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]

给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。

可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。

示例 1:

输入: 2, [[1,0]] 
输出: [0,1]
解释: 总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。

示例 2:

输入: 4, [[1,0],[2,0],[3,1],[3,2]]
输出: [0,1,2,3] or [0,2,1,3]
解释: 总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
     因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。

说明:
    输入的先决条件是由边缘列表表示的图形,而不是邻接矩阵。详情请参见图的表示法。
    你可以假定输入的先决条件中没有重复的边。

提示:
    这个问题相当于查找一个循环是否存在于有向图中。如果存在循环,则不存在拓扑排序,因此不可能选取所有课程进行学习。
    通过 DFS 进行拓扑排序。
    拓扑排序也可以通过 BFS 完成。

解题思路:按照题目的提示来,建立有向图,判断图中是否存在循环或者全部遍历完。做法还是和之前课程表的做法一样,利用 set 记录已经学习的课程。利用 map 记录每个课程的全部前置课程, 其实 map 记录的就是有向图的边。

class Solution {
public:
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
        set<int> started;
        for(int i = 0; i < numCourses; ++i) started.insert(i);
        if(prerequisites.size() == 0) return vector<int>(started.begin(), started.end());
        map<int, set<int>> miv;
        for(auto s : prerequisites) {
            int temp = s[0];
            int preTemp = s[1];
            started.erase(temp);
            auto it = miv.find(temp);
            if(it == miv.end()) {
                set<int> vi = {preTemp};
                miv[temp] = vi;
            }else {
                miv[temp].insert(preTemp);
            }
        }
        vector<int> res;
        // 说明每个课程都有前置,那么肯定不能实现
        if(numCourses == miv.size()) return res;

        res = vecotr<int>(started.begin(), started.end());

        while(res.size() < numCourses) {
            int pre = res.size();
            set<int> newadd;
            for(auto it = miv.begin(); it != miv.end();) {
                for(auto s = it->second.begin(); s != it->second.end();) {
                    if(started.find(*s) != started.end()) {
                        s = it->second.erase(s);
                    }else {
                        ++s;
                    }
                }
                if(it->second.empty()) {
                    newadd.insert(it->first);
                    res.emplace_back(it->first);
                    it = miv.erase(it);
                }else {
                    ++it;
                }
            }
            if(pre == res.size()) break;
            started = newadd;
        }

        if(res.size() != numCourses) res.clear();

        return res;
    }
};

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦