jjzjj

【查找算法】二分查找(C# + 递归、非递归和变种形式)

会敲键盘的肘子 2024-05-05 原文

【查找算法】二分查找(C# + 递归、非递归和变种形式)

写在前面:

本文主要介绍二分查找算法,通过图片解析每一次查找的情况。代码通过C#实现,分别有递归、非递归和变种三种形式。其中变种主要解决数组出现重复数据的问题。最后,我们还分析了二分查找的局限性。


活动地址:CSDN21天学习挑战赛

本文关键字:经典算法、查找算法、二分查找、图解、C#

文章目录

一、算法效率

1. 时间复杂度

度量一个程序(算法)执行时间的两种方法。

  • 事后统计的方法

    这种方法可行,但有两个问题:一是要想对设计的算法的运行性能进行评测,需要事件运行该程序;二是所得时间的统计量依赖于计算机的硬件、软件等环境因素。

  • 事前估算的方法

    通过分析某个算法的时间复杂度来判断哪个算法更优。

  • 时间频度

    一个算法花费的时间与算法中语句的执行次数成正比。一个算法中的语句执行次数称为语句频度或者时间频度。记为T(n)

此处引用清华大学《数据结构》课程的一段话,一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n) / f(n) 的极限值为不等于零的常数,则称f(n)T(n)的同数量级函数。记作 T(n)=O( f(n) ),称O( f(n) ) 为算法的渐进时间复杂度,简称时间复杂度。

2. 空间复杂度

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元。

二、查找算法

1. 顺序(线性)查找

最简单的查找方法。思路也很简单,从数组的一边开始,逐个进行元素的比较,如果与给定的待查找元素相同,则查找成功;如果整个扫描结束后,仍未找到相匹配的元素,则查找失败。

2. 二分查找/折半查找

是一种效率相对较高的查找方法。使用该算法的前提要求是有序数组,因为算法的核心思想是尽快的缩小搜索区间,这就需要保证在缩小范围的同时,不能有元素的遗漏。

3.插值查找

插值查找算法类似于二分查找,不同的是插值查找每次从自适应mid处开始查找。

4.斐波那契查找

斐波那契查找原理与前两种相似,仅仅改变了中间结点(mid)的位置,mid不再是中间或插值得到,而是位于黄金分割点附近

三、算法实践

1. 图解算法原理

二分查找的思想非常简单,有点类似分治的思想。二分查找针对的是一个有序的数据集合,每次都通过跟区间的中间元素对比,将待查找的区间缩小为之前的一半,直到找到要查找的元素,或者区间被缩小为 0。

下面通过一个动图来看一看二分查找的原理,图中也反应了二分查找与遍历查找的效率

  • 图片来源网络

  • 二分查找的前提要求是有序数组

  • 图中展示了二分查找的原理

  • 图中反应二分查找与遍历查找的效率对比

那么,具体是如何查找的,我们以上面动图的数组[1,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59]为例。

假设我们有数组[1,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59],要求查找到37的索引号。

  • 1

    low = 0, high = 16,求出mid = low + (high - low) / 2 = 0 + (16 - 0) / 2 = 8。此时索引号8对应的数值是23,23 小于37,则区间变成右半部分,[9, 16]

  • 2

    low = 9, high = 16,求出mid = low + (high - low) / 2 = 9 + (16 - 9) / 2 = 12。此时索引号12对应的数值是41,41大于37,则区间变成左半部分,[9, 11]

  • 3

    low = 9, high = 11,求出mid = low + (high - low) / 2 = 9 + (11 - 9) / 2 = 10。此时索引号10对应的数值是31,31小于37,则区间变成右半部分,[11, 11]

  • 4

此时low = high,返回索引号11。

与mid值相等:直接返回对应的位置(对于有重复元素的情况,会在其他子专栏中说明)。

比mid值大:由于元素有序,要查找的元素一定在左侧(如有),于是搜索区间变为左一半。

比mid值小:由于元素有序,要查找的元素一定在右侧(如有),于是搜索区间变为右一半。

2. 算法实现

首先,我们假设有序数组中不存在重复元素

非递归实现
	/// <summary>
    /// 二分查找静态类
    /// </summary>
    class BinarySearch
    {
        /// <summary>
        /// 二分查找非递归方法
        /// </summary>
        /// <param name="arr"></param>
        /// <param name="value"></param>
        /// <returns></returns>
		public static int BinarySearchMethod(int[] arr, int value)
		{
            int low = 0;
            int high = arr.Length - 1;
            int count = 0;
            while (low <= high)
            {
                int mid = low + (high - low) / 2;
                if (arr[mid] == value)
                {
                    Console.WriteLine($"查找次数{count}");
                    return mid;
                }
                else if (arr[mid] < value)
                {
                    low = mid + 1;
                }
                else
                {
                    high = mid - 1;
                }
                count++;
            }
            Console.WriteLine($"查找次数{count}");
            return -1;
        }


    }
	class Program
    {
        static void Main(string[] args)
        {
            int[] intArray = new int[] { 1, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59 };
            int value = 37;
            int index = BinarySearch.BinarySearchMethod(intArray,value);
            Console.WriteLine($"{value}的索引号是{index}");

        }
    }

==============运行结果===============
查找次数3
37的索引号是11
递归实现
		/// <summary>
        /// 二分查找递归方法
        /// </summary>
        /// <param name="arr"></param>
        /// <param name="value"></param>
        /// <returns></returns>
		public static int BinarySearchRecursionMethod(int[] arr, int low, int high, int value)
        {
            // 当 low > high 时,说明递归整个数组,但是没有找到
            if (low > high)
            {
                return -1;
            }
            int mid = (high - low) / 2 + low;
            int midVal = arr[mid];

            if (value > midVal)
            { // 向 右递归
                return BinarySearchRecursionMethod(arr, mid + 1, high, value);
            }
            else if (value < midVal)
            { // 向左递归
                return BinarySearchRecursionMethod(arr, low, mid - 1, value);
            }
            else
            {
                return mid;
            }

        }
	class Program
    {
        static void Main(string[] args)
        {
            int[] intArray = new int[] { 1, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59 };
            int value = 37;
            int index = BinarySearch.BinarySearchRecursionMethod(intArray,0, intArray.Length - 1, value);
            Console.WriteLine($"{value}的索引号是{index}");

        }
    }

==============运行结果===============
查找次数3
37的索引号是11

3. 二分查找变种

当一个有序数组中,有多个相同的数值时,如何将所有的数值都查找到。

思路分析

  1. 在找到mid 索引值,不要马上返回
  2. 向mid 索引值的左边扫描,将所有相同的值的元素的下标,加入到集合ArrayList
  3. 向mid 索引值的右边扫描,将所有相同的值 的元素的下标,加入到集合ArrayList
  4. 将Arraylist返回
		/// <summary>
        /// 二分查找递归方法变种
        /// </summary>
        /// <param name="arr"></param>
        /// <param name="value"></param>
        /// <returns></returns>
		public static List<int> BinarySearchRecursionVarietyMethod(int[] arr, int low, int high, int value)
        {
            // 当 low > high 时,说明递归整个数组,但是没有找到
            if (low > high)
            {
                return new List<int>();
            }
            int mid = (high - low) / 2 + low;
            int midVal = arr[mid];

            if (value > midVal)
            { // 向 右递归
                return BinarySearchRecursionVarietyMethod(arr, mid + 1, high, value);
            }
            else if (value < midVal)
            { // 向左递归
                return BinarySearchRecursionVarietyMethod(arr, low, mid - 1, value);
            }
            else
            {
                List<int> resIndexlist = new List<int>();
                //向mid 索引值的左边扫描,将所有满足的元素的下标,加入到集合List
                int temp = mid - 1;
                while (true)
                {
                    if (temp < 0 || arr[temp] != value)
                    {//退出
                        break;
                    }
                    //否则,就temp 放入到 resIndexlist
                    resIndexlist.Add(temp);
                    temp -= 1; //temp左移
                }
                resIndexlist.Add(mid); 

                //向mid 索引值的右边扫描,将所有满足的元素的下标,加入到集合ArrayList
                temp = mid + 1;
                while (true)
                {
                    if (temp > arr.Length - 1 || arr[temp] != value)
                    {//退出
                        break;
                    }
                    //否则,就temp 放入到 resIndexlist
                    resIndexlist.Add(temp);
                    temp += 1; //temp右移
                }

                return resIndexlist;
            }

        }
int[] intArray = new int[] { 1, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37,37,37, 41, 43, 47, 53, 59 };
int value = 37;
System.Collections.Generic.List<int> index = BinarySearch.BinarySearchRecursionVarietyMethod(intArray,0, intArray.Length - 1, value);

此时结果输出一个List,值分别是11、12、13。

3.时间复杂度

每次比较后查找区间会呈等比数列缩小,且每一次缩小操作只涉及两个数据的大小比较,所以,经过了k 次区间缩小操作,时间复杂度就是 O(k),最终区间大小为n/2^k=1,我们可以求得 k=log2n,所以时间复杂度就是 O(logn)

因为logn是一个非常“恐怖”的数量级,即便 n 非常非常大,对应的 logn 也很小。比如 n=2^32,这个数很大了吧?大约是 42 亿。也就是说,如果我们在 42 亿个数据中用二分查找一个数据,最多需要比较 32 次。

我们用大 O 标记法表示时间复杂度的时候,一般会省略掉常数、系数和低阶。对于常量级时间复杂度的算法来说,O(1) 有可能表示的是一个非常大的常量值,比如 O(1000)O(10000)。所以,常量级时间复杂度的算法有时候可能还没有 O(logn) 的算法执行效率高。

上述动态图也很好的反应了这个结论:常量级时间复杂度的算法有时候可能还没有 O(logn) 的算法执行效率高

4.空间复杂度

算法执行过程中,只需要一个临时变量来进行存储插入值,所以空间复杂度是O(1)

四、局限性

前面我们分析过,二分查找的时间复杂度是 O(logn),查找数据的效率非常高。常量级时间复杂度的算法有时候可能还没有 O(logn) 的算法执行效率高。但是二分查找也有很大的局限性。

  • 二分查找依赖的是顺序表结构

    二分查找算法需要按照下标随机访问元素,只能用在数据是通过顺序表来存储的数据结构上。如果你的数据是通过其他数据结构存储的,则无法应用二分查找。

  • 二分查找针对的是有序数据

    如果我们的数据集合有频繁的插入和删除操作,要想用二分查找,要么每次插入、删除操作之后保证数据仍然有序,要么在每次二分查找之前都先进行排序。针对这种动态数据集合,无论哪种方法,维护有序的成本都是很高的。

  • 数据量太小不适合二分查找

    如果要处理的数据量很小,完全没有必要用二分查找,顺序遍历就足够了。比如我们在一个大小为 10 的数组中查找一个元素,不管用二分查找还是顺序遍历,查找速度都差不多。

  • 数据量太大也不适合二分查找

    二分查找的底层需要依赖数组这种数据结构,而数组为了支持随机访问的特性,要求内存空间连续,对内存的要求比较苛刻。比如,我们有 1GB 大小的数据,如果希望用数组来存储,那就需要 1GB 的连续内存空间。


写在结尾:

文章中出现的任何错误请大家批评指出,一定及时修改。

希望看到这里的小伙伴能给个三连支持!

有关【查找算法】二分查找(C# + 递归、非递归和变种形式)的更多相关文章

  1. ruby - 当使用::指定模块时,为什么 Ruby 不在更高范围内查找类? - 2

    我刚刚被困在这个问题上一段时间了。以这个基地为例:moduleTopclassTestendmoduleFooendend稍后,我可以通过这样做在Foo中定义扩展Test的类:moduleTopmoduleFooclassSomeTest但是,如果我尝试通过使用::指定模块来最小化缩进:moduleTop::FooclassFailure这失败了:NameError:uninitializedconstantTop::Foo::Test这是一个错误,还是仅仅是Ruby解析变量名的方式的逻辑结果? 最佳答案 Isthisabug,or

  2. ruby - 查找字符串中的内容类型(数字、日期、时间、字符串等) - 2

    我正在尝试解析一个CSV文件并使用SQL命令自动为其创建一个表。CSV中的第一行给出了列标题。但我需要推断每个列的类型。Ruby中是否有任何函数可以找到每个字段中内容的类型。例如,CSV行:"12012","Test","1233.22","12:21:22","10/10/2009"应该产生像这样的类型['integer','string','float','time','date']谢谢! 最佳答案 require'time'defto_something(str)if(num=Integer(str)rescueFloat(s

  3. c# - 如何在 ruby​​ 中调用 C# dll? - 2

    如何在ruby​​中调用C#dll? 最佳答案 我能想到几种可能性:为您的DLL编写(或找人编写)一个COM包装器,如果它还没有,则使用Ruby的WIN32OLE库来调用它;看看RubyCLR,其中一位作者是JohnLam,他继续在Microsoft从事IronRuby方面的工作。(估计不会再维护了,可能不支持.Net2.0以上的版本);正如其他地方已经提到的,看看使用IronRuby,如果这是您的技术选择。有一个主题是here.请注意,最后一篇文章实际上来自JohnLam(看起来像是2009年3月),他似乎很自在地断言RubyCL

  4. C# 到 Ruby sha1 base64 编码 - 2

    我正在尝试在Ruby中复制Convert.ToBase64String()行为。这是我的C#代码:varsha1=newSHA1CryptoServiceProvider();varpasswordBytes=Encoding.UTF8.GetBytes("password");varpasswordHash=sha1.ComputeHash(passwordBytes);returnConvert.ToBase64String(passwordHash);//returns"W6ph5Mm5Pz8GgiULbPgzG37mj9g="当我在Ruby中尝试同样的事情时,我得到了相同sha

  5. ruby-on-rails - 使用回形针的嵌套形式 - 2

    我有一个名为posts的模型,它有很多附件。附件模型使用回形针。我制作了一个用于创建附件的独立模型,效果很好,这是此处说明的View(https://github.com/thoughtbot/paperclip):@attachment,:html=>{:multipart=>true}do|form|%>posts中的嵌套表单如下所示:prohibitedthispostfrombeingsaved:@attachment,:html=>{:multipart=>true}do|at_form|%>附件记录已创建,但它是空的。文件未上传。同时,帖子已成功创建...有什么想法吗?

  6. 区块链之加解密算法&数字证书 - 2

    目录一.加解密算法数字签名对称加密DES(DataEncryptionStandard)3DES(TripleDES)AES(AdvancedEncryptionStandard)RSA加密法DSA(DigitalSignatureAlgorithm)ECC(EllipticCurvesCryptography)非对称加密签名与加密过程非对称加密的应用对称加密与非对称加密的结合二.数字证书图解一.加解密算法加密简单而言就是通过一种算法将明文信息转换成密文信息,信息的的接收方能够通过密钥对密文信息进行解密获得明文信息的过程。根据加解密的密钥是否相同,算法可以分为对称加密、非对称加密、对称加密和非

  7. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  8. ruby-on-rails - 在 Rails 中更高效地查找或创建多条记录 - 2

    我有一个应用需要发送用户事件邀请。当用户邀请friend(用户)参加事件时,如果尚不存在将用户连接到该事件的新记录,则会创建该记录。我的模型由用户、事件和events_user组成。classEventdefinvite(user_id,*args)user_id.eachdo|u|e=EventsUser.find_or_create_by_event_id_and_user_id(self.id,u)e.save!endendend用法Event.first.invite([1,2,3])我不认为以上是完成我的任务的最有效方法。我设想了一种方法,例如Model.find_or_cr

  9. ruby - 查找重叠的正则表达式匹配项 - 2

    我想找到给定字符串中的所有匹配项,包括重叠匹配项。我怎样才能实现它?#Example"a-b-c-d".???(/\w-\w/)#=>["a-b","b-c","c-d"]expected#Solutionwithoutoverlappedresults"a-b-c-d".scan(/\w-\w/)#=>["a-b","c-d"],but"b-c"ismissing 最佳答案 在积极的前瞻中使用捕获:"a-b-c-d".scan(/(?=(\w-\w))/).flatten#=>["a-b","b-c","c-d"]参见Rubyde

  10. ruby - 递归地将所有数字字符串转换为 Ruby 哈希中的整数 - 2

    我有一个随机大小的散列,它可能有类似"100"的值,我想将其转换为整数。我知道我可以使用value.to_iifvalue.to_i.to_s==value来做到这一点,但我不确定我将如何在我的散列中递归地做到这一点,考虑到一个值可以是一个字符串,或一个数组(哈希或字符串),或另一个哈希。 最佳答案 这是一个非常简单的递归实现(尽管必须同时处理数组和散列会增加一些技巧)。deffixnumifyobjifobj.respond_to?:to_i#IfwecancastittoaFixnum,doit.obj.to_ielsifobj

随机推荐