Authur Whywait 做一块努力吸收知识的海绵
想看博主的所有leetcode卡片学习笔记?传送门点这儿
- 不得不说,这篇学习笔记博客,应该是目前为止花费时间最多的一篇。
- 做题的时候走了很多弯路,也在不断碰壁中学到了很多。
- 有把做题中的一些历程记录下来,作为分享。希望能对正在看的你,有所帮助。
- 如果有不足或者错误之处,欢迎指出;如果对你有帮助,请不要吝啬你的赞~
一大波文字即将来临~
你做好准备了吗~
二叉搜索树卡片小结
二叉搜索树简介 - 小结
我们已经学习了二叉搜索树的相关特性,以及如何在二叉搜索树中实现一些基本操作,比如搜索、插入和删除。
二叉搜索树的优点是,即便在最坏的情况下,也允许你在O(h)
的时间复杂度内执行所有的搜索、插入、删除操作。
通常来说,如果你想 有序地存储数据 或者需要 同时执行搜索、插入、删除 等多步操作,二叉搜索树这个数据结构是一个很好的选择。
一个例子
问题描述:设计一个类,求一个数据流中第k大的数。
一个很显而易见的解法是,先将数组降序排列好,然后返回数组中第k个数。
但这个解法的缺点在于,为了在O(1)时间内执行搜索操作,每次插入一个新值都需要重新排列元素的位置。从而使得插入操作的解法平均时间复杂度变为O(N)。因此,算法总时间复杂度会变为O(N^2)。
鉴于我们同时需要插入和搜索操作,为什么不考虑使用一个二叉搜索树结构存储数据呢?
分析如下:
我们知道,对于二叉搜索树的每个节点来说,它的左子树上所有结点的值均小于它的根结点的值,右子树上所有结点的值均大于它的根结点的值。
换句话说,对于二叉搜索树的每个节点来说,若其左子树共有m个节点,那么该节点是组成二叉搜索树的有序数组中第m + 1个值。
为了方便,我们还需要在每个节点中放置一个计数器,以计算以此节点为根的子树中有多少个节点。
数据流中的第K大元素🚩
设计一个找到数据流中第K大元素的类(class)。注意是排序后的第K大元素,不是第K个不同的元素。
你的 KthLargest
类需要一个同时接收整数 k
和整数数组nums
的构造器,它包含数据流中的初始元素。每次调用 KthLKargest.add
,返回当前数据流中第K大的元素。
分析
我们需要填补的代码部分如下:
typedef struct {
} KthLargest;
KthLargest* kthLargestCreate(int k, int* nums, int numsSize) {
}
int kthLargestAdd(KthLargest* obj, int val) {
}
void kthLargestFree(KthLargest* obj) {
}
简直是疑惑行为大赏。
kthLargestAdd
函数,传入的参数都没有k,怎么就输出了第k大的元素?
我真的是一脸懵逼。
然后到了第二天,早上醒来睁开眼的一瞬间,我突然悟了!
我们最开始使用数据流中的初始元素构造一个构造器(搜索二叉树)的时候,就应该把根节点的位置作为第k大的位置。什么意思呢,就是根节点的右子树中一共有k-1个Tree Node,这样子以来不就可以保证根节点的位置是第k大的了嘛。
至于后面的插入操作,我们只需要和根节点比较,如果大于root,那么根节点就变为根节点的右子树的根节点;如果小于等于,仍未原来的root。
历程
分析在后面,此代码为错误范例,仅作记录。目的是警醒 自己以及与我一般的小伙伴,历程中的代码仅作为警示。可略过~
前方错误代码来袭!
int kthLargestAdd(KthLargest* obj, int val) {
KthLargest* temp = obj, * father = NULL;
while(temp){
father = temp;
temp = val < temp->val ? temp->left : temp->right;
}
temp = (KthLargest *)malloc(sizeof(KthLargest));
temp->val = val;
temp->left = NULL;
temp->right = NULL;
if(!father) obj = temp;
else if(father && father->val > val) father->left = temp;
else father->right = temp;
if(val >= obj->val){
KthLargest * cur = obj->right, * curLeft = cur;
while(curLeft->left) curLeft = curLeft->left;
obj->right = NULL;
curLeft->left = obj;
obj = cur;
}
return obj->val;
}
刷刷刷,三下五除二,具体思路就是类似删除二叉搜索树中的结点类似。传送门点这儿
具体思想就是具体思想就是把根节点以及其子树作为一个整体,一起挪到右子树里头(我们暂且先不讨论这个思路的对错)。但是要着重讨论的一点就是,尽管我在函数内更新了根节点(更新为原来的右子树的根节点),但是每次重新调用上述封装好的函数时,根节点的地址并没有改变。
为什么呢?
敲黑板!当当当!(我这黑板材质跟个钟似的···)重点来了!
**如果函数参数直接传递的是指针类型,那么在函数内改变指针指向,并不能影响函数外的指针实例。**只有传入指针的指针,才能改变指针的指向。
函数里的形参是一般变量,在函数里面改变变量的值,不会改变主函数里实参的值。指针就是地址变量(这是我们应该了解的关于指针的最基础知识),在函数里改变地址变量的值时,不会改变主函数实参地址变量的值。只有在函数里改变指针所指向的变量的值时 主函数实参指针所指向的变量的值才会改变。
想来,如果我们自己写程序,想传啥类型的参数就传啥类型的参数,但是题目规定了你的传入参数以及返回值类型···
人在题海飘,该低头时得低头,不低头就掉头,顿时脖颈凉飕飕。
于是,上述思路自然是行不通了。(值得一提的是,你有没有发现我的代码长得和以前的代码很像?一个类型的题目,写多了就要总结一套模板出来,这样子速度就会up up up!)
话说回来,我竟然会犯上面这种低级错误,我真的是可以脖颈凉飕飕了。
那我们应该怎么去处理这道题呢?
下面给出几种方法。
辅助数组排序法
把数据流丢到一个最长为k的数组里头,排序。
如果新来的数大于数组里头的最小元素,就替换最小元素,然后排序,找到当前数组的最小元素输出;否则直接返回数组最小元素。
优点:简单易懂,超时界杠杠的存在。
不是超越时空,是超出时间限制。(想啥呢小火汁)
代码丢在下面,如果想学习的话可以观看一下(里面也没啥内容)。如果着急的话直接跳到下一个方法。
typedef struct node{
int nums[100000];
int numsSize, trueNumsSize;
} KthLargest;
int comp(const void * a, const void * b){
return *(int *)b - *(int *)a;
}
KthLargest* kthLargestCreate(int k, int* nums, int numsSize) {
KthLargest* obj = (KthLargest *)malloc(sizeof(KthLargest));
obj->nums[0] = 0;
(obj->numsSize) = k;
(obj->trueNumsSize) = k < numsSize ? k : numsSize;
if(!numsSize) return obj;
qsort(nums, numsSize, sizeof(int), comp);
for(int i=0; i<k; i++) obj->nums[i] = i<numsSize ? nums[i] : 0;
return obj;
}
int kthLargestAdd(KthLargest* obj, int val) {
if(obj && obj->trueNumsSize<obj->numsSize) obj->nums[(obj->trueNumsSize)++] = val;
obj->nums[(obj->trueNumsSize)-1] = val > obj->nums[(obj->trueNumsSize)-1] ? val : obj->nums[(obj->trueNumsSize)-1];
qsort(obj->nums, obj->trueNumsSize, sizeof(int), comp);
return obj->nums[(obj->trueNumsSize)-1];
}
void kthLargestFree(KthLargest* obj) {
free(obj);
}
话说,在进行上面这种方法的代码实现时,我意识到:我之前认为的无法使用二叉搜索树(因为k并没有作为参数传递到add函数中所以我们怎么返回第K大元素)是不对的。因为我们可以把k直接丢在结构体里面呀!于是下面,我将用二叉搜索树实现此道题。(真是脑子锈了)
二叉搜索树法
typedef struct node{
int val, cnt, k;
bool flag;
struct node* left, *right;
} KthLargest;
//flag: whether there is elements in obj (the same as numsSize=0)
//cnt: counting
//k: store the k in the struct node
KthLargest* kthLargestCreate(int k, int* nums, int numsSize) {
KthLargest* obj = (KthLargest *)malloc(sizeof(KthLargest));
obj->flag = false;
obj->left = NULL;
obj->right = NULL;
if(!numsSize) return obj;
//if put this sentense in the front, the pointer obj will always be NULL. flag=false means the nums=NULL
obj->flag = true;
obj->val = nums[0];
obj->cnt = 1;
obj->k = k;
//creat the BST
for(int i=1; i<numsSize; i++){
KthLargest * cur = obj, * father = NULL;
while(cur){
cur->cnt++;
father = cur;
cur = nums[i] < cur->val ? cur->left : cur->right;
}
if(nums[i] < father->val){
father->left = (KthLargest *)malloc(sizeof(KthLargest));
cur = father->left;
}
else{
father->right = (KthLargest *)malloc(sizeof(KthLargest));
cur = father->right;
}
cur->val = nums[i];
cur->cnt = 1;
cur->k = k;
cur->left = NULL;
cur->right = NULL;
}
return obj;
}
int kthLargestAdd(KthLargest* obj, int val) {
if(!obj->flag){ //if flag=false, means there is no element in former array nums
obj->val = val;
obj->cnt = 1;
obj->k = 1;
obj->flag = true;
}
else{ //Add the val into the BST as a leaf
KthLargest * cur = obj, * father = NULL;
while(cur){
cur->cnt++;
father = cur;
cur = val < cur->val ? cur->left : cur->right;
}
if(val < father->val){
father->left = (KthLargest *)malloc(sizeof(KthLargest));
cur = father->left;
}
else{
father->right = (KthLargest *)malloc(sizeof(KthLargest));
cur = father->right;
}
cur->val = val;
cur->cnt = 1;
cur->k = obj->k;
cur->left = NULL;
cur->right = NULL;
}
//Now, start seaching
KthLargest* temp = obj;
int k = obj->k;
while(k){
if(!temp->right){ //case 1
if(k==1) return temp->val;
else{
temp = temp->left;
k--;
}
}
else if(temp->right && k > temp->right->cnt){ //case 2
k -= temp->right->cnt;
if(k==1) return temp->val;
else{
temp = temp->left;
k--;
}
}
else temp = temp->right; //case 3
}
return 0;
}
void kthLargestFree(KthLargest* obj) { // the same way as we creat - I think the postorder-search is better!
if(!obj) return;
kthLargestFree(obj->left);
kthLargestFree(obj->right);
free(obj);
}
相关
结构体的释放:遍历释放(树),其他请点击传送门。
二叉搜索树的最近公共祖先🚩
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出: 6
回顾
回想我们之前遇到过的题目《二叉树的最近公共祖先》,此题似乎和那题并无太大区别——仅仅是二叉搜索树变成了二叉树?
既然二叉搜索树包含在二叉树之中,我们自然可以用之前题目一摸一样的程序提交,自然是可以A的。
把之前的代码再贴一下吧,然后附上之前代码在此题中的执行结果。
struct TreeNode* lowestCommonAncestor(struct TreeNode* root, struct TreeNode* p, struct TreeNode* q) {
if(!root || root==q || root==p) return root;
struct TreeNode* left = lowestCommonAncestor(root->left, p, q);
struct TreeNode* right = lowestCommonAncestor(root->right, p, q);
if(!left && !right) return NULL;
else if(left && !right) return left;
else if(!left && right) return right;
else return root;
}
分析
如果我们在做二叉搜索树情况下的最近公共祖先,没有使用二叉搜索树本身的性质,未免有些太可惜了。
用到了什么性质呢?
自然是二叉搜索树的性质。详情点击传送门,你将系统了解二叉搜索树的性质,以及相关操作介绍。
我们回来看一下二叉搜索树的性质与此题如何结合呢?
最近祖先,p节点和q节点,一个在左子树中一个在右子树中,或者其中之一在另一个的子树中。
由二叉搜索树的性质,如上这般的第一种情况,就是两个节点的val值一个节点val值的两侧(即一个大于一个小与)。
我们只要找到那个节点,然后返回就好了。
至于第二种情况(其中之一在另一个的子树中),作为特例判断即可。
仍旧使用的递归法。
代码以及执行结果贴在下面。
代码实现以及执行结果
struct TreeNode* lowestCommonAncestor(struct TreeNode* root, struct TreeNode* p, struct TreeNode* q) {
if(!root || root==q || root==p) return root;
if(p->val<root->val && q->val<root->val) return lowestCommonAncestor(root->left, p, q);
else if(p->val>root->val && q->val>root->val) return lowestCommonAncestor(root->right, p, q);
else return root;
}
比较
两种方法,利用了二叉搜索树相较于其他二叉树不同的性质,代码更加简短。
存在重复元素Ⅲ🚩
给定一个整数数组,判断数组中是否有两个不同的索引 i 和 j,使得 nums [i] 和 nums [j] 的差的绝对值不超过 t,并且 i 和 j 之间的差的绝对值不超过 ķ。
输入: nums = [1,2,3,1], k = 3, t = 0
输出: true
输入: nums = [1,0,1,1], k = 1, t = 2
输出: true
输入: nums = [1,5,9,1,5,9], k = 2, t = 3
输出: false
【吐槽】
这一题槽点颇多:
- 题目就晦涩难懂(其实是翻译不太行),上面的题目是我略微修改之后的。
- 测试用例比较牛逼
出现了nums = [-1,2147483647],k = 1, t = 2147483647,这种测试用例
以至于需要强制转换类型。(这谁又能想到呢?)
【说明】
至于普通的暴力线性求解,就不介绍了,与本文的主题——二叉搜索树不符。
下面解题将使用滑动窗口和二叉搜索树相结合的方法。
“滑动窗口+二叉搜索树”法
代码实现及执行结果
Part 1:结构体定义
typedef struct BSTtreenode{
int val;
struct BSTtreenode * left, * right;
} BSTnode;
Part 2:函数 - 删除二叉搜索树指定结点(详情请点传送门)
BSTnode* deleteKey(BSTnode* obj, int key){
if(!obj) return obj;
if(key > obj->val) obj->right = deleteKey(obj->right, key);
else if(key < obj->val) obj->left = deleteKey(obj->left, key);
else{
if(!obj->right) return obj->left;
else{
BSTnode* cur = obj->right, * pre = NULL;
while(cur->left){
pre = cur;
cur = cur->left;
}
if(pre) pre->left = NULL;
cur->left = obj->left;
if(pre){
BSTnode* rightCur = cur;
while(rightCur->right) rightCur = rightCur->right;
rightCur->right = obj->right;
}
return cur;
}
}
return obj;
}
Part 3:函数 - 往二叉搜索树中添加指定大小的结点(详情请点传送门)
BSTnode* addKey(BSTnode* obj, int key){
if(!obj){
obj = (BSTnode *)malloc(sizeof(BSTnode));
obj->val = key;
obj->left = NULL;
obj->right = NULL;
}
BSTnode* father = NULL, * cur = obj;
while(cur){
father = cur;
cur = key<cur->val ? cur->left : cur->right;
}
cur = (BSTnode *)malloc(sizeof(BSTnode));
cur->val = key;
cur->left = NULL;
cur->right = NULL;
if(key<father->val) father->left = cur;
else father->right = cur;
return obj;
}
Part 4:函数 - 中序遍历二叉搜索树,查找满足条件数对(详情请点击传送门)
void inorder(BSTnode* obj, bool* flag, int* nums, int* index, int t, int k){
if(!obj || (*flag) || (*index)==k) return;
inorder(obj->left, flag, nums, index, t, k);
nums[*index] = obj->val;
if(*index && ((long)nums[*index]-(long)nums[(*index)-1])<=t) *flag = 1;
(*index)++;
inorder(obj->right, flag, nums, index, t, k);
}
强制转换类型就藏在 Part 4 里面哟
Part 5: 函数主体
bool containsNearbyAlmostDuplicate(int* nums, int numsSize, int k, int t){
if(!numsSize || k==10000) return 0;
BSTnode* obj = (BSTnode *)malloc(sizeof(BSTnode));
obj->val = nums[0];
obj->left = NULL;
obj->right = NULL;
for(int i=1; i<(k+1<numsSize?k+1:numsSize); i++) addKey(obj, nums[i]);
int left = 0, right = (k+1<numsSize?k+1:numsSize);
while(right<numsSize+1){
bool *flag = (bool *)malloc(sizeof(bool));
int* index = (int *)malloc(sizeof(int));
*index = 0; *flag = 0;
int* nums_temp = (int *)malloc(sizeof(int) * (k+1<numsSize?k+1:numsSize));
inorder(obj, flag, nums_temp, index, t, (k+1<numsSize?k+1:numsSize));
if(*flag) return 1;
obj = deleteKey(obj, nums[left++]);
if(right<numsSize) obj = addKey(obj, nums[right++]);
else return 0;
}
return 0;
}
执行结果
因为开始用
VScode
写题目,所以有时候执行结果就在VScode
里面截图咯~
说明
仔细看上述代码肯定会发现,为什么会出现如果k为10000就返回false呢?
因为最后一个测试用例中k为10000,而此法超时。
观题解,需要使用 自平衡二叉搜索树 。
在之后的博文中将介绍二叉搜索树的内容,于是此处便只是使用二叉搜索树而非自平衡二叉搜索树。
都看到这里了,确定不点个赞再走?┏ (゜ω゜)=☞