一、数据结构与算法简介
1、什么是数据结构?什么是算法?
数据结构就是指一组数据的存储结构。算法就是操作数据的一组方法。
2、数据结构和算法的关系?
数据结构和算法是相辅相成的,数据结构是为算法服务的,算法要作用在特定的数据结构之上。 数据结构是静态的,它只是组织数据的一种方式。如果不在它的基础上操作、构建算法,孤立存在的数据结构就是没用的。
比如,因为数组具有随机访问的特点,常用的二分查找算法需要用数组来存储数据。但如果我们选择链表这种数据结构,二分查找算法就无法工作了,因为链表并不支持随机访问。
二、算法
1、算法的概念
算法是计算机处理信息的本质,因为计算机程序本质上是一个算法来告诉计算机确切的步骤来执行一个指定的任务。一般地,当算法在处理信息时,会从输入设备或数据的存储地址读取数据,把结果写入输出设备或某个存储地址供以后再调用。
算法是独立存在的一种解决问题的方法和思想。对于算法而言,实现的语言并不重要,重要的是思想。
算法可以有不同的语言描述实现版本(如C描述、C++描述、Python描述等),我们现在是在用Python语言进行描述实现。
算法的五大特性
输入: 算法具有0个或多个输入;
输出: 算法至少有1个或多个输出;
有穷性: 算法在有限的步骤之后会自动结束而不会无限循环,并且每一个步骤可以在可接受的时间内完成;
确定性:算法中的每一步都有确定的含义,不会出现二义性;
可行性:算法的每一步都是可行的,也就是说每一步都能够执行有限的次数完成。
举例:
'''
如果 a+b+c=2000,且 a^2+b^2=c^2(a,b,c 为自然数),如何求出所有a、b、c可能的组合?
枚举法
a = 0
b = 0
c = 1000
a**2 + b**2 + c**2 = 1000
a = 1
b = 2
c = 1000 - a - b
'''
import time
start_time = time.time()
for a in range(0, 1001):
for b in range(0, 1001):
for c in range(0, 1001):
if a**2 + b**2 == c**2 and a + b + c == 1000:
print("a, b, c:%d,%d,%d" % (a, b, c))
# 每台机器执行的的总时间不同
# 执行的基本运算的数量大体是相同
# T = 1000 * 1000 * 1000 * 2
# T = 2000 * 2000 * 2000 * 2
# 1000换成N
# T = N * N * N *2 = N^3 * 2
# T(n) = N^3 * 2
# f(n) = N^3
# T(n) = f(n)*2
# T(n) = O(f(n))
# T(n)表示代码执行的时间
# n表示数据规模的大小
# f(n)表示每行代码执行的次数总和
#公式中的 O,表示代码的执行时间 T(n) 与 f(n) 表达式成正比
for a in range(0, 1001):
for b in range(0, 1001):
c = 1000 - a - b
if a**2 + b**2 == c**2:
print("a, b, c:%d,%d,%d" % (a, b, c))
end_time = time.time()
print("time:%f" % (end_time - start_time)) # 0.825315 不到一秒
print("end")
2、大O复杂度表示法
所有代码的执行时间 T(n) 与每行代码的执行次数 n 成正比。
公式:
T(n) = O(f(n))
参数:
T(n)表示代码执行的时间
n表示数据规模的大小
f(n)表示每行代码执行的次数总和
O表示代码的执行时间 T(n) 与 f(n) 表达式成正比
大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以,也叫作渐进时间复杂度(asymptotic time complexity),简称时间复杂度。
当n很大时,你可以把它想象成 10000、100000。而公式中的低阶、常量、系数三部分并不左右增长趋势,所以都可以忽略。我们只需要记录一个最大量级就可以了
3、时间复杂度分析
(1)只关注循环执行次数最多的一段代码
大 O 这种复杂度表示方法只是表示一种变化趋势。我们通常会忽略掉公式中的常量、低阶、系数,只需要记录一个最大阶的量级就可以了。所以,我们在分析一个算法、一段代码的时间复杂度的时候,也只关注循环执行次数最多的那一段代码就可以了。这段核心代码执行次数的 n 的量级,就是整段要分析代码的时间复杂度。
def cal(n):
sum = 0
i = 1
for i in range(n):
sum += i
i += 1
return sum
cal(4)
# sum=0和i=1 常量级的
# n次
# T(n) = O(n)
'''
其中第 2、3 行代码都是常量级的执行时间,与 n 的大小无关,所以对于复杂度并没有影响。
循环执行次数最多的是第 4、5 行代码,所以这块代码要重点分析。前面我们也讲过,这两行代码被执行了 n,所以总的时间复杂度就是 O(n)。
'''
(2)加法法则:总复杂度等于量级最大的那段代码的复杂度
def cal(n):
sum = 0
i = 1
for i in range(100):
sum += i
for i in range(n + 1):
sum += i
i += 1
for i in range(n + 1):
for j in range(n + 1):
pass
# O(1) O(n) O(n^2)
# T(n) = max(O(1) + O(n) + O(n^2)) = O(n^2)
'''
综合这三段代码的时间复杂度,我们取其中最大的量级。所以,整段代码的时间复杂度就为 O(n2)。也就是说:总的时间复杂度就等于量级最大的那段代码的时间复杂度
'''
4、最坏时间复杂度
分析算法时,存在几种可能的考虑:
(1)算法完成工作最少需要多少基本操作,即最优时间复杂度
(2)算法完成工作最多需要多少基本操作,即最坏时间复杂度
(3)算法完成工作平均需要多少基本操作,即平均时间复杂度
对于列表排序,要查找的变量 x 可能出现在数组的任意位置。如果数组中第一个元素正好是要查找的变量 x,那就不需要继续遍历剩下的 n-1 个数据了,那时间复杂度就是 O(1)。
但如果数组中不存在变量 x,那我们就需要把整个数组都遍历一遍,时间复杂度就成了 O(n)。所以,不同的情况下,这段代码的时间复杂度是不一样的。
对于最优时间复杂度,其价值不大,因为它没有提供什么有用信息,其反映的只是最乐观最理想的情况,没有参考价值。
对于最坏时间复杂度,提供了一种保证,表明算法在此种程度的基本操作中一定能完成工作。
对于平均时间复杂度,是对算法的一个全面评价,因此它完整全面的反映了这个算法的性质。但另一方面,这种衡量并没有保证,不是每个计算都能在这个基本操作内完成。而且,对于平均情况的计算,也会因为应用算法的实例分布可能并不均匀而难以计算。
因此,我们主要关注算法的最坏情况,亦即最坏时间复杂度。
常见时间复杂度
常见的复杂度并不多,从低阶到高阶有:O(1)、O(logn)、O(n)、O(nlogn)、O(n2)。
执行次数函数举例 | 阶 | 非正式术语 |
12 | O(1) | 常数阶 |
2n + 3 | O(n) | 线性阶 |
3n^2 + 3n + 1 | O(n^2) | 平方阶 |
log2n + 20 | O(logn) | 对数阶 |
2n+3nlog2n+19 | O(nlogn) | nlogn阶 |
2^n | O(2^n) | 指数阶 |
四、Python内置类型性能分析
代码执行时间测量模块
li = []
li.append()
li.insert()
timeit模块
timeit模块可以用来测试一小段Python代码的执行速度。
class timeit.Timer(stmt=‘pass’, setup=‘pass’, timer=)
Timer是测量小段代码执行速度的类。
stmt参数是要测试的代码语句(statment);
setup参数是运行代码时需要的设置;
timer参数是一个定时器函数,与平台有关。
timeit.Timer.timeit(number=1000000)
Timer类中测试语句执行速度的对象方法。number参数是测试代码时的测试次数,默认为1000000次。方法返回执行代码的平均耗时,一个float类型的秒数
from timeit import Timer
def ts1():
l = []
for i in range(1000):
l = l + [i]
def ts2():
l = []
for i in range(1000):
l.append(i) # 追加 末尾
def ts3():
# 列表推导式
l = [i for i in range(1000)]
def ts4():
l = list(range(1000))
def ts5():
l = []
for i in range(1000):
l.insert(0, i) # 从头开始添加
# 当前的文件的名字 from __main__ import ts1
t1 = Timer("ts1()", "from __main__ import ts1")
print("add",t1.timeit(number=1000)) #add 1.4652585439999999
t2 = Timer("ts2()", "from __main__ import ts2")
print("append",t2.timeit(number=1000))#append 0.066084861
t3 = Timer("ts3()", "from __main__ import ts3")
print("list derivation",t3.timeit(number=1000))#list derivation 0.036144565999999934
t4 = Timer("ts4()", "from __main__ import ts4")
print("list range",t4.timeit(number=1000))#list range 0.014681925999999956
t5 = Timer("ts5()", "from __main__ import ts5")
print("insert",t5.timeit(number=1000))#insert 0.545034354
# int float char=>str
五、数据结构
我们如何用Python中的类型来保存一个班的学生信息?
实际上当我们在思考这个问题的时候,我们已经用到了数据结构。列表和字典都可以存储一个班的学生信息;
但是想要在列表中获取一名同学的信息时,就要遍历这个列表,其时间复杂度为O(n);
而使用字典存储时,可将学生姓名作为字典的键,学生信息作为值,进而查询时不需要遍历便可快速获取到学生信息,其时间复杂度为O(1)。
'''
我们如何用Python中的类型来保存一个班的学生信息?
'''
'''
{
'Alex':{
'age':18,
'address':csc
}
}
sts['Alex'] O(1)
[
('Alex',18,'csc'),
('Alex',18,'csc'),
('Alex',18,'csc'),
]
for i in sts:
if xxx == 'juran':
pass
O(n)
[
{
'name':'Alex'
'age':18
'address':csc
}
]
'''
我们为了解决问题,需要将数据保存下来,然后根据数据的存储方式来设计算法实现进行处理,那么数据的存储方式不同就会导致需要不同的算法进行处理。我们希望算法解决问题的效率越快越好,于是我们就需要考虑数据究竟如何保存的问题,这就是数据结构。
1、概念
数据是一个抽象的概念,将其进行分类后得到程序设计语言中的基本类型。如:int,float,char等。数据元素之间不是独立的,存在特定的关系,这些关系便是结构。数据结构指数据对象中数据元素之间的关系。
Python给我们提供了很多现成的数据结构类型,这些系统自己定义好的,不需要我们自己去定义的数据结构叫做Python的内置数据结构,比如列表、元组、字典。而有些数据组织方式,Python系统里面没有直接定义,需要我们自己去定义实现这些数据的组织方式,这些数据组织方式称之为Python的扩展数据结构,比如栈,队列等。
2、算法与数据结构的区别
数据结构只是静态的描述了数据元素之间的关系。
高效的程序需要在数据结构的基础上设计和选择算法。
程序 = 数据结构 + 算法
总结:算法是为了解决实际问题而设计的,数据结构是算法需要处理的问题载体
3、抽象数据类型(Abstract Data Type)
抽象数据类型(ADT)的含义是指一个数学模型以及定义在此数学模型上的一组操作。即把数据类型和数据类型上的运算捆在一起,进行封装。引入抽象数据类型的目的是把数据类型的表示和数据类型上运算的实现与这些数据类型和运算在程序中的引用隔开,使它们相互独立。
六、计算机内存对象表示
1、内存单元和地址
计算中(程序中)直接使用的数据保存在计算机的内存储器(简称内存)。内存是CPU可以直接访问的数据存储设备。与之相对应的是外存储器,简称外存,如磁盘、光盘、磁带等。保存在外存里的数据必须先装入内存,而后CPU才能使用它们。
内存的基本结构是线性排列的一批存储单元。每个单元的大小相同,可以保存一个单位大小的数据。具体单元大小可能因计算机的不同而有所不同。在目前最常见的计算机中,一个单元可以保存一个字节(8位二进制码)的数据。因此存放一个整数或浮点数,需要连续的几个单元。例如标准的浮点数需要8个单元。
内存单元具有唯一编号,称为单元地址,或者简称地址。单元地址从0开始连续排列,全部可用地址为从0开始的一个连续的正整数区间。
在程序执行中,对内存单元的访问(存取其中的数据)都通过单元的地址进行,因此,要访问一个单元,必须先掌握其地址。基于地址访问内存单元是一个O(1)操作,与单元的位置或整个内存的大小无关,这是分析与数据结构有关的算法时的一个基本假设。在高级语言层面讨论和分析数据结构问题时,人们通常不关心具体的单元大小或地址范围,只假定所考虑数据保存在内存的某处,而且假定这种访问是常量时间的。
七、顺序表
在程序中,经常需要将一组(通常是同为某个类型的)数据元素作为整体管理和使用,需要创建这种元素组,用变量记录它们,传进传出函数等。一组数据中包含的元素个数可能发生变化(可以增加或删除元素)。
对于这种需求,最简单的解决方案便是将这样一组元素看成一个序列,用元素在序列里的位置和顺序,表示实际应用中的某种有意义的信息,或者表示数据之间的某种关系。
这样的一组序列元素的组织形式,我们可以将其抽象为线性表。一个线性表是某类元素的一个集合,还记录着元素之间的一种顺序关系。线性表是最基本的数据结构之一,在实际程序中应用非常广泛,它还经常被用作更复杂的数据结构的实现基础。
根据线性表的实际存储方式,分为两种实现模型:
顺序表,将元素顺序地存放在一块连续的存储区里,元素间的顺序关系由它们的存储顺序自然表示。
链表,将元素存放在通过链接构造起来的一系列存储块中。
1、顺序表的基本形式
图a表示的是顺序表的基本形式,数据元素本身连续存储,每个元素所占的存储单元大小固定相同,元素的下标是其逻辑地址,而元素存储的物理地址(实际内存地址)可以通过存储区的起始地址Loc (e0)加上逻辑地址(第i个元素)与存储单元大小(c)的乘积计算而得,即:
Loc(ei) = Loc(e0) + c*i
故,访问指定元素时无需从头遍历,通过计算便可获得对应地址,其时间复杂度为O(1)。
如果元素的大小不统一,则须采用图b的元素外置的形式,将实际数据元素另行存储,而顺序表中各单元位置保存对应元素的地址信息(即链接)。由于每个链接所需的存储量相同,通过上述公式,可以计算出元素链接的存储位置,而后顺着链接找到实际存储的数据元素。注意,图b中的c不再是数据元素的大小,而是存储一个链接地址所需的存储量,这个量通常很小。
图b这样的顺序表也被称为对实际数据的索引,这是最简单的索引结构。
从数组存储的内存模型上来看,“下标”最确切的定义应该是“偏移(offset)”。前面也讲到,如果用 a 来表示数组的首地址,a[0] 就是偏移为 0 的位置,也就是首地址,a[k] 就表示偏移 k 个 type_size 的位置,所以计算a[k] 的内存地址只需要用这个公式:
a[k]_address = base_address + k * type_size
但是,如果数组从 1 开始计数,那我们计算数组元素 a[k] 的内存地址就会变为:
a[k]_address = base_address + (k-1)*type_size
对比两个公式,我们不难发现,从 1 开始编号,每次随机访问数组元素都多了一次减法运算,对于 CPU 来说,就是多了一次减法指令。数组作为非常基础的数据结构,通过下标随机访问数组元素又是其非常基础的编程操作,效率的优化就要尽可能做到极致。所以为了减少一次减法操作,数组选择了从 0 开始编号,而不是从 1 开始。
2、顺序表的结构
一个顺序表的完整信息包括两部分,一部分是表中的元素集合,另一部分是为实现正确操作而需记录的信息,即有关表的整体情况的信息,这部分信息主要包括元素存储区的容量和当前表中已有的元素个数两项。3、顺序表的两种基本实现方式
图a为一体式结构,存储表信息的单元与元素存储区以连续的方式安排在一块存储区里,两部分数据的整体形成一个完整的顺序表对象。
一体式结构整体性强,易于管理。但是由于数据元素存储区域是表对象的一部分,顺序表创建后,元素存储区就固定了。
图b为分离式结构,表对象里只保存与整个表有关的信息(即容量和元素个数),实际数据元素存放在另一个独立的元素存储区里,通过链接与基本表对象关联。
4、元素存储区替换
一体式结构由于顺序表信息区与数据区连续存储在一起,所以若想更换数据区,则只能整体搬迁,即整个顺序表对象(指存储顺序表的结构信息的区域)改变了。
分离式结构若想更换数据区,只需将表信息区中的数据区链接地址更新即可,而该顺序表对象不变。
5、元素存储区扩充
采用分离式结构的顺序表,若将数据区更换为存储空间更大的区域,则可以在不改变表对象的前提下对其数据存储区进行了扩充,所有使用这个表的地方都不必修改。只要程序的运行环境(计算机系统)还有空闲存储,这种表结构就不会因为满了而导致操作无法进行。人们把采用这种技术实现的顺序表称为动态顺序表,因为其容量可以在使用中动态变化。
扩充的两种策略:
(1)每次扩充增加固定数目的存储位置,如每次扩充增加10个元素位置,这种策略可称为线性增长。
特点:节省空间,但是扩充操作频繁,操作次数多。
(2)每次扩充容量加倍,如每次扩充增加一倍存储空间。
特点:减少了扩充操作的执行次数,但可能会浪费空间资源。以空间换时间,推荐的方式。
6、顺序表的操作
(1)增加元素
a. 尾端加入元素,时间复杂度为O(1)
b. 非保序的加入元素(不常见),时间复杂度为O(1)
c. 保序的元素加入,时间复杂度为O(n) # 保序是插入元素,原先的顺序不变(2)删除元素
a. 删除表尾元素,时间复杂度为O(1)
b. 非保序的元素删除(不常见),时间复杂度为O(1)
c. 保序的元素删除,时间复杂度为O(n)
7、list的基本实现技术
Python标准类型list就是一种元素个数可变的线性表,可以加入和删除元素,并在各种操作中维持已有元素的顺序(即保序),而且还具有以下行为特征:
(1)基于下标(位置)的高效元素访问和更新,时间复杂度应该是O(1);
为满足该特征,应该采用顺序表技术,表中元素保存在一块连续的存储区中。
(2)允许任意加入元素,而且在不断加入元素的过程中,表对象的标识(函数id得到的值)不变。
为满足该特征,就必须能更换元素存储区,并且为保证更换存储区时list对象的标识id不变,只能采用分离式实现技术。
在Python的官方实现中,list就是一种采用分离式技术实现的动态顺序表。这就是为什么用list.append(x) (或list.insert(len(list), x),即尾部插入)比在指定位置插入元素效率高的原因。
在Python的官方实现中,list实现采用了如下的策略:在建立空表(或者很小的表)时,系统分配一块能容纳8个元素的存储区;在执行插入操作(insert或append)时,如果元素存储区满就换一块4倍大的存储区。但如果此时的表已经很大(目前的阀值为50000),则改变策略,采用加一倍的方法。引入这种改变策略的方式,是为了避免出现过多空闲的存储位置。
八、链表
1、为什么需要链表
顺序表的构建需要预先知道数据大小来申请连续的存储空间,而在进行扩充时又需要进行数据的搬迁,所以使用起来并不是很灵活。
链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。
2、链表的定义
链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是不像顺序表一样连续存储数据,而是在每一个节点(数据存储单元)里存放下一个节点的位置信息(即地址)。
3、单向链表
单向链表也叫单链表,是链表中最简单的一种形式,它的每个节点包含两个域,一个信息域(元素域)和一个链接域。这个链接指向链表中的下一个节点,而最后一个节点的链接域则指向一个空值。
(1)表元素域elem用来存放具体的数据。
(2)链接域next用来存放下一个节点的位置(python中的标识)。
(3)变量p指向链表的头节点(首节点)的位置,从p出发能找到表中的任意节点。
'''
节点的实现
'''
class SingleNode(object):
"""单链表的节点"""
def __init__(self, item):
# item存放数据元素
self.item = item
# next是下一个节点的标识
self.next = None
单链表的操作:
is_empty() :链表是否为空
length() :链表长度
travel() :遍历整个链表
add(item) :链表头部添加元素
append(item) :链表尾部添加元素
insert(pos, item) :指定位置添加元素
remove(item) :删除节点
search(item) :查找节点是否存在
单链表的实现
class SingleLinkList(object):
"""单链表"""
# 空的单链表 self.__head = None
# 非空的单链表 self.__head = node
def __init__(self, node=None):
self.__head = node #首节点的地址,是私有的,不希望在外面被调用
def is_empty(self):
"""链表是否为空"""
if self.__head == None:
return True
else:
return False
# return self.__head == None
def length(self):
"""链表长度"""
# 空链表的情况下
# cur游标 指向首节点 用来遍历
cur = self.__head # None
count = 0 # count = 1 正常情况下 空链表的情况下 count如果等于1的话 链表长度不对
while cur != None:
count += 1
# 将游标后移动一位
cur = cur.next
return count
def travel(self):
"""遍历整个链表"""
cur = self.__head
while cur != None:
# 节点中的数据
print(cur.item, end=' ')
cur = cur.next
print("")
def append(self, item):
"""链表尾部添加元素"""
# item 你要具体插入的数据
node = SingleNode(item)
# 考虑空链表
if self.is_empty():
self.__head = node
else:
cur = self.__head # None node
# AttributeError: 'NoneType' object has no attribute 'next'
while cur.next != None:
# 找到最后一个节点
cur = cur.next
cur.next = node
def add(self, item):
"""链表头部添加元素"""
# item 你要具体插入的数据
node = SingleNode(item)
# 将新节点的链接区next指向头结点
node.next = self.__head
# 将链表的头指向新节点
self.__head = node
def insert(self, pos, item): # insert(2,100) insert(-1,100) insert(10,100)
"""指定位置添加元素"""
# item 你要具体插入的数据
# 头部插入
if pos <= 0:
self.add(item)
# 尾部插入
elif pos > self.length()-1:
self.append(item)
else:
# 指定位置插入
node = SingleNode(item)
pre = self.__head
count = 0
while count < (pos - 1):
count += 1
pre = pre.next
node.next = pre.next
pre.next = node
def remove(self, item):
"""删除节点"""
# remove(100) [100,200,100] 删除一个100
cur = self.__head # 空链表 None
pre = None
while cur != None:
# 找到了指定的元素
if cur.item == item:
# 删除第一个元素
if cur == self.__head:
self.__head = cur.next
else:
# 删除
pre.next = cur.next
break
else:
# 继续移动
pre = cur
cur = cur.next
def search(self, item):
"""查找节点是否存在"""
cur = self.__head
while cur != None:
if cur.item == item:
return True
else:
# 让游标继续执行
cur = cur.next
return False
# 有一个节点,存放的数据是100
# 测试
s = SingleLinkList()
print(s.is_empty())
print(s.length())
循环链表的实现
头部添加:
class Node(object):
"""节点"""
def __init__(self, item):
# item存放数据元素
self.item = item
# next是下一个节点的标识
self.next = None
class SinCycLinkList(object):
"""单项循环链表"""
# 空的单链表 self.__head = None
# 非空的单链表 node.next = node
def __init__(self, node=None):
self.__head = None
if node:
node.next = node
def is_empty(self):
"""判断链表是否为空"""
if self.__head == None:
return True
else:
return False
def length(self):
"""返回链表的长度"""
# 空链表的情况下
if self.is_empty():
return 0
# cur游标 指向首节点 用来遍历
cur = self.__head # None
# is 对象 == 数值是否相等
count = 1
while cur.next != self.__head:
count += 1
# 将游标后移动一位
cur = cur.next
return count
def travel(self):
"""遍历"""
# 空链表
if self.is_empty():
return None
cur = self.__head
while cur.next != self.__head:
# 节点中的数据
print(cur.item, end=' ')
cur = cur.next
# 退出循环的时候,cur指向尾结点,但是尾结点并没有打印
print(cur.item)
def add(self, item):
"""链表头部添加元素"""
# item 你要具体插入的数据
node = Node(item)
# 空链表
if self.is_empty():
self.__head = node
node.next = self.__head
else:
cur = self.__head # None
while cur.next != self.__head:
cur = cur.next
# 找到尾节点
# 添加的节点指向原先的头节点
node.next = self.__head
# __head指向node
self.__head = node
# 尾结点指向新的节点
cur.next = node
def append(self, item):
"""链表尾部添加元素"""
node = Node(item)
# 空链表
if self.is_empty():
self.__head = node
node.next = self.__head
else:
cur = self.__head
while cur.next != self.__head:
cur = cur.next
# 找到尾节点
# 将尾结点指向node
cur.next = node
# 将node指向__head
node.next = self.__head
def insert(self, pos, item):
"""指定位置添加元素"""
# 头部插入
if pos <= 0:
self.add(item)
# 尾部插入
elif pos > self.length() - 1:
self.append(item)
else:
# 指定位置插入
node = Node(item)
cur = self.__head
count = 0
while count < (pos - 1):
count += 1
cur = cur.next
node.next = cur.next
cur.next = node
def search(self, item):
"""查找节点是否存在"""
if self.is_empty():
return False
cur = self.__head
while cur.next != self.__head:
if cur.item == item:
return True
else:
# 让游标继续执行
cur = cur.next
# 循环退出 cur指向尾节点
if cur.item == item:
return True
return False
def remove(self, item):
"""删除节点"""
# 空链表
if self.is_empty():
return
cur = self.__head
pre = None
# 头节点的元素就是要删除的元素
if cur.item == item:
# 链表中的节点不止一个
if cur.next != self.__head:
while cur.next != self.__head:
cur = cur.next
# 循环结束 cur 指向尾结点
cur.next = self.__head.next
self.__head = cur.next
else:
self.__head = None
# 第一个节点不是要删除的
else:
while cur.next != self.__head:
if cur.item == item:
# 删除
pre.next = cur.next
return
else:
# 游标继续往下走
pre = cur
cur = cur.next
if cur.item == item:
pre.next = cur.next
s = SinCycLinkList()
# print(s.is_empty())
# print(s.length())
# s.add(1) # add头部添加 [4321]
# s.add(2)
# s.add(3)
# s.add(4)
s.append(6)
s.insert(2, 100)
s.insert(-1, 200)
s.insert(2, 300)
# s.append(8)
# print(s.search(100))
# print(s.search(400))
s.remove(6)
s.remove(100)
s.remove(300)
s.travel()
# print(s.is_empty())
# print(s.length())
双向链表的实现
指定位置插入:
class Node(object):
"""节点"""
def __init__(self, item):
# item存放数据元素
self.item = item
# next是下一个节点的标识
self.next = None
# perv是上一个节点的表示
self.prev = None
class DoubleLinkList(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):
"""返回链表的长度"""
# 空链表的情况下
if self.is_empty():
return 0
# cur游标 指向首节点 用来遍历
cur = self.__head
# is 对象 == 数值是否相等
count = 0
while cur != None:
count += 1
# 将游标后移动一位
cur = cur.next
return count
def travel(self):
"""遍历"""
# 空链表
if self.is_empty():
return None
cur = self.__head
while cur != None:
# 节点中的数据
print(cur.item, end=' ')
cur = cur.next
print("")
# 退出循环的时候,cur指向None
def add(self, item):
"""链表头部添加元素"""
# item 你要具体插入的数据
node = Node(item)
if self.is_empty():
self.__head = node
else:
# 将node的next指向__head的头节点
node.next = self.__head
# 将__head的头节点的prev指向node
self.__head.prev = node
# 将__head指向node
self.__head = node
def append(self, item):
"""链表尾部添加元素"""
# item 你要具体插入的数据
node = Node(item)
# 考虑空链表
if self.is_empty():
self.__head = node
else:
cur = self.__head # None node
while cur.next != None:
# 找到最后一个节点
cur = cur.next
# 循环结束之后,cur指向尾节点
cur.next = node
node.prev = cur
def insert(self, pos, item): # insert(2,100) insert(-1,100) insert(10,100)
"""指定位置添加元素"""
# item 你要具体插入的数据
# 头部插入
if pos <= 0:
self.add(item)
# 尾部插入
elif pos > self.length()-1:
self.append(item)
else:
# 指定位置插入
node = Node(item)
cur = self.__head
count = 0
while count < (pos - 1):
count += 1
cur = cur.next
# 循环结束,找到插入位置的前一个结点
# 将node的prev指向cur
node.prev =cur
# 将node的next指向cur的下一个结点
node.next = cur.next
# 将cur的下一个结点的prev指向node
cur.next.prev = node
# 将cur的next指向node
cur.next = node
def search(self, item):
"""查找节点是否存在"""
cur = self.__head
while cur != None:
if cur.item == item:
return True
else:
# 让游标继续执行
cur = cur.next
return False
def remove(self, item):
"""删除节点"""
# remove(100) [100,200,100] 删除一个100
if self.is_empty():
return
else:
cur = self.__head # 空链表 None
# 如果首结点的元素就是要删除的元素
if cur.item == item:
print(1)
# 如果链表只有一个结点
if cur.next == None:
self.__head = None
else:
cur.next.prev = None
self.__head = cur.next
return
while cur != None:
if cur.item == item:
print(2)
cur.prev.next = cur.next
if cur.next:
cur.next.prev = cur.prev
break
cur = cur.next
if __name__ == '__main__':
d = DoubleLinkList()
print(d.is_empty())
print(d.length())
d.add(1)
d.add(2)
d.add(3)
d.append(4)
d.append(200)
d.insert(2, 300)
d.insert(-2, 400)
d.insert(20, 700)
# print(d.search(700))
# print(d.search(900))
d.travel()
# d.remove(2)
# d.remove(700)
# d.remove(200)
# d.remove(400)
d.remove(900)
d.travel()
九、栈
1、如何理解“栈”?
关于“栈”,我有一个非常贴切的例子,就是一摞叠在一起的盘子。我们平时放盘子的时候,都是从下往上一个一个放;取的时候,我们也是从上往下一个一个地依次取,不能从中间任意抽出。后进者先出,先进者后出,这就是典型的“栈”结构。
从栈的操作特性上来看,栈是一种“操作受限”的线性表,只允许在一端插入和删除数据。
2、相比链表,栈带给我的只有限制,并没有任何优势。那我直接使用数组或者链表不就好了吗?为什么还要用这个“操作受限”的“栈”呢?
事实上,从功能上来说,链表确实可以替代栈,但你要知道,特定的数据结构是对特定场景的抽象,而且,数组或链表暴露了太多的操作接口,操作上的确灵活自由,但使用时就比较不可控,自然也就更容易出错。
当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出、先进后出的特性,我们就应该首选“栈”这种数据结构。
3、如何实现一个“栈”?
从刚才栈的定义里,我们可以看出,栈主要包含两个操作,入栈和出栈,也就是在栈顶插入一个数据和从栈顶删除一个数据。理解了栈的定义之后,我们来看一看如何用代码实现一个栈。
实际上,栈既可以用顺序表来实现,也可以用链表来实现。用顺序表实现的栈,我们叫作顺序栈,用链表实现的栈,我们叫作链式栈。
4、栈的操作
Stack() 创建一个新的空栈
push(item) 添加一个新的元素item到栈顶
pop() 弹出栈顶元素
peek() 返回栈顶元素
is_empty() 判断栈是否为空
size() 返回栈的元素个数
# 模块的名字重复的话!!!
class Stack(object):
'''栈 用列表的方式来实现'''
def __init__(self):
self.__items = []
def is_empty(self):
"""判断栈是否为空"""
if self.__items == []:
return True
else:
return False
# return self.__items == []
def push(self, item):
"""添加一个新的元素item到栈顶"""
self.__items.append(item)
# self.__items.insert(0, item)
def pop(self):
"""弹出栈顶元素"""
return self.__items.pop()
def peek(self):
"""返回栈顶元素"""
if self.is_empty():
return None
else:
return self.__items[-1]
# return self.__items[len(self.__items)-1]
def size(self):
"""返回栈的元素个数"""
return len(self.__items)
def travel(self):
"""遍历"""
print(self.__items)
# for item in self.__items:
# print(item)
if __name__ == '__main__':
s = Stack()
print(s.is_empty())
s.push(1)
s.push(2)
s.push(3)
s.pop()
print(s.size())
print(s.is_empty())
print(s.peek())
s.travel()
5、案例
浏览器的前进、后退功能,我想你肯定很熟悉吧?
当你依次访问完一串页面 a-b-c 之后,点击浏览器的后退按钮,就可以查看之前浏览过的页面 b 和 a。当你后退到页面 a,点击前进按钮,就可以重新查看页面 b 和 c。但是,如果你后退到页面 b 后,点击了新的页面 d,那就无法再通过前进、后退功能查看页面 c 了。
假设你是 Chrome 浏览器的开发工程师,你会如何实现这个功能呢?
十、队列
1、对列的概念
队列这个概念非常好理解。你可以把它想象成排队买票,先来的先买,后来的人只能站末尾,不允许插队。先进者先出,这就是典型的“队列”。
2、队列的操作
我们知道,栈只支持两个基本操作:入栈 push()和出栈 pop()。队列跟栈非常相似,支持的操作也很有限,最基本的操作也是两个:入队 enqueue(),放一个数据到队列尾部;出队 dequeue(),从队列头部取一个元素。
所以,队列跟栈一样,也是一种操作受限的线性表数据结构。
队列的概念很好理解,基本操作也很容易掌握。作为一种非常基础的数据结构,队列的应用也非常广泛,特别是一些具有某些额外特性的队列,比如循环队列、阻塞队列、并发队列。它们在很多偏底层系统、框架、中间件的开发中,起着关键性的作用。
3、队列的实现
Queue() 创建一个空的队列
enqueue(item) 往队列中添加一个item元素
dequeue() 从队列头部删除一个元素
is_empty() 判断一个队列是否为空
size() 返回队列的大小
class Queue(object):
def __init__(self):
self.__items = []
def enqueue(self, item):
"""
:param item: 往队列中添加一个item元素
:return: None
"""
self.__items.append(item)
def dequeue(self):
"""
从队列头部删除一个元素
:return: 队列头元素
"""
return self.__items.pop(0)
def is_empty(self):
"""
判断一个队列是否为空
:return: True or False
"""
return self.__items == []
def size(self):
return len(self.__items)
def travel(self):
"""遍历"""
# print(self.__items)
for item in self.__items:
print(item)
if __name__ == '__main__':
q = Queue()
print(q.is_empty())
q.enqueue(1)
q.enqueue(2)
q.enqueue(3)
q.enqueue(4)
print(q.is_empty())
print(q.size())
q.travel()
q.dequeue()
q.travel()
4、双端队列
(1)概念
双端队列(deque,全名double-ended queue),是一种具有队列和栈的性质的数据结构。
双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行。双端队列可以在队列任意一端入队和出队。
(2)操作
Deque() 创建一个空的双端队列
add_front(item) 从队头加入一个item元素
add_rear(item) 从队尾加入一个item元素
remove_front() 从队头删除一个item元素
remove_rear() 从队尾删除一个item元素
is_empty() 判断双端队列是否为空
size() 返回队列的大小
'''
Deque() 创建一个空的双端队列
add_front(item) 从队头加入一个item元素
add_rear(item) 从队尾加入一个item元素
remove_front() 从队头删除一个item元素
remove_rear() 从队尾删除一个item元素
is_empty() 判断双端队列是否为空
size() 返回队列的大小
'''
class Deque(object):
"""双端队列"""
def __init__(self):
self.__items = []
def is_empty(self):
"""
判断双端队列是否为空
:return:True or False
"""
return self.__items == []
def size(self):
return len(self.__items)
def add_front(self, item):
"""从队头加入一个item元素"""
self.__items.insert(0, item)
def add_rear(self, item):
"""从队尾加入一个item元素"""
self.__items.append(item)
def remove_front(self):
"""从队头删除一个item元素"""
return self.__items.pop(0)
def remove_rear(self):
"""从队尾删除一个item元素"""
return self.__items.pop()
def travel(self):
"""遍历"""
# print(self.__items)
for item in self.__items:
print(item)
if __name__ == '__main__':
d = Deque()
print(d.is_empty())
d.add_front(1)
d.add_rear(2)
d.travel()
d.remove_front()
d.remove_rear()
print(d.size())
# 链表 无限排队的无界队列 响应时间 比较敏感 不合适
# 顺序表 有界的队列 如果超过 直接拒绝 响应时间 比较敏感 合适
# 这个概念是说,如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。
# [1,2,6,2,3,4]
# [1,2,2,3,4,6]
# 稳定 不稳定
# 电商 订单 下单时间 订单金额 10W 按照金额从小到大排序
# 对于金额相同的 按照下单时间来排序
# 先按照金额排序 在按照时间来排序
5、阻塞队列
阻塞队列其实就是在队列基础上增加了阻塞操作。简单来说,就是在队列为空的时候,从队头取数据会被阻塞。因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。
6、案例
我们知道,CPU 资源是有限的,任务的处理速度与线程个数并不是线性正相关。相反,过多的线程反而会导致CPU 频繁切换,处理性能下降。所以,线程池的大小一般都是综合考虑要处理任务的特点和硬件环境,来事先设置的。例如,一个QQ是一个进程,一个聊天窗口是一个线程。
当我们向固定大小的线程池中请求一个线程时,如果线程池中没有空闲资源了,这个时候线程池如何处理这个请求?是拒绝请求还是排队请求?各种处理策略又是怎么实现的呢?
链表 无限排队的无界队列 响应时间 比较敏感 不合适
顺序表 有界的队列 如果超过 直接拒绝 响应时间 比较敏感 合适
十一、递归算法
1、如何理解“递归”
递归是一种应用非常广泛的算法(或者编程技巧)。之后我们要讲的很多数据结构和算法的编码实现都要用到递归,比如 DFS 深度优先搜索、前中后序二叉树遍历等等。所以,搞懂递归非常重要,否则,后面复杂一些的数据结构和算法学起来就会比较吃力。
2、递归需要满足的三个条件
刚刚这个例子是非常典型的递归,那究竟什么样的问题可以用递归来解决呢?我总结了三个条件,只要同时满足以下三个条件,就可以用递归来解决。
(1)一个问题的解可以分解为几个子问题的解;
(2)这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样;
(3)存在递归终止条件。
def dp(s):
if isinstance(s,(int,str)):
print(s)
else:
for item in s:
dp(item)
l=['jack',('tom',23),'rose',(14,55,67)]
dp(l)
十二、排序算法
排序算法太多了,有很多可能你连名字都没听说过,比如猴子排序、睡眠排序、面条排序等。这里只讲众多排序算法中的一小撮,也是最经典的、最常用的:冒泡排序、插入排序、选择排序、归并排序、快速排序、希尔排序
如何分析一个“排序算法”?
学习排序算法,我们除了学习它的算法原理、代码实现之外,更重要的是要学会如何评价、分析一个排序算法。那分析一个排序算法,要从哪几个方面入手呢?
1.排序算法的执行效率
对于排序算法执行效率的分析,我们一般会从这几个方面来衡量:
(1)时间复杂度;
(2)比较次数和交换(或移动)次数。
- 排序算法的内存消耗
原地排序算法,就是特指空间复杂度是 O(1) 的排序算法。
3.排序算法的稳定性
仅仅用执行效率和内存消耗来衡量排序算法的好坏是不够的。针对排序算法,我们还有一个重要的度量指标,稳定性。这个概念是说,如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。
4、冒泡排序
冒泡排序(英语:Bubble Sort)是一种简单的排序算法。它重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
冒泡排序算法的运作如下:
(1)比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个。
(2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
(3)针对所有的元素重复以上的步骤,除了最后一个。
(4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
冒泡排序的分析
冒泡排序的实现
#方法一
def bubble_sort(alist):
for j in range(len(alist)-1,0,-1):#步长控制方向
# j表示每次遍历需要比较的次数,是逐渐减小的
for i in range(j):
if alist[i] > alist[i+1]:
alist[i], alist[i+1] = alist[i+1], alist[i]
#方法二
def bu_sort(li):
n = len(li) # n = 9
# 外层控制趟数
for j in range(n-1): # 8
# 0-8 j
for i in range(0, n-1-j):
# 内层循环为当前i趟数,所需比较的次数
# 9-1-0 8 9-1-1 7 6 5 4 3 2 1
if li[i] > li[i + 1]:
li[i], li[i + 1] = li[i + 1], li[i]
li = [54,26,93,17,77,31,44,55,20]
bubble_sort(li)
print(li)
泡排序是稳定的排序算法吗?
在冒泡排序中,只有交换才可以改变两个元素的前后顺序。为了保证冒泡排序算法的稳定性,当有相邻的两个元素大小相等的时候,我们不做交换,相同大小的数据在排序前后不会改变顺序,所以冒泡排序是稳定的排序算法。
冒泡排序的时间复杂度是多少?
最好情况下,要排序的数据已经是有序的了,我们只需要进行一次冒泡操作,就可以结束了,所以最好情况时间复杂度是 O(n)。而最坏的情况是,要排序的数据刚好是倒序排列的,我们需要进行 n 次冒泡操作,所以最坏情况时间复杂度为 O(n2)。
5、选择排序
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已
排序序列的末尾。以此类推,直到所有元素均排序完毕。
选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。
选择排序的实现
def selection_sort(li):
"""选择排序"""
n = len(li)
for i in range(n-1): # 0-7
# 记录最小位置
min_index = i
for j in range(min_index, n):
# print(min_index)
if li[j] < li[min_index]:
min_index = j
li[i], li[min_index] = li[min_index], li[i]
selection_sort(li)
print(li)
#稳定 要比 不稳定 更好
#最小值重复 稳定
#最大值重复 不稳定
6、插入排序
插入排序是一种简单直观的排序算法,类似抓扑克牌的排序方式。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。在插入排序实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
首先,我们将数组中的数据分为两个区间,已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中的元素为空,算法结束。
插入排序也包含两种操作,一种是元素的比较,一种是元素的移动。当我们更需要将一个数据a插入到已排序区间时,需要拿a与已排序区间的元素依次比较大小,找到合适的插入位置。找到插入点之后,我们还需要将插入点之后的元素顺序往后移动一位,这样才能腾出位置给元素a插入。
def insert_sort(li):
n = len(li)
# 从第二个位置,即下标为1的开始向前插入
for i in range(1, n):
# 从第i个元素 向前比较 如果小于前一个元素 交换
for j in range(i, 0, -1):
if li[j] < li[j - 1]:
li[j], li[j - 1] = li[j - 1], li[j]
if __name__ == '__main__':
li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
insert_sort(li)
print(li)
li = [54, 26, 93, 17, 77, 31, 44, 55, 20] # 稳定
# li = [54 ]
7、希尔排序
希尔排序是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因DL.Shell于1959年提出而得名。希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
希尔排序过程:
希尔排序的基本思想是:将数组列在一个表中并对列分别进行插入排序,重复这过程,不过每次用更长的列(步长更长了,列数更少了)来进行。最后整个表就只有一列了。将数组转换至表时为了更好地理解这算法,算法本身还是使用数组进行排序。
def shell_sort(li):
n = len(li)
gap = n//2 # python2 python3
while gap > 0:
for i in range(gap, n): # i 4 - 8
# gap = 4
# i = 4 li[0] > li[4] 54 > 77
# i = 5 li[1] > li[5] 26 > 31
# i = 6 li[2] > li[6] 93 > 44
# i = 8 li[4] > li[8] 77 > 20
while i >= gap and li[i-gap] > li[i]:
li[i-gap], li[i] = li[i], li[i-gap]
i -= gap
gap = gap//2
if __name__ == '__main__':
li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
shell_sort(li)
print(li)
时间复杂度:
(1)最优时间复杂度:根据步长序列的不同而不同;
(2)最坏时间复杂度:O(n^2);
(3)稳定性:不稳定。
8、快速排序
快速排序,又称划分交换排序,通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
步骤:
(1)从数列中挑出一个元素,称为“基准”;
(2)重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以放到任意一边),在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区的操作。
(3)递归地把小于基准值元素的子数列和大于基准值元素的子数列排序。
'''
快排
log(3)9=2
又3^2=9
'''
def quick_sort(li,start,end):
# low = 0
# high = len(li) - 1
low = start
high = end
mid = li[low]
if start >= end:
return
# 循环退出 low = high
while low < high:
while low < high and li[high] >= mid:
high -= 1
li[low] = li[high]
while low < high and li[low] < mid:
low += 1
li[high] = li[low]
li[low] = mid
# [20, 26, 44, 17, 31, 54, 77, 55, 93]
# 对前面的元素 比54小的 进行快排 [20, 26, 44, 17, 31]
quick_sort(li, start, low-1)
# 对后面的元素 比54大的 进行快排 [77, 55, 93]
quick_sort(li, low+1, end)
# quick_sort(li[:low-1])
# quick_sort(li[low+1:])
if __name__ == '__main__':
li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
quick_sort(li, 0, len(li)-1)
# quick_sort(li)
print(li)
#不稳定
9、归并排序
归并排序是采用分治法的一个非常典型的应用。归并排序的思想就是先递归分解数组,再合并数组。
将数组分解最小之后,然后合并两个有序数组,基本思路是比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。
'''
归并排序
将数组分解最小之后,然后合并两个有序数组,基本思路是比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。
'''
def merge_sort(li):
'''
归并排序
:param li:
:return: sort li
'''
# 什么时候不在继续拆分了?
if len(li) <= 1:
return li
# 二分分解
mid_index = len(li) // 2 # 4
left_li = merge_sort(li[:mid_index]) # left_li[:4]
right_li = merge_sort(li[mid_index:]) # right li[4:]
# while
l_index = 0
r_index = 0
result = [] # 26
while l_index < len(left_li) and r_index < len(right_li):
if left_li[l_index] < right_li[r_index]:
result.append(left_li[l_index])
l_index += 1
else:
result.append(right_li[r_index])
r_index += 1
# print(left_li)
# print(right_li)
print(l_index)
result += left_li[l_index:]
result += right_li[r_index:]
# 排完序的列表
return result
if __name__ == '__main__':
li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
s = merge_sort(li)
print(s)
十三、搜索
搜索是在一个项目集合中找到一个特定项目的算法过程。搜索通常的答案是真的或假的,因为该项目是否存在。搜索的几种常见方法:顺序查找、二分法查找、二叉树查找、哈希查找。
1、二分查找
二分查找又称折半查找,优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除困难。因此,折半查找方法适用于不经常变动而查找频繁的有序列表。首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个字表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一字表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。
'''
0-99 23
49 > 23 0-49
17 < 23 0-23
'''
# (非递归实现)
# def binary_search(li, item):
#
# first = 0
# last = len(li) - 1
#
# while first <= last:
# mid = (first + last) // 2
# if li[mid] == item:
# return True
# elif item < li[mid]: # 3 < 44
# last = mid - 1
# else:
# first = mid + 1
#
# return False
# (递归实现)
def binary_search(li, item):
if len(li) <= 0:
return False
else:
mid = len(li) // 2
if li[mid] == item:
return True
else:
if item < li[mid]: # 33 < 44 [17, 20, 26, 31] 33 < 26 [ 31] li[:0] li[0:]
return binary_search(li[:mid], item)
else:
return binary_search(li[mid+1:], item)
if __name__ == '__main__':
li = [17, 20, 26, 31, 44, 54, 55, 77, 93]
print(binary_search(li, 33))
print(binary_search(li, 44))
十四、树与树算法
树是一种抽象数据类型(ADT)或是 视作这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由n(n>=1)个有限节点组成一个具有层次关系的集合,把它叫做“树”,是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
(1)每个节点有零个或多个子节点;
(2)没有父节点的节点称为根节点;
(3)每一个非根节点有且只有一个父节点;
(4)除了根节点外,每个子节点可以分为多个不想交的子树;
树这种数据结构真的很像我们现实生活中的“树”,这里面每个元素我们叫做“节点”;用来连接相邻节点之间的关系,我们叫做“父子关系”。
A节点就是B节点的父节点,B节点是A节点的子节点。B、C、D这三个节点的父节点是同一个节点,所以他们之间互称为兄弟节点。我们把没有父节点的节点叫做根节点,也就是图中的节点E。我们把没有子节点的节点叫做叶子节点或叶节点,比如图中的G、H、I、J、K、L都是叶子节点。除此之外,关于“树”,还有三个比较相似的概念:高度、深度、层。他们的定义是这样的(见下图):
(1)节点的高度:节点到叶子节点的最长路径;
(2)节点的深度:根节点到这个节点所经历的边的个数;
(3)节点的层数:节点的深度+1;
(4)树的高度:根节点的高度。
树的种类:
- 无序树:树中任意节点的子节点之间没有顺序关系,这种树称为无序树,也称为自由树;–>>>无使用意义
- 有序树:树中任意节点的子节点之间有顺序关系,这种树称为有序树;
(1)二叉树:每个节点最多含有两个子树的树称为二叉树;
*完全二叉树:*对于一颗二叉树,假设其深度为d(d>1)。除了第d层外,其他各层的节点数目均已达到最大值,且第d层所有节点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树,其中满二叉树的定义是所有叶节点都在最底层的完全二叉树;
*平衡二叉树(AVL树):*当且仅当任何节点的两棵子树的高度差异不大于1的二叉树;
排序二叉树(二叉查找树,也称二叉搜索树、有序二叉树):
(2)霍夫曼树(用于信息编码):带权路径最短的二叉树称为哈夫曼树或最优二叉树;
(3)B树:一种对读写操作进行优化的自平衡的二叉查找树,能够保持数据有序,拥有多余两个子树。
上图里面,有两个比较特殊的二叉树,分别编号2和编号3这两个。
其中,编号2的二叉树中,叶子节点全都在最底层,除了叶子节点之外,每个节点都有左右两个子节点,这种二叉树就叫做满二叉树。
编号3的二叉树中,叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大,这种二叉树叫做完全二叉树。
树的存储与表示
*顺序存储:*将数据结构存储在固定的数组中,然后在遍历速度上有一定的优势,但因所占空间比较大,是非主流二叉树。二叉树通常以链式存储。
链式存储:
从图中可以很清楚地看到,每个节点有三个字段,其中一个存储数据,另外两个是指向左右子节点的指针。我们只有拎住根节点,就可以通过左右子节点的指针,把整棵树都串起来。这种存储方式我们比较常用。大部分二叉树代码都是通过这种结构来实现的。
常见的一些树的应用场景:
(1)xml、html等,那么编写这些东西的解析器的时候,不可避免用到树;
(2)路由协议就是使用了树的算法;
(3)mysql数据库索引;
(4)文件系统的目录结构;
(5)很多经典AI算法其实都是树搜索,此外机器学习中的decision tree也是树结构。
class Node(object):
'''节点类'''
def __init__(self, item, litem=None, ritem=None):
self.item = item
self.litem = litem
self.ritem = ritem
# 深度为3的 完全二叉树 添加
class Tree(object):
"""树类"""
def __init__(self, root=None):
self.root = root
def add(self, item):
# 创建一个节点
node = Node(item)
# 空树
if self.root == None:
self.root = node
else:
queue = []
# 根节点放到队列中
queue.append(self.root)
# [] bool False [None] bool True
# 当前的节点
while queue:
cur_node = queue.pop(0)
if cur_node.litem == None:
cur_node.litem = node
return
elif cur_node.ritem == None:
cur_node.ritem = node
return
else:
queue.append(cur_node.litem)
queue.append(cur_node.ritem)
def breadth_travel(self):
if self.root == None:
return
else:
queue = []
# 根节点放到队列中
queue.append(self.root)
while queue:
node = queue.pop(0)
print(node.item, end=' ')
if node.litem != None:
queue.append(node.litem)
if node.ritem != None:
queue.append(node.ritem)
def preorder(self, node):
"""递归实现先序遍历"""
if node == None:
return
print(node.item, end=" ")
self.preorder(node.litem)
self.preorder(node.ritem)
def inorder(self, node):
"""递归实中序遍历"""
if node == None:
return
self.inorder(node.litem)
print(node.item, end=" ")
self.inorder(node.ritem)
if __name__ == '__main__':
tree = Tree()
tree.add(0)
tree.add(1)
tree.add(2)
tree.add(3)
tree.add(4)
tree.add(5)
tree.add(6)
tree.add(7)
tree.add(8)
tree.add(9)
# tree.breadth_travel()
tree.preorder(tree.root)
print("")
tree.inorder(tree.root)