欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

leetcode 二分查找和二叉查找树

程序员文章站 2022-06-05 17:33:53
...

1.二分查找预备知识

二分查找

给定一个数字target,在有序序列中查找
二分查找的前提就是序列是有序的,使用分治的思想,逐渐缩小查找区间,使得查找时间复杂度为O(logn)

思路

使用begin ,end 两个指针分别指向数组的开始和结尾,mid指向数组中间元素,如果待查找的元素target==arr[mid],查找到元素直接返回,,则target>arr[mid] 则说明target在【mid+1,end】,否则target在【begin,mid-1】之间

通过迭代不断的缩小查找范围,当缩小到begin>end的时候则表示没有查找到

代码(递归版本)
bool binary_search(vector<int>&arr,int &target,int begin,int end)
{
   if(begin>end)       //查找区间已经不合法了,则表示没有查找到
   return false;
   
   int mid = (begin+end)/2;
   if(target==arr[mid])
       return true;
   if(target>arr[mid])   //taget大于中间值,则target应该在【mid+1,end】之间,所以修改begin=mid+1
       begin=mid+1;
   else
       end = mid-1;    //taget小于中间值,则target应该在【begin,mid-1】之间,所以修改end = mid-1
    
   //缩小查找范围后递归查找    
   return binary_search(arr,target,begin,end);     
}

//单纯具有分治思想的递归程序适合用while来改写,能够提高效率,但是对于有回溯思想的不适合用while的方式查找

代码(非递归版本)
bool binary_search(vector<int>&arr,int &target)
{
   bool found = false;
   int mid;

   while(begin<end)        //当区间合法时则循环查找
   {
        int mid = (begin+end)/2;
       if(target==arr[mid])
       {
           found = true;
           break;    
       } 
       if(target>arr[mid])   //taget大于中间值,则target应该在【mid+1,end】之间,所以修改begin=mid+1
       {
           begin=mid+1;
       }
       else
       {
           end = mid-1;    //taget小于中间值,则target应该在【begin,mid-1】之间,所以修改end = mid-1
       } 
   }
    
   return found; 
} 

2.搜索插入位置

题目 (Leetcode35)

给定一个排序数组(无重复元素)和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置
示例:
2不在【1,3,5,6】中,返回插入位置1
3在【1,3,5,6】中,返回实际位置1

leetcode 二分查找和二叉查找树

思路

因为给定数组是一个排序数组—>有序数组,且没有重复元素,正好适合二分查找的应用场景,只不过查找之后返回值不再是true和false,而是查找到位置下标,如果没有查找到则返回要插入的位置:
现在分析没有查找到情况(也就是target位于两个元素之间),分析的时候我们以mid作为参考:

  • 如果过target<arr[mid],则说明在左边,但是此时如果我们调到左边也有可能找不到target,因此我们在判断target<arr[mid]的时候,还要判断target和mid的前一个值的大小关系,如果 target>arr[mid-1],那就说明target不在数组中,但是可以确定他的范围就是在这【mid-1,mid】之间,这样就可以同时确定target不存在,并找到它应该插入的索引mid
  • 其中细节需要注意的是由于target>arr[mid-1]要比较,如果mid0,可能会越界,例如查找-2时,找到最后mid0了,begin==0,这时候已经说明找不到,而且插入位置,就应该mid等于0的位置,需要特殊考虑
  • 总结一下规律如下:
    leetcode 二分查找和二叉查找树
代码
class Solution {
public:
   int searchInsert(vector<int>& nums, int target) {
       //设定开始搜素区间
       int begin=0;
       int end = nums.size()-1;
       int mid; 
       int index =-1;          //表示还没有确定索引值 
       while(index==-1)        
       {
           mid = (begin+end)/2;
           if(nums[mid]==target)
           {
               index = mid;
           }
           else if(target<nums[mid])   //target比mid小,则要往前查找,但是要判断target是否在【mid-1,mid】之间
           {
               if((mid==0) ||(target>nums[mid-1]))   //如果target在[mid-1,mid]之间,则说明不在数组中,修改索引准备返回,mid==0要提前判断,作为边界条件
                   index =mid;                         //修改了index下一次循环就退出了
               end = mid-1;              
               
           }
           else       //target>nums[mid]
           {
               if((mid==nums.size()-1)||(target<nums[mid+1]))  //target在【mid,mid+1】之间,不在数组中,修改index
                   index = mid+1;
               begin =mid+1;
           } 
       } 
       return index; 
   }
};


3.区间查找

题目 (Leetcode34)

给定一个按照升序排列的整数数组 nums(可能存在重复元素),和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。你的算法时间复杂度必须是 O(log n) 级别。如果数组中不存在目标值,返回 [-1, -1]。
例如:
leetcode 二分查找和二叉查找树

思路

分析:
因为数组中可能存在重复元素,例如查找元素8的时候,通过二分查找,可能查找其他某一个8,确不能确定是第一个8还是第二个8,所有单纯的二分查找不能完成。

  • 因为要查找一个去区间,区间包含左右两个端点,那能不能通过限制条件分别求出左右端点呢?
  • 以左端点为例:区间左端点具有的性质,首先要查找到这个元素,即target==arr[mid],同时由于是左端点,那么arr[mid-1]应该不等于target才可以,更确切的说arr[mid-1]<target,则可以说明是区间的左端点
  • 同样由于要访问arr[mid-1],要提前判断mid是否为0
    leetcode 二分查找和二叉查找树
  • 查找左端点条件 :targetarr[mid] 的情况下(mid0 || target>arr[mid-1]),则说明找到左边界,返回,否则按照二分查找的思路缩小查找返回
  • 查找右端点同理
代码
class Solution {
public:
   vector<int> searchRange(vector<int>& nums, int target) {
    
       vector<int>bound;
       bound.push_back(left_bound(nums,target));
       bound.push_back(right_bound(nums,target));
       return bound;
   }
private:
   int left_bound(vector<int>&nums,int target)
   {
       int  begin =0;
       int  end = nums.size()-1;
       int  mid;
       while(begin<=end)
       {
           //搜仙判断是否为左边界
           mid = (begin+end)/2;
           if(target==nums[mid])
           {
               if(mid==0 || nums[mid-1]<target)        //找到左边界,返回
                   return mid;
               //如果mid-1,也等于target,说明不是最左边那个target,还得向左走
               end  =mid-1;        //向左搜素
           }
           else if(target<nums[mid])
                   end = mid-1;
           else
                   begin = mid+1; 
       } 
       return -1;//未搜素到
   }
   
   
      int right_bound(vector<int>&nums,int target)
   {
       int  begin =0;
       int  end = nums.size()-1;
       int  mid;
       while(begin<=end)
       {
           //判断是否为左边界
           mid = (begin+end)/2;
           if(target==nums[mid])
           {
               if(mid==(nums.size()-1) || nums[mid+1]>target)        //找到右边界,返回
                   return mid;
               //如果mid+1,也等于target,说明不是最右边那个target,还得向右边
               begin  =mid+1;        //向右搜素
           }
           else if(target<nums[mid])
                   end = mid-1;
           else
                   begin = mid+1; 
       } 
       return -1;//未搜索到
   }
   
};`

4.旋转数组查找

题目 (Leetcode33)

假设按照升序排序的数组(不存在重复的元素)在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
leetcode 二分查找和二叉查找树

思路

旋转数组的特点:

  • 是由一个有序数组循环移动得到,移动之后不再是一个有序数组
  • 但却形成了两个有序的数组,如下图中红色线标记部分和未标记部分
    leetcode 二分查找和二叉查找树
    如果依然采用二分查找的思路的话,由于数组总体不是有序的,则可能会出现错误

leetcode 二分查找和二叉查找树
这里查找12的时候二分落到了有序区间【7,15】,循环查找可以找到它
但是这里3的时候二分落到了无序区间【20,6】,循环查找也找不到

考虑到二分查找只能在有序区间中查找,那我们能不能通过某些限定条件,来区分哪一部分是有序的,哪一部分是无序的呢?

  • 因为旋转数组是由两个递增数组组成,第二个递增数组所有值都小于第一个递增数组,所以只要mid<high,那说明mid和high一定落入到递增区间中,这个是有序区间

  • 找到了有序区间中判断我们查找的target ,是否真的在【mid+1,high】区间,如果目标在这个区间,则缩小查找范围lo=mid+1,否则转到无序数组,进行下一轮循环。

leetcode 二分查找和二叉查找树

  • 因为旋转数组是由两个递增数组组成,第二个递增数组所有值都小于第一个递增数组,所以只要mid>=high,那说明[low,mid-1]是递增区间,这个是有序区间
  • 找到了有序区间中判断我们查找的target ,是否真的在【low,mid-1】区间,如果目标在这个区间,则缩小查找范围high=mid-1,否则转到无序数组,进行下一轮循环。
    leetcode 二分查找和二叉查找树
代码
class Solution{
   
   public:
   int search(vector<int>& nums, int target) {
       if(nums.empty())
           return -1;
       int lo = 0, hi = nums.size() - 1;
       int mid;
       while(lo <= hi)
       {
           mid = (lo + hi) / 2;
           if(nums[mid] == target)
               return mid;
          
           if(nums[mid] < nums[hi])   //mid<high 说明在mid和high之间是有序序列(因为旋转数组是由两个递增数组组成,第二个递增数组所有值都小于第一个递增数组,所以只要mid<high,那说明mid和high一定落入到递增区间中)
           {//在这个有序区间中我们去二分查找我们的目标数据
                 
               if(nums[mid] < target && target <= nums[hi])//有序序列中查找
                   lo = mid + 1;       //缩小有序序列查找范围
               else
                   hi = mid - 1;    //转到剩下的无序数组
           } 
           
           else   //mid>high,则说明mid和high之间是无序序列,所以(low,mid)之间是有序序列
           {
               //在这个有序区间中我们去二分查找我们的目标数据
               if(nums[lo] <= target && target < nums[mid])
                   hi = mid - 1;       //缩小有序数组的查找范围
               else
                   lo = mid + 1;   //转到剩下的无序数组
           }                 
       }
       return -1;
   }
   
};

6.二叉搜索树

题目 (二叉搜素树基础)

二叉搜索树的性质:

  • 是一个完全二叉树
  • 左子树中的值都比根节点小,右子树的值都比根节点大
  • 最小值在左子树的最左节点,最大值在右子树的最右节点
  • 二叉树的中序遍历结果是递增的有序序列

二叉搜索树插入

bool BST_Find(TreeNode*root,int val)
{
    if(root==nullptr)
    return false;
    
    if(val==root->val)
        return true;
    //根据值的大小递归插入左子树、右子树
   if(val<root->val)   //小于当前值,在左子树中查找
        return BST_Find(root->left,val);
   
   else (val>root->val))   //大于当前值,在右子树中查找
        return BST_Find(root->right,val); 
}

二叉搜索树插入

void BST_Insert(TreeNode*root,TreeNode*insert_node)
{
    if(root==nullptr)
    {
        root=insert_node;   //遇到空节点的时候插入节点,并返回
        return;
    }
    
    //根据值的大小递归插入左子树、右子树
    if(insert_node->val<root->val)   //小于当前值,插入左子树
         BST_Insert(root->left,insert_node);
    
    else if(insert_node->val > root->val)   //大于当前值,插入右子树
        BST_Insert(root->right,insert_node); 
    //else {} 相等则不用插入
    }
    
}

5.二叉树的编码和解码

题目 (Leetcode199)

序列化是将数据结构或对象转换为一系列位的过程,以便它可以存储在文件或内存缓冲区中,或通过网络连接链路传输,以便稍后在同一个或另一个计算机环境中重建。
leetcode 二分查找和二叉查找树

思路

在序列化的时候我们使用先序遍历,在每个空的节点位置用124来填充,如下图我们可以得到序列化后的数组为:124$ $ 3 5 6 6

在反序列化的过程中,
先考虑如果左右子树都已经构建好了,对于当前节点应该怎么处理
:如果该节点为有效数字应该将左右子树连接起来,如果该节点为“$”则说明传入的是叶子节点的空指针,不用处理,作为递归返回的条件

leetcode 二分查找和二叉查找树

这里的举例是用的个位数表示的节点,实际中节点中的数字可能为多位数,比如最后一个节点数据变成67后,如果不加区分,不知道是一个节点的值为67,还是两个节点,一个为6,一个为7,因此为了容易区分这里加入逗号分隔符号
leetcode 二分查找和二叉查找树

  • 辅助代码:需要一个int2string和string2int的代码
  • int2string思路就是int数对10取余,得到最后一位,除以10后更新,循环获得每一位变成字符
  • string2int思路:从高位开始,没获得一位乘以10之后加上后面的值
代码
//序列化二叉树

class Codec { 
      string int2string(int val) { 
             string str; 
             string str_reverse; 
             while (val)  { 
                    str += val % 10 + '0'; 
                    val = val / 10; 
             }

             for (int i = str.length() - 1; i >= 0; i--) 
                    str_reverse += str[i]; 
             return str_reverse; 
      } 
      int string2int(string str)  { 
             int  val = 0; 
             for (int i = 0; i<str.length(); i++)  { 
                    val = val * 10 + str[i] - '0'; 
             } 
             return val; 
      } 

      void serializeCore(TreeNode* root, string&str) { 
             //使用先序遍历序列化二叉树 
             if (root == nullptr)  { 
                    str += "$";      //空节点用$符号代替 
                    str += ",";      //用逗号作为分割符号 
                    return; 
             } 
             //如果有值,则转化为字符串 
             str += int2string(root->val); 
             str += ',';        //用逗号作为分割符号  
             //序列化左子树,右子树 
             serializeCore(root->left, str); 
             serializeCore(root->right, str); 
      } 

      vector<string>  split(string& str, string&delim) { //将分割后的子字符串存储在vector中 
             vector<string> res; 
             if ("" == str) return  res;  
             string strs = str + delim; //*****扩展字符串以方便检索最后一个分隔出的字符串 
             size_t pos; 
             size_t size = strs.size(); 
             for (int i = 0; i < size; ++i) { 
                    pos = strs.find(delim, i); //pos为分隔符第一次出现的位置,从i到pos之前的字符串是分隔出来的字符串 
                    if (pos < size-1) { //如果查找到,如果没有查找到分隔符,pos为string::npos 
                          string s = strs.substr(i, pos - i);//*****从i开始长度为pos-i的子字符串 
                          res.push_back(s);//两个连续空格之间切割出的字符串为空字符串,这里没有判断s是否为空,所以最后的结果中有空字符的输出, 
                          i = pos + delim.size() - 1; 
                    } 
             } 
             return res;

      } 
      /*--------------------- 
      来源:CSDN 
      原文:https://blog.csdn.net/Mary19920410/article/details/77372828 
      版权声明:本文为博主原创文章,转载请附上博文链接! 
      */ 

      void deserializeCore(TreeNode*&root, vector<string> &str, int &index) 
      { 
             if (index>str.size() - 1) return; 
             //从string 里面读取数据 
             string tmp = str[index]; 
             index += 1; 
             if (tmp.length() == 1 && tmp[0] == '$')//如果是空节点,则跳过不构造树 
             { 
                    return; 
             } 
             //如果是数组,则转化为数字 
             int val = string2int(tmp); 
             root = new TreeNode(val);         //创建节点 
             //继续读取数据创建左子树,右子树 
             deserializeCore(root->left, str, index); 
             deserializeCore(root->right, str, index); 
             }

public: 
      // Encodes a tree to a single string. 
      string serialize(TreeNode* root) { 
             string str; 
             serializeCore(root, str); 
             return str;

      } 
      // Decodes your encoded data to tree. 
      TreeNode* deserialize(string data) { 
             string del; 
            TreeNode*root = nullptr; 
             int index = 0; 
             del += ','; 
             vector<string> str = split(data, del);  
             deserializeCore(root, str, index); 
             return root; 
      } 
};




6.计算右侧小于当前元素的个数

题目 (Leetcode315)

给定一个整数数组 nums,按要求返回一个新数组 counts。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。
leetcode 二分查找和二叉查找树

思路

最笨的办法就是,遍历每个元素,统计它后面的元素比它小的元素个数,时间复杂度为O(n^2)

另外这个题目统计的是后面的元素比当前元素小的个数,由于数据是顺序读入的,如果正访问当前元素,后面的元素还没有读入内存,不适合统计
因此可以考虑将数据逆置之后再考虑,逆置之后题目就转化为当前元素前面的元素比它小的个数

要统计A(i)前面的数据比A(i)小的个数,最好在A输入之前的其他元素A(i-1),A(i-2)等元素能够具备该性质,即A(i-1)有一个属性count记录比A(i-1)小的元素个数,A(i-2)有个属性count记录比A(i-2)小的元素个数,这样的话,最好A(i-1),A(i-2)是有序的,当我们新输入A(i)时候我们只用让A(i)与A(i-1)比较,如果比A(i-1)大,则A(i)的count 直接等于A(i-1)的count+1,否则继续和A(i-2)比较

基于以上分析,我们发现使用二叉查找树在构建的时候有一个比较的过程,同时能够保证有序,只不过我们要自己给每个节点定义一个count,如下所示:****
leetcode 二分查找和二叉查找树

构建完成后树的中信息如下:
leetcode 二分查找和二叉查找树
如果要对比它小的元素计数,其中某个状态,如插入第二个5的时候的流程如下
leetcode 二分查找和二叉查找树

代码


class Solution
{
   	struct BSTNode
   {
   	int val;
   	int cnt;		//记录比当前元素小的元素个数
   	BSTNode* left;
   	BSTNode* right;
   	BSTNode(int x):val(x),cnt(0),left(NULL),right(NULL){}
   	
   };
   public:
   	vector<int> countSmaller(vector<int> &nums)
   	{
   		vector<int>result;
   		vector<BSTNode*> node_vec;			//为了构建二叉树,分配节点内存,节点池
   		if(nums.size()==0) return result;
   		
   		//new出新的节点放在节点池中
   		for(int i=nums.size()-1;i>=0;i--)		//同时逆序
   		{
   			node_vec.push_back(new BSTNode(nums[i]));
   		} 
   		
   		vector<int>count;	    //存储比当前节点小的元素个数,count反转一下就是result
   		count.push_back(0);		//第一个插入的节点的count_small=0
   		
   		//将每个元素依次插入到二叉搜索树中,每次插入都计算插入节点时,比当前节点小的元素个数
   		for(int i=1;i<node_vec.size();i++)
   		{
   			int cnt =0;					//每次插入都计算插入节点时,比当前节点小的元素个数
   			BST_Insert(node_vec[0],node_vec[i],cnt);
   			count.push_back(cnt); 
   		}
   		
   		//回收内存
   		for(int i=0;i<node_vec.size();i++)
   			delete node_vec[i]; 
   		for(int i=count.size()-1;i>=0;i--)
   		{
   			result.push_back(count[i]);		//因为逆置了,这里再反回去
   		} 
   		return result;
   	}
    
void BST_Insert(BSTNode*&root,BSTNode* insert,int &count_small)   //count_small表示比插入节点小的元素个数,这里的root要传引用,里面要修改他
{
   if(root==NULL)
   {
   	root =insert;  	return;
   }
   
   //如果节点不为空,则按照二叉查找树的规则插入树
   if(insert->val <= root->val)	//插入左子树
   {
   	root->cnt ++;			//有元素比当前节点小,则记录
   	BST_Insert(root->left,insert,count_small);
   }
   else
   {
   	count_small += (root->cnt+1);					//如果插入的元素大于当前节点,则比插入元素小的元素个数在当前节点个数基础上加1
   	BST_Insert(root->right,insert,count_small);
   }
} 
};