数据结构与算法之美 | 二分查找:剑指offer53 在排序数组中查找数字

简介: 我们先分析如何找到第一个k。二分查找算法总是先拿数组中间的数字和k做比较。如果中间的数字比k大,那么k只有可能出现在数组的前半段,下一轮我们只在数组的前半段查找就可以了。如果中间的数字比k小,那么k只有可能出现在数组的后半段,下一轮我们只在数组的后半段查找就可以了。

如何利用“数组是排序的”这一特点设计更快的算法是这一题最好的解决办法?本节我们讨论的本题都是基于这一特点展开的。


通常,我们需要在一个长度为n的数组中查找一个数,需要O(n)O(n)次,所以顺序扫描/查找的时间复杂度为O(n)O(n)。显然这不是最好的方法。


接下来,我们思考如何更好地利用二分查找算法O(logn)O(logn)。二分查找无非就是从数组的中间位置开始,然后讨论三种情况(针对升序数组)。


  • 如果中间位置的数满足/等于设定条件,找到结束;
  • 如果中间位置的数小于设定条件,满足条件的数在后半段;
  • 如果中间位置的数大与设定条件,满足条件的数在前半段;


程序实现的伪代码框架可以有以下参考:


int Binary_Search(int* numbers, int length)
{
  if(numbers == nullptr || length <= 0)
    return -1;
  int left = 0;
  int right = length - 1;
  while(legt <= right)
  {
    int middle = left + (right-left)>>1;
    if(numbers[middle] == middle)  //设定条件
      return middle;
    if(numbers[middle] > middle)
      right = middle - 1;
    else
      left = middle + 1;
  }
  return -1;
}


分析至此,我们清楚地知道,二分查找的思想就是在于找到这个设定条件。那么,我们就来以剑指offer中面试题53的三种情况做分别介绍。


题目一:数字在排序数组中出现的次数。


统计一个数字在排序数组中出现的次数。例如,输入排序数组{1,2,3,3,3,3,4,5}和数字3,由于3在数组中出现了4次,因此输出为4。


假设我们要统计数字k在排序数组中出现的次数。如何采用二分查找的方法找到第一个k和最后一个k是本题的核心问题。


我们先分析如何找到第一个k。二分查找算法总是先拿数组中间的数字和k做比较。如果中间的数字比k大,那么k只有可能出现在数组的前半段,下一轮我们只在数组的前半段查找就可以了。如果中间的数字比k小,那么k只有可能出现在数组的后半段,下一轮我们只在数组的后半段查找就可以了。


如果中间的数字和k相等呢?我们先判断这个数字是不是第一个k。如果中间数字的前面一个数字不是k,那么此时中间的数字刚好就是第一个k;如果中间数字的前面一个数字也是k,那么第一个k肯定实在数组的前半段,下一轮我们仍然需要在数组的前半段查找。


基于这种思路,我们可以很容易地写出递归的代码找到排序数组中的第一个k。下面是一段参考代码:


int GetFirstK(int* data, int length, int k, int start, int end)
{
  if(start > end)
    return -1;
  int middleIndex = (start + end)/2;
  int middleData = data[middleIndex];
  if(middle == k)
  {
    if((middleIndex > 0 && data[middleIndex - 1] != k)
      || middleIndex == 0)
      return middleIndex;
    else 
      end = middleIndex - 1;
  }
  else if(middleData > k)
    end = middleIndex - 1;
  else
    start = middleIndex + 1;
  return GetFirstK(data,length,k,start, end);
}


我们可以用同样的思路在排序数组中找到最后一个k。基于递归写出如下代码:


int GetLastK(int* data, int length, int k, int start, int end)
{
  if(start > end)
    return -1;
  int middleIndex = (start + end)/2;
  int middleData = data[middleIndex];
  if(middle == k)
  {
    if((middleIndex < length -1 && data[middleIndex - 1] != k)
      || middleIndex == length -1 )
      return middleIndex;
    else 
      start = middleIndex + 1;
  }
  else if(middleData < k)
    start = middleIndex + 1;
  else
    end = middleIndex - 1;
  return GetFirstK(data,length,k,start, end);
}


在分别找到第一个k和最后一个k的下标之后,我们就能计算出k在数组中出现的次数。相应的代码如下:


int GetNumberOfK(int* data, int length, int k)
{
  int number = 0;
  if(data != nullptr && length > 0)
  {
    int first = GetFirstK(data,length,k,0,length-1);
    int last = GetLastK(data,length,k,0,length-1);
    if(first > -1 && last > -1)
      number = last - first + 1;
  }
  return number;
}

题目二:0~n中缺失的数字


一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-10 n−1之内。在范围0~n-10 n−1内的n个数字中有且只有一个数字不在该数组中,请找出这个数。


因为0~n-10 n−1这些数字在数组中是排序的,因此数组中开始的一些数字与它们下标相同。也就是说,0在下标为0的位置,1在下标为1的位置,以此类推。如果不在数组中的那个数字记为m,那么所有比m小的数字的下标都与它们的值相同。

由于m不在数组中,那么m+1处在下表为m的位置,m+2处在下标为m+1的位置,以此类推。我们发现m正好是数组中第一个数值和下标不相等的下标,因此这个问题转换成在排序数组中找出第一个值和下标不相等的元素。


int GetMissingNumber(int* numbers, int length)
{
  if(numbers == nullptr || length <= 0)
    return -1;
  int left = 0;
  int right = length -1;
  while(left < right)
  {
    int middle = (right + left)>>1;
    if(numbers[middle] != middle)
    {
      if(middle == 0 || numbers[middle-1] == middle -1)
        return middle;
      right = middle -1;
    }
    else
      left = middle + 1;
  }
  if(left == length)
    return length;
}


题目三:数组中数值和下标相等的元素


假设一个单调递增的数组里的每个元素都输证书并且是唯一的。请编写程序实现一个函数,找出数组中任意一个数值等于其下标的元素。例如,在数组{-3,-1,1,3,5}中,数字3和它的下标相等。


采用二分查找方法,假设我们某一步抵达数组中的第i个数字。如果我们很幸运,该数字的值刚好也是i,那么我们就找到了一个数字和其下标相等。


那么当数字的值和下标不相等的时候该怎么办呢?


假设数字的值为m,我们先考虑m大与i的情形,即数字的值大与它的下标。由于数组中的所有数字都唯一并且单调递增,那么对于任意大于0的k,位于下标i+k的数字的值大与或等于m+k。另外,因为m>i,所以m+k>i+k。因此,位于下标i+k的数字的值一定大与它的下标。这意味着如果第i个数字的值大与i,那么他右边的数字都大于对应的下标,我们都可以忽略。下一轮查找我们只需要从他左边的数字中查找即可。


数字的值m小于它的下标i的情形和上面类似。下面是基于二分查找的参考代码:


int GetNumberSameAsIndex(int* numbers, int length)
{
  if(numbers == nullptr || length <= 0)
    return -1;
  int left = 0;
  int right = length - 1;
  while(legt <= right)
  {
    int middle = left + (right-left)>>1;
    if(numbers[middle] == middle)  
      return middle;
    if(numbers[middle] > middle)
      right = middle - 1;
    else
      left = middle + 1;
  }
  return -1;
}


二分查找算法属于分治的思想,它的应用非常广,之后我再给大家总结其他类型的题目。例如,二进制解析、药品有效性查找、进阶可采用三分法等。

相关文章
|
16天前
|
算法 索引
【算法】——二分查找合集
二分查找基础模版和进阶模版,查找元素位置,搜索插入位置,x的平方根,山脉数组的峰顶索引,寻找峰值,点名
|
2月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
78 5
|
2月前
|
存储 人工智能 算法
数据结构实验之C 语言的函数数组指针结构体知识
本实验旨在复习C语言中的函数、数组、指针、结构体与共用体等核心概念,并通过具体编程任务加深理解。任务包括输出100以内所有素数、逆序排列一维数组、查找二维数组中的鞍点、利用指针输出二维数组元素,以及使用结构体和共用体处理教师与学生信息。每个任务不仅强化了基本语法的应用,还涉及到了算法逻辑的设计与优化。实验结果显示,学生能够有效掌握并运用这些知识完成指定任务。
63 4
|
2月前
|
搜索推荐 算法 C语言
【排序算法】八大排序(下)(c语言实现)(附源码)
本文继续学习并实现了八大排序算法中的后四种:堆排序、快速排序、归并排序和计数排序。详细介绍了每种排序算法的原理、步骤和代码实现,并通过测试数据展示了它们的性能表现。堆排序利用堆的特性进行排序,快速排序通过递归和多种划分方法实现高效排序,归并排序通过分治法将问题分解后再合并,计数排序则通过统计每个元素的出现次数实现非比较排序。最后,文章还对比了这些排序算法在处理一百万个整形数据时的运行时间,帮助读者了解不同算法的优劣。
161 7
|
2月前
|
搜索推荐 算法 C语言
【排序算法】八大排序(上)(c语言实现)(附源码)
本文介绍了四种常见的排序算法:冒泡排序、选择排序、插入排序和希尔排序。通过具体的代码实现和测试数据,详细解释了每种算法的工作原理和性能特点。冒泡排序通过不断交换相邻元素来排序,选择排序通过选择最小元素进行交换,插入排序通过逐步插入元素到已排序部分,而希尔排序则是插入排序的改进版,通过预排序使数据更接近有序,从而提高效率。文章最后总结了这四种算法的空间和时间复杂度,以及它们的稳定性。
133 8
|
3月前
|
算法 程序员 索引
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
栈的基本概念、应用场景以及如何使用数组和单链表模拟栈,并展示了如何利用栈和中缀表达式实现一个综合计算器。
58 1
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
|
3月前
|
算法 C# 索引
C#二分查找算法
C#二分查找算法
|
3月前
|
算法 搜索推荐 Java
数据结构与算法学习十三:基数排序,以空间换时间的稳定式排序,速度很快。
基数排序是一种稳定的排序算法,通过将数字按位数切割并分配到不同的桶中,以空间换时间的方式实现快速排序,但占用内存较大,不适合含有负数的数组。
48 0
数据结构与算法学习十三:基数排序,以空间换时间的稳定式排序,速度很快。
|
3月前
|
存储 算法 C语言
【C语言】二分查找算法
【C语言】二分查找算法
|
3月前
|
算法
❤️算法笔记❤️-(每日一刷-83、删除排序链表中的重复项)
❤️算法笔记❤️-(每日一刷-83、删除排序链表中的重复项)
38 0