16 基本查找算法:二分查找算法

二分查找算法

原理

二分查找算法也叫折半法查找法,要求待查找的列表必须是按关键字大小有序排列的顺序表。
查找过程如下所示:
(1)将表中间位置记录的关键字与查找关键字比较,如果两者相等则表示查找成功;否则利用中间位置记录将表分成前、后两个子表。
(2)如果中间位置记录的关键字大于查找关键字,则进一步查找前一个子表,否则查找后一个子表。
(3)重复以上过程,一直到找到满足条件的记录为止时表明查找成功。
(4)如果最终子表不存在,则表明查找不成功。

性能分析

接下来用平均查找长度来分析二分查找算法的性能,我们可以使用一个被称为判定树的二叉树来描述二分查找过程。首先验证树中的每一个节点对应表中的一个记录,但是节点值不是用来记录关键字的,而是用于记录在表中的位置序号的。根节点对应当前区间的中间记录,左子树对应前一个子表,右子树对应后一个子表。当折半查找成功时,关键字的比较次数不会超过判定树的深度。因为判定树的叶节点和所在层次的差是1,所以n个节点的判定树的深度与n个节点的完全二叉树的深度相等,都是(log2 n)+1。这样,折半查找成功时,关键字比较次数最多不超过(log2 n)+1。相应地,当折半查找失败时,其整个过程对应判定树中从根节点到某个含空指针的节点的路径。所以当折半查找成功时,关键字比较次数也不会超过判定树的深度(log2 n)+1。可以假设表的长n=2h -1,相应的判定树一定是深度为h的满二叉树,log2 (n+1)。在此假设将长度为n的表分成b块,每块含有s个元素,即b=n/s。又假设每个记录的查找概率相等,则折半查找成功时的平均查找长度为:

二分查找算法性能分析.png

二分查找方法具有比较次数少、查找速度快及平均性能好的优点。缺点是要求待查表为有序表,且插入、删除困难。因此,折半查找方法适用于不经常变动而查找频繁的有序列表。

算法实现
说明:

元素必须是有序的,如果是无序的则要先进行排序操作。

基本思想:

二分查找基于分治思想,在实现上可以使用递归或迭代,首先用给定值k先与中间结点的关键字比较,中间结点把线形表分成两个子表,若相等则查找成功;若不相等,再根据k与该中间结点关键字的比较结果确定下一步查找哪个子表,这样递归进行,直到查找到或查找结束发现表中没有这样的结点。

复杂度分析:
最坏情况下,关键词比较次数为log2(n+1),且期望时间复杂度为O(log2n);

注:二分查找的前提条件是需要有序表顺序存储,对于静态查找表,一次排序后不再变化,二分查找能得到不错的效率。
但对于需要频繁执行插入或删除操作的数据集来说,维护有序的排序会带来不小的工作量,那就不建议使用。

Go语言描述

最简单的版本:即序列中忽略重复元素的位置情况:

// 迭代版本
func BinarySearch1(arr []int, v int) int {
    start, end := 0, len(arr)-1
    for start <= end {
        mid := start + (end-start) / 2
        // fmt.Println("mid:",mid)
        if arr[mid] == v {
            return mid
        } else if arr[mid] > v {
            // 左边
            end = mid - 1
        } else {
            // 右边
            start = mid + 1
        }
    }

    return -1
}

// 递归版本
func BinarySearch2(arr []int, v, start, end int) int {
    if start <= end {
        mid := (start + end) / 2
        // fmt.Println("mid:",mid)
        if arr[mid] == v {
            // 找到
            return mid
        } else if arr[mid] > v {
            // 左边递归
            return BinarySearch2(arr, v, start, mid-1)
        } else {
            // 右边递归
            return BinarySearch2(arr, v, mid+1, end)
        }
    }
    return -1
}

二分查找算法的变体

以上的算法实现是没考虑重复元素的严格情况的,如需考虑重复元素且返回特定的索引,有以下变体:

变体一:查找第一个值等于给定值的元素
// 变体一:查找第一个值等于给定值的元素
func BinarySearch3(arr []int, v int) int {
    start, end := 0, len(arr)-1
    for start <= end {
        mid := start + (end-start)/2
        // fmt.Println("mid:",mid)
        if arr[mid] == v {
            if mid == 0 || arr[mid-1] != v {
                return mid
            } else {
                end = mid - 1
            }
        } else if arr[mid] > v {
            // 左边
            end = mid - 1
        } else {
            // 右边
            start = mid + 1
        }
    }

    return -1
}
变体二:查找最后一个值等于给定值的元素
// 变体二:查找最后一个值等于给定值的元素
func BinarySearch4(arr []int, v int) int {
    start, end := 0, len(arr)-1
    for start <= end {
        mid := start + (end-start)/2
        if arr[mid] == v {
            if mid == end || arr[mid+1] != v {
                return mid
            } else {
                end = mid - 1
            }
        } else if arr[mid] > v {
            // 左边
            end = mid - 1
        } else {
            // 右边
            start = mid + 1
        }
    }

    return -1
}

关于二分查找算法的局限性

二分查找的时间复杂度是O(logn),查找数据的效率非常高。不过,并不是什么情况下都可以用二分查找,它的应用场景是有很大局限性的。那 什么情况下适合用二分查找,什么情况下不适合呢?

  • 首先,二分查找依赖的是顺序表结构,简单点说就是数组。
    那二分查找能否依赖其他数据结构呢?比如链表。答案是不可以的,主要原因是二分查找算法需要按照下标随机访问元素。我们在线性表讲过,数组按照下标随机访问数据的时间复杂度是O(1),而链表随机访问的时间复杂度是O(n)。所以,如果数据使用链表存储,二分查找的时间复杂就会变得很高。 二分查找只能用在数据是通过顺序表来存储的数据结构上。如果你的数据是通过其他数据结构存储的,则无法应用二分查找。

  • 其次,二分查找针对的是有序数据。
    二分查找对这一点的要求比较苛刻,数据必须是有序的。如果数据没有序,我们需要先排序,排序的时间复杂度最低是O(nlogn)。所以,如果我们针对的是一组静态的数据,没有频繁地插入、删除,我们可以进行一次排序,多次二分查找。这样排序的成本可被均摊,二分查找的边际成本就会比较低。如果我们的数据集合有频繁的插入和删除操作,要想用二分查找,要么每次插入、删除操作之后保证数据仍然有序,要么在每次二分查找之前都先进行排 序。针对这种动态数据集合,无论哪种方法,维护有序的成本都是很高的。所以,二分查找只能用在插入、删除操作不频繁,一次排序多次查找的场景中。针对动态变化的数据集合,二分查找将不再适用。那针对动态数据集合,如何在其中快速查找某个数据呢?对于这种需求就需要特定的数据结构了,如二叉查找树就是针对这种需求的。

  • 再次,数据量太小不适合二分查找。
    如果要处理的数据量很小,完全没有必要用二分查找,顺序遍历就足够了。比如我们在一个大小为10的数组中查找一个元素,不管用二分查找还是顺序遍历,查 找速度都差不多。只有数据量比较大的时候,二分查找的优势才会比较明显。
    不过,这里有一个例外。如果数据之间的比较操作非常耗时,不管数据量大小,我都推荐使用二分查找。比如,数组中存储的都是长度超过300的字符串,如此长 的两个字符串之间比对大小,就会非常耗时。我们需要尽可能地减少比较次数,而比较次数的减少会大大提高性能,这个时候二分查找就比顺序遍历更有优势。

  • 最后,数据量太大也不适合二分查找。
    二分查找的底层需要依赖数组这种数据结构,而数组为了支持随机访问的特性,要求内存空间连续,对内存的要求比较苛刻。比如,我们有1GB大小的数据,如果希望用数组来存储,那就需要 的连续内存空间。 注意这里的“连续”二字,也就是说,即便有2GB的内存空间剩余,但是如果这剩余的2GB内存空间都是零散的,没有连续的1GB大小的内存空间,那照样无法申请一个1GB大小的数组。而我们的二分查找是作用在数组这种数据结构之上的,所以太大的数据用数组存储就比较吃力了,也就不能用二分查找了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,710评论 4 376
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,839评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,295评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,776评论 0 223
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,198评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,074评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,200评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,986评论 0 214
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,733评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,877评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,348评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,675评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,393评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,209评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,212评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,003评论 2 280

推荐阅读更多精彩内容

  • 本文的整理基于:http://blog.csdn.net/qq_23217629/article/details/...
    阿阿阿阿毛阅读 1,550评论 0 3
  • 目录 [1. 顺序查找][2. 二分查找][3. 插值查找][4. 斐波那契查找][5. 树表查找][6. 分块查...
    jiangmo阅读 16,777评论 4 6
  • 一些定义: 事件的最早发生时间(ve(j)):从源点到j结点的最长的路径。意味着事件最早能够发生的时间。 事件的最...
    北风知我意阅读 692评论 0 0
  • 顺序查找 说明:顺序查找适合于存储结构为顺序存储或链接存储的线性表。 基本思想:顺序查找也称为线形查找,属于无序查...
    沙海tao金阅读 2,542评论 0 0
  • 9.1 静态查找表 查找操作的性能分析平均查找长度(Average Search Length)。其中为查找表中第...
    Collie阅读 282评论 0 0