难度中等520
你这个学期必须选修 numCourse
门课程,记为 0
到 numCourse-1
。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们:[0,1]
给定课程总量以及它们的先决条件,请你判断是否可能完成所有课程的学习?
示例 1:
输入: 2, [[1,0]] 输出: true 解释: 总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。所以这是可能的。
示例 2:
输入: 2, [[1,0],[0,1]] 输出: false 解释: 总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0;并且学习课程 0 之前,你还应先完成课程 1。这是不可能的。
提示:
- 输入的先决条件是由 边缘列表 表示的图形,而不是 邻接矩阵 。详情请参见图的表示法。
- 你可以假定输入的先决条件中没有重复的边。
1 <= numCourses <= 10^5
此题就是拓扑排序吧。
课程安排图是否是 有向无环图(DAG)。即课程间规定了前置条件,但不能构成任何
环路,否则课程前置条件将不成立。
思路是通过 拓扑排序 判断此课程安排图是否是 有向无环图(DAG) 。 拓扑排序原
理: 对 DAG 的顶点进行排序,使得对每一条有向边 (u,v),均有 u(在排序记录
中)比 v 先出现。亦可理解为对某点 v 而言,只有当 v 的所有源点均出现了,v
才能出现。
通过课程前置条件列表 prerequisites 可以得到课程安排图的 邻接表 adjacenc
y,以降低算法时间复杂度,以下两种方法都会用到邻接表。
作者:jyd
链接:https://leetcode-cn.com/problems/course-schedule/solution/cou
rse-schedule-tuo-bu-pai-xu-bfsdfsliang-chong-fa/
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<vector<int>> course(numCourses);
vector<int> degree(numCourses);
// for (const auto& info: prerequisites) {
for(vector<int> v:prerequisites){
course[v[1]].push_back(v[0]);// 0——>1、、
degree[v[0]]++; //degree[1]++;
}
queue<int> q;
for(int i=0;i<numCourses;i++){
if(degree[i]==0) q.push(i);
}
int visit=0;
while(!q.empty()){
// int size=q.size();
// for(int i=0;i<size;i++){
visit++;
int x=q.front();
// cout<<x<<endl;
q.pop();
for(int y:course[x]){
// cout<<y<<endl;
degree[y]--;
if(degree[y]==0) q.push(y);
}
// }
}
return visit==numCourses;
}
};
难度中等367
实现一个 Trie (前缀树),包含 insert
, search
, 和 startsWith
这三个操作。
示例:
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
说明:
- 你可以假设所有的输入都是由小写字母
a-z
构成的。 - 保证所有输入均为非空字符串。
Trie 是一颗非典型的多叉树模型,多叉好理解,即每个结点的分支数量可能为多个。
为什么说非典型呢?因为它和一般的多叉树不一样,尤其在结点的数据结构设计上,比如一般的多叉树的结点是这样的:
struct TreeNode {
VALUETYPE value; //结点值
TreeNode* children[NUM]; //指向孩子结点
};
而 Trie 的结点是这样的(假设只包含'a'~'z'中的字符):
struct TrieNode {
bool isEnd; //该结点是否是一个串的结束
TrieNode* next[26]; //字母映射表
};
要想学会 Trie 就得先明白它的结点设计。我们可以看到TrieNode结点中并没有直接保存字符值的数据成员,那它是怎么保存字符的呢?
这时字母映射表next 的妙用就体现了,TrieNode* next[26]中保存了对当前结点而言下一个可能出现的所有字符的链接,因此我们可以通过一个父结点来预知它所有子结点的值:
for (int i = 0; i < 26; i++) {
char ch = 'a' + i;
if (parentNode->next[i] == NULL) {
说明父结点的后一个字母不可为 ch
} else {
说明父结点的后一个字母可以是 ch
}
}
Trie 中一般都含有大量的空链接,因此在绘制一棵单词查找树时一般会忽略空链接,同时为了方便理解我们可以画成这样:
作者:huwt
链接:https://leetcode-cn.com/problems/implement-trie-prefix-tree/solution/trie-tree-de-shi-xian-gua-he-chu-xue-zhe-by-huwt/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
总结出 Trie 的几点性质:
Trie 的形状和单词的插入或删除顺序无关,也就是说对于任意给定的一组单词,Trie 的形状都是唯一的。
查找或插入一个长度为 L 的单词,访问 next 数组的次数最多为 L+1,和 Trie 中包含多少个单词无关。
Trie 的每个结点中都保留着一个字母表,这是很耗费空间的。如果 Trie 的高度为 n,字母表的大小为 m,最坏的情况是 Trie 中还不存在前缀相同的单词,那空间复杂度就为 O(m^n)。
最后,关于 Trie 的应用场景,希望你能记住 8 个字:一次建树,多次查询。(慢慢领悟叭~~)
class Trie {
private:
bool isEnd;
Trie* next[26];
public:
/** Initialize your data structure here. */
Trie() {
isEnd=false;
memset(next, 0, sizeof(next));
}
/** Inserts a word into the trie. */
void insert(string word) {
Trie* node=this;
for(char c:word){
if(node->next[c-'a']==NULL){
node->next[c-'a']=new Trie();
}
node=node->next[c-'a'];
}
node->isEnd=true;
}
/** Returns if the word is in the trie. */
bool search(string word) {
Trie* node=this;
for(char c:word){
node=node->next[c-'a'];
if(node==NULL) return false;
}
return node->isEnd;
}
/** Returns if there is any word in the trie that starts with the given prefix. */
bool startsWith(string prefix) {
Trie* node=this;
for(char c:prefix){
node=node->next[c-'a'];
if(node==NULL) return false;
}
return true;
}
};
/**
* 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);
*/
难度中等648
在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
示例 1:
输入: [3,2,1,5,6,4] 和
k = 2
输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6] 和
k = 4
输出: 4
说明:
你可以假设 k 总是有效的,且 1 ≤ k ≤ 数组的长度。
这道题据说是面试的高频考题,同时也是基础算法的应用。
借助 partition 操作定位到最终排定以后索引为 len - k
的那个元素(特别注意:随机化切分元素)
partition(切分)操作,使得:
对于某个索引 j,nums[j] 已经排定,即 nums[j] 经过 partition(切分)操作以后会放置在它 “最终应该放置的地方”;
nums[left] 到 nums[j - 1] 中的所有元素都不大于 nums[j];
nums[j + 1] 到 nums[right] 中的所有元素都不小于 nums[j]。
partition(切分)操作总能排定一个元素,还能够知道这个元素它最终所在的位置,这样每经过一次 partition(切分)操作就能缩小搜索的范围,这样的思想叫做 “减而治之”(是 “分而治之” 思想的特例)。
int findKthLargest(vector<int> &nums,int k){
int len=nums.size();
int left=0;int right=len-1;
int target=len-k;//第K大
while(1){
int index=partition(nums,left,right);
if(index==target) return nums[index];
else if(index<target) left=index+1;
else right=index-1;
}
}
//&非常重要!!!
int partition(vector<int>& nums,int left,int right){
int pivot=nums[left];
int j=left;
for(int i=left+1;i<=right;i++){
if(nums[i]<pivot){
j++;
swap(nums,i,j);
}
}
//满足了
swap(nums,j,left);
return j;
}
优先队列的思路是很朴素的。因为第 K 大元素,其实就是整个数组排序以后后半部分最小的那个元素。因此,我们可以维护一个有 K 个元素的最小堆:
1、如果当前堆不满,直接添加;
2、堆满的时候,如果新读到的数小于等于堆顶,肯定不是我们要找的元素,只有新都到的数大于堆顶的时候,才将堆顶拿出,然后放入新读到的数,进而让堆自己去调整内部结构。
假设数组有 len 个元素。
思路1:把 len 个元素都放入一个最小堆中,然后再 pop() 出 len - k 个元素,此时最小堆只剩下 k 个元素,堆顶元素就是数组中的第 k 个最大元素。
思路2:把 len 个元素都放入一个最大堆中,然后再 pop() 出 k - 1 个元素,因为前 k - 1 大的元素都被弹出了,此时最大堆的堆顶元素就是数组中的第 k 个最大元素。
搞懂「建堆」、「调整」和「删除」的过程
void maxHeapify(vector<int>& a,int i,int heapSize){
int l=i*2+1;
//0 1 2
while(l<heapSize){
if(l+1<heapSize&&a[l+1]>a[l]) l=l+1;
if(a[l]>a[i]){
swap(a[l],a[i]);
i=l;l=i*2+1;
}else break;
}
}
void buildMaxHeap(vector<int>& a,int heapSize){
for(int i=heapSize/2;i>=0;i--)
maxHeapify(a,i,heapSize);
}
int findKthLargest(vector<int> &nums,int k){
int heapSize=nums.size();
buildMaxHeap(nums,heapSize);
for(int i=nums.size()-1;i>=nums.size()-k+1;i--){
swap(nums[0],nums[i]);
--heapSize;
maxHeapify(nums,0,heapSize);
}
return nums[0];
}
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
// if(k>nums.size()) return
priority_queue<int,vector<int>,less<int>> q;
for(int i=0;i<nums.size();i++) q.push(nums[i]);
for(int i=0;i<k-1;i++) q.pop();
return q.top();
}
};
难度中等505收藏分享切换为英文关注反馈
在一个由 0 和 1 组成的二维矩阵内,找到只包含 1 的最大正方形,并返回其面积。
示例:
输入: 1 0 1 0 0 1 0 1 1 1 1 1 1 1 1 1 0 0 1 0 输出: 4
class Solution {
public:
int maximalSquare(vector<vector<char>>& matrix) {
if(matrix.size()==0) return 0;
// 动态规划
int n=matrix.size();int m=matrix[0].size();
vector<vector<int>> dp(n,vector<int>(m));
int maxn=0;
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(i==0||j==0) dp[i][j]=matrix[i][j]-'0';
else if(matrix[i][j]=='1')
dp[i][j]=min(dp[i-1][j-1],min(dp[i][j-1],dp[i-1][j]))+1;
if(dp[i][j]>maxn) maxn=dp[i][j];
}
}
return maxn*maxn;
}
};
难度简单531
翻转一棵二叉树。
示例:
输入:
4 / \ 2 7 / \ / \ 1 3 6 9
输出:
4 / \ 7 2 / \ / \ 9 6 3 1
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
TreeNode right = invertTree(root.right);
TreeNode left = invertTree(root.left);
root.left = right;
root.right = left;
return root;
}
这道题的核心在于遍历节点,只要能接触到每一个节点,就能反转它的左右孩子,至于遍历方式反而不重要了,先序后序中序层次都一样,都可以用。
难度简单595收藏分享切换为英文关注反馈
请判断一个链表是否为回文链表。
示例 1:
输入: 1->2 输出: false
示例 2:
输入: 1->2->2->1 输出: true
进阶:
你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
快慢指针,然后翻转一半链表即可。
class Solution {
public:
bool isPalindrome(ListNode* head) {
if(!head || !head->next)
return 1;
ListNode *fast = head, *slow = head;
ListNode *p, *pre = NULL;
while(fast && fast->next){
p = slow;
slow = slow->next; //快慢遍历
fast = fast->next->next;
p->next = pre; //翻转
pre = p;
}
if(fast) //奇数个节点时跳过中间节点
slow = slow->next;
while(p){ //前半部分和后半部分比较
if(p->val != slow->val)
return 0;
p = p->next;
slow = slow->next;
}
return 1;
}
};
难度中等687收藏分享切换为英文关注反馈
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
示例 1:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1 输出: 3 解释: 节点5
和节点1
的最近公共祖先是节点3。
示例 2:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4 输出: 5 解释: 节点5
和节点4
的最近公共祖先是节点5。
因为根据定义最近公共祖先节点可以为节点本身。
说明:
- 所有节点的值都是唯一的。
- p、q 为不同节点且均存在于给定的二叉树中。
最近公共祖先的定义: 设节点 root为节点 p,q的某公共祖先,若其左子节点 root.left和右子节点 root.right都不是 p,q 的公共祖先,则称 root 是 “最近的公共祖先” 。
根据以上定义,若 root 是 p,q 的 最近公共祖先 ,则只可能为以下情况之一:
p 和 q 在 root的子树中,且分列 root的 异侧(即分别在左、右子树中);
p=root ,且 q 在 root 的左或右子树中;
q=root,且 p 在 root 的左或右子树中;
函数:
- 如果 ppp 和 qqq 都存在,则返回它们的公共祖先;
- 如果只存在一个,则返回存在的一个;
- 如果 ppp 和 qqq 都不存在,则返回NULL
具体思路:
(1) 如果当前结点 root 等于 NULL,则直接返回 NULL
(2) 如果 root 等于 p or q ,那这棵树一定返回 p q
(3) 然后递归左右子树,因为是递归,使用函数后可认为左右子树已经算出结果,用 left和 right表示
(4) 此时若left为空,那最终结果只要看 right;若 right为空,那最终结果只要看 left
(5) 如果 left right 都非空,说明一边一个,因此 root 是他们的最近公共祖先
(6) 如果 left right 都为空,则返回空(其实已经包含在前面的情况中了)
时间复杂度是 O(n):每个结点最多遍历一次,空间复杂度是 O(n):需要系统栈空间
作者:Wilson79
链接:https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/solution/c-jing-dian-di-gui-si-lu-fei-chang-hao-li-jie-shi-/
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(root == NULL)
return NULL;
if(root == p || root == q)
return root;
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if(left == NULL)
return right;
if(right == NULL)
return left;
if(left && right) // p和q在两侧
return root;
return NULL; // 必须有返回值
}
};
class Solution {
public:
TreeNode* ans;
bool dfs(TreeNode* root, TreeNode* p, TreeNode* q) {
if (root == nullptr) return false;
bool lson = dfs(root->left, p, q);
bool rson = dfs(root->right, p, q);
if ((lson && rson) || ((root->val == p->val || root->val == q->val) && (lson || rson))) {
ans = root;
}
return lson || rson || (root->val == p->val || root->val == q->val);
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
dfs(root, p, q);
return ans;
}
};
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/solution/er-cha-shu-de-zui-jin-gong-gong-zu-xian-by-leetc-2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
难度中等545收藏分享切换为英文关注反馈
给你一个长度为 n 的整数数组 nums
,其中 n > 1,返回输出数组 output
,其中 output[i]
等于 nums
中除 nums[i]
之外其余各元素的乘积。
示例:
输入:[1,2,3,4]
输出:[24,12,8,6]
提示:题目数据保证数组之中任意元素的全部前缀元素和后缀(甚至是整个数组)的乘积都在 32 位整数范围内。
说明: 请不要使用除法,且在 O(n) 时间复杂度内完成此题。
进阶:
你可以在常数空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组不被视为额外空间。)
result[i] = left_i*right_i
利用动态规划分别求出left_i和right_i即可,O(1)空间实现也非常简单,只需要在结果数组中计算出left_i,布置一个right_i的临时变量,遍历两次即可求解。
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
vector<int> res(nums.size(), 1);
int left = 1, right = 1;
for (int i = 0, j = nums.size() - 1; i < nums.size(); i ++, j --) {
res[i] *= left; res[j] *= right;
left *= nums[i]; right *= nums[j];
}
return res;
}
};
作者:OrangeMan
链接:https://leetcode-cn.com/problems/product-of-array-except-self/solution/cjian-ji-dai-ma-shuang-zhi-zhen-6xing-dai-ma-by-or/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
难度中等395收藏分享切换为英文关注反馈
编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target。该矩阵具有以下特性:
- 每行的元素从左到右升序排列。
- 每列的元素从上到下升序排列。
示例:
现有矩阵 matrix 如下:
[ [1, 4, 7, 11, 15], [2, 5, 8, 12, 19], [3, 6, 9, 16, 22], [10, 13, 14, 17, 24], [18, 21, 23, 26, 30] ]
给定 target = 5
,返回 true
。
给定 target = 20
,返回 false
。
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int i=matrix.size()-1,j=0;
while(i>=0&&j<matrix[0].size())
if(matrix[i][j]==target) return true;
else if(matrix[i][j]>target) i--;
else j++;
return false;
}
};