leetcode【数据结构简介】《二叉搜索树》卡片 - 小结

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

【吐槽】
这一题槽点颇多:

  1. 题目就晦涩难懂(其实是翻译不太行),上面的题目是我略微修改之后的。
  2. 测试用例比较牛逼
    出现了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,而此法超时。
观题解,需要使用 自平衡二叉搜索树

在之后的博文中将介绍二叉搜索树的内容,于是此处便只是使用二叉搜索树而非自平衡二叉搜索树。

都看到这里了,确定不点个赞再走?┏ (゜ω゜)=☞

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AuthurLEE

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值