python数据结构与算法

数据结构

补充
内存的存储结构:内存是以字节为基本存储单位的, 每个基本存储空间都有自己的地址。大多数情况下整型(int)占4个字节,字符(char)占1个字节,但是在python里int占:总字节数 = 固定头部(16 B) + 变长部分(4 B × 数字绝对值用到的“limb”数)
存储单位从大到小依次为:YB>ZB>EB>PB>TB>GB>MB>KB>B (1B = 8bit)

数据结构的分类

线性结构

线性结构就是数据结构中各个结点具有线性关系

线性结构的特点:

  1. 线性结构是非空集
  2. 线性结构所有结点都最多只有一个直接前驱结点和一个直接后继结点

典型的线性结构 : 栈、队列等

非线性结构

非线性结构就是数据结构中各个结点之间具有多个对应关系

非线性结构的特点:

  1. 非线性结构是非空集
  2. 非线性结构的一个结点可能有多个直接前驱结点和多个直接后继结点

典型的非线性结构 : 树结构和图结构等

线性结构的存储方式

线性结构的实际存储方式,分为两种:

  1. 顺序表 : 将元素顺序地存放在一块连续的存储区里,元素间的顺序关系由它们的存储顺序自然表示
  2. 链表 : 将元素存放在通过链接构造起来的一系列存储块中 , 存储区是非连续的
顺序表
介绍

顺序表元素顺序地存放在一块连续的存储区里,具体的存储方式的两种情况:

  1. 一体式存储
    举例:
    一体式存储

num会记录列表的首元素地址0x11
由于列表所有的数据都是整形,大小都为4个字节,偏移量为4

  • 第一个元素10 : 通过0x11偏移4*0个字节可以找到
  • 第二个元素20 : 通过0x11偏移4*1个字节可以找到
  • 第三个元素30 : 通过0x11偏移4*2个字节可以找到

查找数据的公示 : 0x11 偏移 4 * (n-1) 个字节

  1. 分离式存储
    当存储的类型不同时:
    分离式
    num会记录列表的首元素地址0x11
    由于列表中数据大小不固定 , 偏移量也不确定 , 无法通过偏移的方式查找
    这个时候就可以采用分离式存储
    举例:分离式存储
    地址的大小为4字节是固定的 , 我们可以不存储数据 , 而是存储地址,后续可以通过地址查询到数据。

无论一体式结构还是分离式结构,顺序表在获取数据的时候直接通过下标偏移就可以找到数据所在空间的地址 , 而无需遍历后才可以获取地址 . 所以顺序表在获取地址操作时的时间复杂度 : O(1)

顺序表的结构和扩充
  • 顺序表的结构
    顺序表的完整信息包括两部分:
    ①数据区
    ②信息区,即元素存储区的容量和当前表中已有的元素个数
    (大白话:信息区对数据进行描述、信息显示)
  • 两种扩充策略
    数据区更换为存储空间更大的区域时

①每次扩充增加固定数目的存储位置,如每次扩充增加10个元素位置,这种策略可称为线性增长
特点:节省空间,但是扩充操作频繁,操作次数多。

②每次扩充容量加倍,如每次扩充增加一倍存储空间。
特点:减少了扩充操作的执行次数,但可能会浪费空间资源 , 以空间换时间,推荐的方式

  • 元素存储区的替换
    替换
    一体式存储的顺序表存储在连续的空间,则只能整体搬迁,即信息区和数据区都改变。
    说明:
    左图:顺序表想要扩充:因为黄色部分已经占用,只能整体搬迁让顺序表扩大一倍
    右图:顺序表整体搬迁后扩充一倍
    分离式替换
    分离式存储的顺序表可只整体更换数据区,信息区的链接更新即可
顺序表增加删除元素
  • 增加元素
    • 尾端加入元素,时间复杂度为O(1)
    • 非保序的加入元素(不常见),时间复杂度为O(1) 比如:在指定非尾端位置加入元素,将该元素放入指定位置,原该位置的元素放到尾端
    • 保序的元素加入,时间复杂度为O(n),插入位置后的元素依次后移
  • 删除元素
    • 删除表尾元素,时间复杂度为O(1)
    • 非保序的元素删除(不常见),时间复杂度为O(1),与增加类似,把尾端元素替换到删除元素的位置
    • 保序的元素删除,时间复杂度为O(n),删除的元素后面的元素依次前移
链表
介绍

不需要连续的存储空间
链表
存储: [10 , 20 , 30 , 40]
每个结点有2部分:元素域,下一个结点的内存地址(链接域);最后一个结点链接域为None

链表结构

单链表(单向链表)是链表的一种形式,每个结点包含两个域:元素域和链接域 .
这个链接指向链表中的下一个结点 , 而最后一个结点的链接域则指向一个空值None
链表结构
①表元素域item用来存放具体的数据
②链接域next用来存放下一个结点的位置
③变量head指向链表的头结点(首结点)的位置,从head出发能找到表中的任意结点

  • 节点的代码实现
class SingleNode(object):
    """链表结点实现"""
        def __init__(self, item):
                # item: 存放元素
                self.item = item
                # next: 标识下一个结点
                self.next = None

如果 node 是一个结点:
获取结点元素 : node.item
获取下一个结点 : node.next

  • 单链表的代码实现
class SingleLinkList(object):
    """单链表的实现"""
    def __init__(self, node=None):
           # 首结点
           self.head = node

添加其他功能:is_empty(self) 链表是否为空
length(self) 链表长度
travel(self. ) 遍历整个链表
add(self, item) 链表头部添加元素
append(self, item) 链表尾部添加元素
insert(self, pos, item) 指定位置添加元素
remove(self, item) 删除节点
search(self, item) 查找节点是否存在

链表判空、长度、遍历
  • 判空
class SingleLinkList(object):
    """单链表的实现"""
    def __init__(self, node=None):
            # 首结点
            self.head = node
    def is_empty(self):
            # 判断链表是否为空
                if self.head == None:
                        return True        
                else:            
                	return False
  • 长度
# 获取链表长度
def length(self):
    # 游标记录当前所在的位置
    cur = self.head
    # 记录链表的长度
    count = 0    
    while cur is not None:
        cur = cur.next
        count += 1
  • 遍历
# 遍历链表
def travel(self):
    cur = self.head
    while cur is not None:
        print(cur.item)
        cur = cur.next
链表增加节点
  • 头部添加
# 头部增加结点
def add(self, item):
    # 新结点存储新数据
    node = SingleNode(item)
    node.next = self.head
    self.head = node
  • 尾部添加
# 尾部增加结点
def append(self, item):
    # 新结点存储新数据
    node = SingleNode(item)
    if self.is_empty():
        self.head = node
    else:        
    	cur = self.head
	    while cur.next is not None:
	        cur = cur.next
	        # 当前结点后面连接新结点
	    cur.next = node
  • 指定位置添加
# 指定位置增加结点
def insert(self, pos, item):
    # 头部添加新结点
    if pos <= 0:
    	self.add(item)
    # 尾部添加新结点
    elif pos >= self.length():
	    self.append(item)    
    # 添加新结点
    else:       
        # 游标
        cur = self.head
        # 计数
        count = 0
        # 新结点
        node = SingleNode(item)
	    # 1 找到插入位置的前一个结点
	    while count < pos-1:
	    	cur = cur.next
	    	count += 1      
	    # 2 完成插入新结点        
	    node.next = cur.next        
	    cur.next = node
链表删除和查找节点
  • 删除节点
# 删除结点
def remove(self, item):
    # 游标
    cur = self.head
    # 辅助游标(指向前一个结点的游标)
    pre = None
    while cur is not None:        
	    # 找到要删除的结点       
	    if cur.item == item:
	    # 若删除是头结点
			if cur == self.head:
		        self.head = cur.next
		    else:
		        pre.next = cur.next
		    return        
		# 没有找到要删除的元素
		else:
		    pre = cur            
		    cur = cur.next
  • 查找节点是否存在
# 查找结点是否存在
def search(self, item):
	# 游标
	cur = self.head
	while cur is not None:
	    if cur.item == item:
	        return True
	    cur = cur.next
	return False
顺序表和链表的比较

比较
链表失去了顺序表随机读取的优点,链表由于增加了结点的连接域,空间开销比较大,但对存储空间的使用要相对灵活。
复杂度对比:
复杂度

非线性结构 —— 树
介绍

数据结构分为线性结构和非线性结构
线性结构特点:
①集合中必存在唯一的一个"第一个元素”
②集合中必存在唯一的一个"最后的元素"
③除最后元素之外,其它数据元素均有唯一的"后继"
④除第一元素之外,其它数据元素均有唯一的"前驱”
数据结构中线性结构指的是数据元素之间存在着“一对一”的线性关系的数据结构
非线性结构的逻辑特征是一个结点元素可能对应多个直接前驱和多个后继

树(tree)就是一种非线性结构,它是用来模拟具有树状结构性质的数据集合. 它是由n(n>=1)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的. 它具有以下的特点:
①每个节点有零个或多个子节点
②没有父节点的节点称为根节点
③每一个非根节点有且只有一个父节点
④除了根节点外,每个子节点可以分为多个不相交的子树

对树的描述术语

节点的度:一个节点含有的子节点的个数称为该节点的度
树的度:一棵树中,最大的节点的度称为树的度
叶节点或终端节点:度为零的节点
父亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点
兄弟节点:具有相同父节点的节点互称为兄弟节点
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推
树的高度或深度:树中节点的最大层次
堂兄弟节点:父节点在同一层的节点互为堂兄弟
节点的祖先:从根到该节点所经分支上的所有节点
子孙:以某节点为根的子树中任一节点都称为该节点的子孙
森林:由m(m>=0)棵互不相交的树的集合称为森林

树的分类及存储
分类

无序树:树中任意节点的子节点之间没有顺序关系,这种树称为无序树,也称为自由树
有序树:树中任意节点的子节点之间有顺序关系,这种树称为有序树

有序树:
霍夫曼树(用于信息编码):带权路径最短的二叉树称为哈夫曼树或最优二叉树
B树:一种对读写操作进行优化的自平衡的二叉查找树,能够保持数据有序,拥有多于两个的子树
二叉树:每个节点最多含有两个子树的树称为二叉树(重点介绍)

二叉树的种类

完全二叉树:对于一颗二叉树,假设其深度为d(d>1)。除了第d层外,其它各层的节点数目均已达最大值,且第d层所有节点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树,其中满二叉树的定义是所有叶节点都在最底层的完全二叉树
完全二叉树
平衡二叉树(AVL树):当且仅当任何节点的两棵子树的高度差不大于1的二叉树
为什么需要平衡二叉树:防止树退化为链表
平衡二叉树
排序二叉树(二叉查找树(英语:Binary Search Tree),也称二叉搜索树、有序二叉树)

排序二叉树(BST)的要求:
1.若左子树不空,则左子树上所有节点的值均小于它的根节点的值
2.若右子树不空,则右子树上所有节点的值均大于它的根节点的值
3.左、右子树也分别为二叉排序树
排序二叉树包含空树

注1:一般称为二叉排序树
注2:中序遍历排序二叉树,会得到一个有序的序列
排序二叉树

各类二叉树的作用

满二叉树:层次存储的时候,从左到右控制节点产生
平衡二叉树:防止树变成链表
排序二叉树:对数据排序,检索起来速度快。比如查找4

二叉树的存储

顺序存储:将二叉树存储在固定的数组中,虽然在遍历速度上有一定的优势,但因所占空间比较大,是非主流二叉树
存储方式.二叉树通常以链式存储
链式存储:由于对节点的个数无法掌握,常见树的存储表示都转换成二叉树进行处理,子节点个数最多为2

树的应用场景

①xml,html等,那么编写这些东西的解析器的时候,不可避免用到树
②路由协议就是使用了树的算法
③mysql数据库索引
④文件系统的目录结构
⑤所以很多经典的AI算法其实都是树搜索,此外机器学习中的decision tree也是树结构

二叉树的概念和性质

二叉树是每个节点最多有两个子树的树结构
通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)

性质1: 在二叉树的第i层上至多有 2i-1 个结点(i>0) eg:第3层最多结点个数 2^(3−1)
性质2: 深度为k的二叉树至多有2k - 1个结点(k>0) eg: 层次2^(3)−1= 7
性质3: 对于任意一棵二叉树,如果其叶结点数为N_0,而度数为2的结点总数为N_2 ,则N_0 = N_2+1
性质4: 最多有n个结点的完全二叉树的深度必为 log2(n+1)
性质5: 对完全二叉树,若从上至下、从左至右编号,则编号为i 的结点,其左孩子编号必为2i,其右孩子编号必为2i+1 , 其父节点的编号必为i//2(i=1 时为根,除外)

二叉树的广度优先遍历

广度优先可以找到最短路径:
相当于层次遍历,先把第1层给遍历完,看有没有终点;再把第2层遍历完,看有没有重点

二叉树的深度优先遍历

深度优先往往可以很快找到搜索路径:
比如:先找一个结点看看是不是终点,若不是继续往深层去找,直到找到终点。、中序,先序,后序属于深度优先算法

先序遍历: 根 左 右
中序遍历: 左 根 右
后序遍历: 左 右 根

二叉树遍历

知道中序遍历 和 先序遍历 或者 后序遍历 就可以推出二叉树的结构

算法

排序

介绍
  • 所谓排序,是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作
  • 排序算法,就是如何使得记录按照要求排列的方法
  • 排序算法在很多领域是非常重要
    • 在大量数据的处理方面:一个优秀的算法可以节省大量的资源。
    • 在各个领域中考虑到数据的各种限制和规范,要得到一个符合实际的优秀算法,得经过大量的推理和分析
算法的稳定性

1、假定在待排序的记录序列中,存在多个具有相同的关键字的记录
2、若经过排序,这些记录的相对次序保持不变,则称这种排序算法是稳定的, 否则称为不稳定的
记忆:具有相同关键字的纪录经过排序后,相对位置保持不变,这样的算法是稳定性算法

不稳定的排序算法: 选择排序、快速排序、希尔排序、堆排序
稳定的排序算法: 冒泡排序、插入排序、归并排序和基数排序

冒泡排序
思想

冒泡排序:重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成

这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”

记忆:
相邻位置两个元素比较, 前面的元素比后面的元素大则交换, 把最大的数给找到经
过一轮一轮的比较最终把序列给排序

步骤
过程:
    1. 比较相邻的两个元素,如果第一个比第二个大,就交换他们的位置。
    2. 对每一对相邻的元素做同样的工作,从开始第一对到结尾的最后一对,最后的元素会是最大的数。
    3. 针对所有的元素重复以上的步骤,除了最后一个。
    4. 重复以上的步骤,直到没有任何一对数字需要比较。

冒泡排序举例:
冒泡排序过程

代码实现
# 1: 定义函数bubble_sort(my_list)
def bubble_sort(my_list):
    # 1.1:获取列表的长度
    n = len(my_list)
    # 1.2:外层循环,比较的总轮数
    for i in range(n-1):
        # 定义变量,记录每轮交换的次数。
        count = 0
        # 1.3: 内层循环,每轮比较的次数
        for j in range(n-1-i):
            # 1.4:谁和谁比较:    my_list[j]和my_list[j+1]
            if my_list[j] > my_list[j+1]:
                # 1.5: 如果 前面的元素比后面的元素大,则交换位置
                my_list[j], my_list[j+1] = my_list[j+1], my_list[j]
                # 1.6: 记录交换的次数
                count += 1

        if count == 0:
            # 1.7: 如果没有交换,则提前结束
            break

        print(f"第{i+1}次交换了{count}次")

if __name__ == '__main__':
    my_list = [5, 3, 4, 7, 2]
    print("排序前:", my_list)
    bubble_sort(my_list)
    print("排序后:", my_list)
复杂度与稳定性
  • 复杂度:
    最差时间复杂度 : O(n2)
    最优时间复杂度 : O(n)
  • 稳定性
    遍历一遍发现没有任何元素发生了位置交换,终止排序
    算法稳定性 : 稳定算法
选择排序
思想

选择排序的工作原理:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾. 以此类推,直到全部待排序的数据元素的个数为零。

记忆:选择排序就是把符合要求的数据选择出来进行排序

步骤

假定列表长度为5:

比较轮数每轮比较次数谁(索引)和谁(索引)比较外循环次数 (i)内循环次数 (j)
040 和 1, 0 和 2, 0 和 3, 0 和 401–4
131 和 2, 1 和 3, 1 和 412–4
222 和 3, 2 和 423–4
313 和 434

选择排序举例:
选择排序过程

代码实现
# 1: 定义函数,select_sort(my_list)
def select_sort(my_list):
    # 1.1: 获取列表的长度
    n = len(my_list)
    # 1.2: 外层循环,比较的总轮数
    for i in range(n-1):
        # 定义最小值的索引。
        min_index = i
        # 1.2: 内循环,每轮比较的次数
        for j in range(i+1, n):
            # 1.3: 谁(索引)和谁(索引)比较, 如果当前元素比min_index对应的元素值小,就更新min_index
            if my_list[j] < my_list[min_index]:
                min_index = j

        # 1.4:走到治理,说明1轮的比较完毕,要看min_index和i是不是同一个元素
        if min_index != i:
            # 1.5:如果不是同一个元素,就交换位置
            my_list[i], my_list[min_index] = my_list[min_index], my_list[i]

if __name__ == '__main__':
    my_list = [5, 3, 4, 7, 2]
    print("排序前:", my_list)
    select_sort(my_list)
    print("排序后:", my_list)
复杂度与稳定性
  • 复杂度
    最差时间复杂度 : O(n2)
    最优时间复杂度 : O(n)
  • 稳定性
    算法稳定性 : 不稳定算法
插入排序
思想

插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序

插入排序的组成:
插入算法把要排序的数组分成两部分:第一部分是有序的数字(这里可以默认数组第一个数字为有序的第一部分),第二部分为无序的数字(这里除了第一个数字以外剩余的数字可以认为是无序的第二部分)

步骤
推理过程:
    1:将列表分为两部分,已排序部分和未排序部分。
    2:从未排序部分取出一个元素,插入到已排序部分的合适位置。
    3:重复第二步,直到未排序部分为空。

插入排序举例:
插入排序

代码实现
# 1: 定义函数,insert_sort(my_list)
def insert_sort(my_list):
    # 1.1: 获取列表的长度
    n = len(my_list)
    # 1.2: 外层循环,开始每轮,每次的比较
    for i in range(1, n):
        # 1.3:内循环,从已排序部分的最后一个元素开始,比较到第一个元素
        """
        range(i, 0, -1) 表示从索引i开始递减到1(不包括0)
        每次循环将当前元素与前一个元素比较
        如果当前元素较小,则交换位置
        直到找到正确的位置或到达已排序部分的开头
        """
        for j in range(i, 0, -1):
            # 1.4:具体的比较过程,如果当前元素比前一个元素小,则交换位置
            if my_list[j] < my_list[j-1]:
                my_list[j], my_list[j-1] = my_list[j-1], my_list[j]
            else:
                # 1.5: 如果当前元素比前一个元素大,则提前结束
                break


if __name__ == '__main__':
    my_list = [5, 3, 4, 7, 2]
    print("排序前:", my_list)
    insert_sort(my_list)
    print("排序后:", my_list)
复杂度与稳定性
  • 复杂度
    最差时间复杂度 : O(n2)
    最优时间复杂度 : O(n)
  • 稳定性
    算法稳定性 : 稳定算法

插入排序(Insertion sort)是一种简单直观且稳定的排序算法。
如果有一个已经有序的数据序列,要求在这个已经排好的数据序列中插入一个数,但要求插入后此数据序列仍然有序,这个时候就可以使用插入排序法
插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序

快速排序
思想

基本思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列

步骤
	快速排序算法通过多次比较和交换来实现排序,其排序流程如下:
		1. 首先设定一个分界值,通过该分界值将数组分成左右两部分
		2. 将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边
	此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值 
		3. 然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,
	将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也做类似处理
		4. 重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序
	当左、右两个部分各数据排序完成后,整个数组的排序也就完成了

快速1
快速2
快速3
快速4

代码实现
def quick_sort_simple(arr):
    """
    快速排序 - 简单写法
    思想:选择一个基准元素,将数组分为两部分,一部分小于基准,一部分大于基准,递归处理两部分
    """
    # 递归终止条件:数组长度小于等于1时直接返回
    if len(arr) <= 1:
        return arr
    
    # 选择基准元素(这里选择第一个元素)
    pivot = arr[0]
    
    # 分割数组:小于基准的放左边,等于基准的放中间,大于基准的放右边
    left = [x for x in arr[1:] if x < pivot]      # 小于基准的元素
    middle = [x for x in arr if x == pivot]       # 等于基准的元素
    right = [x for x in arr[1:] if x > pivot]     # 大于基准的元素
    
    # 递归排序左右两部分,并合并结果
    return quick_sort_simple(left) + middle + quick_sort_simple(right)


# 测试简单写法
if __name__ == "__main__":
    test_array = [64, 34, 25, 12, 22, 11, 90]
    print("原数组:", test_array)
    sorted_array = quick_sort_simple(test_array)
    print("排序后:", sorted_array)

当然这样写会在执行过程中不断的产生新数组,比较浪费资源,下面介绍一种原地快速排序的方法

def quick_sort_inplace(arr, low=0, high=None):
    """
    原地快速排序实现
    
    参数:
        arr: 待排序的数组
        low: 排序区间的起始索引
        high: 排序区间的结束索引
    """
    if high is None:
        high = len(arr) - 1

    if low < high:
        # 获取分区点
        pivot_index = partition(arr, low, high)
        
        # 递归排序左半部分
        quick_sort_inplace(arr, low, pivot_index - 1)
        
        # 递归排序右半部分
        quick_sort_inplace(arr, pivot_index + 1, high)


def partition(arr, low, high):
    """
    分区函数:将数组分为两部分
    
    参数:
        arr: 数组
        low: 起始索引
        high: 结束索引
    
    返回:
        基准元素的最终位置索引
    """
    pivot = arr[high]  # 选择最后一个元素作为基准
    i = low - 1  # 指向小于基准区域的边界

    for j in range(low, high):
        # 如果当前元素小于等于基准元素
        if arr[j] <= pivot:
            i += 1
            arr[i], arr[j] = arr[j], arr[i]  # 交换元素

    # 将基准放到正确位置
    arr[i + 1], arr[high] = arr[high], arr[i + 1]
    
    return i + 1  # 返回基准的最终位置


# 测试执行
if __name__ == "__main__":
    # 测试原地排序版本
    test_list1 = [3, 6, 8, 10, 1, 2, 9, 4, 7, 5]
    print("原始数组:", test_list1)
    quick_sort_inplace(test_list1)
    print("原地排序结果:", test_list1)
    
复杂度与稳定性
  • 复杂度
    最差时间复杂度 : O(n2)
    最优时间复杂度 : O(nlogn)
  • 稳定性
    算法稳定性 :不稳定算法

二分查找

介绍

二分查找又称折半查找,它是一种效率较高的查找方法

原理:将数组分为三部分,依次是中值前,中值,中值后将要查找的值与中值进行比较,若小于中值则在中值前面找,若大于中值则在中值后面找,等于中值时直接返回。

注意
①必须采用顺序存储结构 (直接地址偏移就可以检索到数值 O(1)
②必须按关键字大小有序排列
时间复杂度
最差时间复杂度: O(log n)
最优时间复杂度: O(1)

代码实现
递归实现
def binary_search_optimized(alist, item, low=0, high=None):
    """二分查找"""
    if high is None:
        high = len(alist) - 1

    if low > high:
        return False

    mid = (low + high) // 2

    if item == alist[mid]:
        return True
    elif item < alist[mid]:
        return binary_search_optimized(alist, item, low, mid - 1)
    else:
        return binary_search_optimized(alist, item, mid + 1, high)

if __name__ == '__main__':
    # 测试1:要查找的数字70
    alist = [8, 22, 31, 32, 44, 55, 56, 70, 78]
    result = binary_search_optimized(alist, 70)
    print(result)

非递归实现
def binary_search(alist, item):
    # 1 设置初始化搜索空间 起始位置start 结束位置end
    start = 0
    end = len(alist) - 1

    # 2 循环检索
    while start <= end:
        # 2-1 获取中间值索引 mid / (start+end)//2
        mid = (start + end) // 2

        # 2-2 item等于中间值 返回True
        if item == alist[mid]:
            return True
        # 2-3 item小于中间值 在前半部空间搜索 修改搜索空间 end /mid - 1
        elif item < alist[mid]:
            end = mid - 1
        # 2-4 item大于中间值 在后半部空间搜索 修改搜索空间 start /mid + 1
        elif item > alist[mid]:
            start = mid + 1

    # 如果未找到,返回False
    return False

if __name__ == '__main__':
    # 测试1:要查找的数字70
    alist = [8, 22, 31, 32, 44, 55, 56, 70, 78]
    result = binary_search(alist, 70)
    print(result)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值