数据结构与算法笔记

数据结构与算法笔记

什么是数据结构

数据结构是一门研究非数值计算的程序设计问题中的操作对象,以及他们之间的关系和操作等相关问题的学科。

数据结构就是把数据元素按照一定的关系组织起来的集合,用来组织和存储数据。

最常用的数据运算有五种:插入 删除 修改 查找 排序

数据结构的分类

传统上,我们可以把数据结构分为逻辑结构物理结构两大类。

逻辑结构的分类

逻辑结构是从具体问题中抽象出来的横型,是抽象意义上的结构,按照对象中数据元素之间的相互关系分类。

  • 集合结构:集合结构中数据元素除了属于同一个集合外,他们之间没有任何其他的关系。

在这里插入图片描述

  • 线性结构:线性结构中的数据元素之间存在一对一的关系。

在这里插入图片描述

  • 树形结构:树形结构中的数据元素之间存在一对多的层次关系

在这里插入图片描述

  • 图形结构:图形结构的数据元素是多对多的关系

在这里插入图片描述

物理结构分类

逻结构在计算机中真正的表示方式(又称为融像)称为物理结构,也可以叫做存储结构。常见的物理结构有顺序存储结构
式存储结构


  • 顺序存储结构:把数据元素放到地址连续的存储单元里面,其数据间的逻辑关系和物理关系是一致的,比如我们常用的数组就是顺序存储结构。

    在这里插入图片描述

因为数据元素存储的地址是连续的,所以每个数据单元是有对应的索引的,所以我们访问时可以通过索引就很快可以找到对应你的元素。

顺序存储结构存在一定的弊端,就像生活中排时也会有人插队也可能有人有特殊情况突然离开,这时候整个结构都处于变化中,
此时就需要链式存储结构。

  • 链式存储结构:是把数据元素存放在任意的存储单元里面,这组存储单元可以是连续的也可以是不连续的。此时,数据元素之间并不能反映元素间的逻辑关系,因此在链式存储结构中引进了一个指针存放数据元素的地址,这样通过地址就可以找到相关联数据元素的位置。比如我们要找4,就可以从1找到2,从2找到3,从3找到4,这样就可以找到4了,这样的查找效率显然没有顺序存储结构高。但当我们要在3和4之间插入一个10元素,因为链式存储结构的地址可以是不连续的,我们可以把3和4之间的链接断掉,让3指向10,再让10指向4,就完成了插入操作,所以链式存储结构的插入效率会比顺序存储结构高。

在这里插入图片描述

算法

​ 算法是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法解决问题的策略机制。也就是
说,能够对一定规范的输入,在有限时间内获得所要求的输出。

​ 根据一定的条件,对一些数据进行计算,得到需要的结果。

​ 在程序中,解决一个相同的问题可以有不同的算法解决,但不同的算法的成本也是不同的,一个好的算法追求俩个目标:

		1. 花最少的时间来完成需求
		1. 占用最少的内存空间来完成需求

算法分析

研究算法的最终目的就是如何花更少的时间,如何占用更少的内存去完成相同的需求

有关算法时间耗费分析,我们称之为算法的时间复杂度分析,有关算法的空间耗费分析,我们称之为算法的空间复杂度分析。

算法的时间复杂度分析

要计算算法时间耗费情况,首先我们得度量算法的执行时间,有俩种分析方法:

1.事后分析估算方法

2.事前分析估算方法

我们分析一个算法的运行时间,最重要的就是把核心操作的次数与输入的规模关联起来。

事后分析方法

用计算机计时器对不同的算法编制的程序的运行时间进行比较,从而确定算法效率的高低,但是这种方法有很大的缺陷:必须依据算
法实现编制好的测试程序,通常要花费大量时间和精力,测试完了如果发现测试的是非常糟糕的算法,那么之前所做的事情就全部白
费了,并且不同的测试环境(硬件环境)的差别导致测试的结果差异也很大。

事前分析方法

​ 在计算机程序编写前,依据统计方法对算法进行估算,经过总结,我们发现一个高级语言编写的程序程序在计算机上运行所消耗
的时间取决于下列因素:
​ 1.算法采用的策略和方案:
​ 2编译产生的代码质量:
​ 3.问题的输入规横(所谓的问题输入规模就是输入量的多少):
​ 4.机器执行指令的速度:

由此可见,抛开这些与计算机硬件、软件有关的因素,一个程序的运行时间依赖于算法的好坏和问题的输入规模。如果算法固
定,那么该算法的执行时间就只和问题的输入规模有关系了。

算法时间复杂度

函数渐近增长

我们分析一个算法的运行时间,最重要的就是把核心操作的次数与输入的规模关联起来。

在我们比较算法随着输入规模的增长量时,可以有以下规则

  • 算法函数中的常数可以忽略;
  • 算法函数中最高次幂的常数因子可以忽略;
  • 算法函数中最高次幂越小,算法效率越高。
大O记法

定义

​ 在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随着n的变化情况并确定T(n)的量级。算法的时
间复杂度,就是算法的时间量度,记作:T(n)=O(fn)。它表示随着问题规模的增大,算法执行时间的增长率和f()的增长率相同,称作
算法的渐近时间复杂度,简称时间复杂度,其中f(n)是问题规模的某个函数。
​ 在这里,我们需要明确一个事情:执行次数=执行时间
​ 用大写O()来体现算法时间复杂度的记法,我们称之为大O记法。一般情况下,随着输入规模的增大,T()增长最慢的算法为最优
算法。

1.算法1:

sum=0				#执行一次
n=100				#执行一次
sum=(n+1)*n/2		#执行一次
print(sum)

2.算法2:

sum=0					#执行一次
n=100					#执行一次
for i in range(n):
	sum+=i				#执行n次
print(sum)

3.算法3:

sum=0					#执行一次
n=100					#执行一次
for i in range(n):
	for j in range(n):
		sum+=i			#执行n的平方次
print(sum)

算法一:3次

算法二:n+2次

算法三:n^2+2次

如果用大O记法表示上述每个算法的时间复杂度,应该如何表示呢?基于我们对函数渐近增长的分析,推导大O阶的表示法有以下几个
规则可以使用:
1.用常数1取代运行时间中的所有加法常数
2.在修改后的运行次数中,只保留高阶项
3.如果最高阶项存在,且常数因子不为1,则去除与这个项相乘的常数

所以,上述算法的大O记法分别为:
算法一:0(1)
算法二:O(n)
算法三:O(n^2)

常见的大O阶
  • 线性阶

一般含有非嵌套循环涉及线性阶,线性阶就是随着输入规模的扩大,对应计算次数呈直线增长,例如:

sum=0					#执行一次
n=100					#执行一次
for i in range(n):
	sum+=i				#执行n次
print(sum)

上面这段代码,它的循环的时间复度为O(n),因为循环体中的代码需要执行n次.

  • 平方阶

一般嵌套循环属于这种时间复杂度

sum=0					#执行一次
n=100					#执行一次
for i in range(n):
	for j in range(n):
		sum+=i			#执行n的平方次
print(sum)

上面这段代码,n=100,也就是说,外层循环每执行一次,内层循环就执行100次,那总共程序想要从这两个循环中出来,就需要执行
100*100次,也就n的平方次,所以这段代码的时间复杂度是0(n^2)

  • 立方阶

一般三层嵌套循环属于这种时间复度

sum=0
n=100
for i in ragne(n):
	for j in range(n):
		for k in range(n):
			sum+=1
print(sum)

上面这段代码,n=100,也就是说,外层循环每执行一次,中间循环循环就执行100次,中间循环每执行一次,最内层循环需要执行
100次,那总共程序想要从这三个循环中出来,就需要执行100100100次,也就是的立方,所以这段代码的时间复杂度是O(n^3).

  • 对数阶
i=1
n=100
while(i<n):
	i=i*2

由于每次i*2之后,就距离n更近一步,假设有x个2相乘后大于n,则会退出循环。由于是2^x=n,得到x=log(2)n,所以这个循环的时间复
杂度为(logn)

对于对数阶,由于随着输入规模的增大,不管底数为多少,他们的增长趋势是一样的,所以我们会忽略底数。


  • 常数阶

一般不涉及循环操作的都是常数阶,因为它不会随着的增长而增加操作次数。例如:

n=100
i=n+2

他们的复杂程度从低到高依次为:
O(1)<o(logn)<o(n)<o(nlogn)<o(n42)O(n^3)
会发现,从平方阶开始,随着输入规模的增大,时间成本会急剧增大,所以,我们的算法,尽可能的追求的是O(1),o(logn),O(n),O(nlogn)这几种时间复杂度,而如果发现算法的时间复杂度为平方阶、立方阶或者更复杂的,那我们可以分为这种算法是不可取的,需要优化。

函数调用的时间复杂度分析
def show(i):
	print(i)
	
n=100
for i in range(n):
	show(i)

有一个for循环,循环体调用了show方法,由于show方法内部只执行了一行代码,所以show的时间复杂度为
O(1),那时间复杂度就是0(n)

在时间复杂度上,我们关注的是最坏的结果,就是最坏的时间复杂度

时间复杂度的几条基本计算规则:

​ 1.基本操作,即只有常数项,认为其时间复杂度为O(1)
​ 2.顺序结构,时间复杂度按加法进行计算
​ 3.循环结构,时间复杂度按乘法进行计算
​ 4.分支结构,时间复杂度取最大值
​ 5.判断一个算法的效率时,往往只需要关注操作数量的最高次项,其它次要项和常数项可以忽略
​ 6.在没有特殊说明时,我们所分析的算法的时间复杂度都是指最坏时间复杂度

python数据结构

常见的内存占用
  • 基本数据类型的内存占用情况
数据类型内存占用的字节数
byte1
short2
int4
long8
float4
double8
boolean1
char2
  • 计算机访问内存的方式都是一次一个字节 一个字节有八位,一个字节有一个地址标识

在这里插入图片描述

例如,int a=1,它存储在内存中,首先他得转为二进制形式 ,又因为一个int占四个字节,所以转为 0000 0000 0000 0000 0000 0000 0000 0001 存在地址里面

在这里插入图片描述

变量标识的本质

在python中,a=10,a的空间不是存储10,而是存储10这个空间的地址,这也就是我们在python中不需要定义变量的类型,也可以完成

a,b=b,a ,的交换;而在其他语言中,a的空间则是存储10

在这里插入图片描述

线性表

在程序中,经常需要将一组(通常是同为某个类型的)数据元素作为整体管理和使用,需要创建这种元素组,用变量记录它们,传进传出函数等。一组数据中包含的元素个数可能发生变化(可以增加或别除元素)

对于这种需求,最简单的解决方案便是将这样一组元素看成一个序列,用元素在序列里的位置和顺序,表示实际应用中的某种有意义的信息,或者表示数据之间的某种关系。

这样的一组序列元素的组织形式,我们可以将其抽象为线性表。一个线性表是某类元素的一个集合,还记录着元素之间的一种顺序关系。线性表是最基本的数据结构之一,在实际程序中应用非常广泛,它还经常被用作更复杂的数据结构的实现基础。

根据线性表的实际存储方式,分为两种实现模型:

  • 顺序表,将元素顺序地存放在一块连续的存储区里,元素间的顺序关系由它们的存储顺序自然表示。
  • 链表,将元素存放在通过链接构造起来的一系列存储块中。

顺序链表

当我们要存储a=[1,23,4] 或者不同类型的b=[1 , ‘df’ , 1.34 , 100] 时,系统会去内存申请一块空间

在这里插入图片描述

​ 该图表示的是顺序表的基本形式,数据元素本身连续存储,每个元素所占的存储单元大小固定相同,元素的下标是其逻辑地址,而元素

存储的物理地址(实际内存地址)可以通过存储区的起始地址Loc(e0)加上逻辑地址(第i个元素)与存储单元大小©的乘积计算而得,

即:Loc(ej)=Loc(eo)+c*i , 故访问指定元素时无需从头遍历,通过计算便可获得对应地址,其时间复杂度为O(1)。

​ 如果元素的大小不统一,则须采用图的元素外置的形式,将实际数据元素另行存储,而顺序表中各单元位置保存对应元素的地址信息

(即链接),因为每个地址的大小都是一样的,是四个字节,所以存储地址可以形成一个链表。由于每个链接所需的存储量相同,通过上

述公式,可以计算出元索链接的存储位置,而后顺着链接找到实际存储的数据元素。注意,图中的c不再是数据元素的大小,而是存储一

个链接地址所需的存储量,这个量通常很小。这样的顺序表也被称为对实际数据的索引,这是最简单的索引结构。

顺序表的结构与实现

在这里插入图片描述

一个顺序表的完整信息包括两部分,一部分是表中的元素集合,另一部分是为实现正确操作而需记录的信息,即有关表的整体情况的信息,这部分信息主要包括元素存储区的容量和当前表中已有的元素个数两项。

顺序表实现的俩种方式

在这里插入图片描述

俩种方式为一体式结构分离式结构

​ 图a为一体式结构,存储表信息的单元与元素存储区以连续的方式安排一块存储区里,两部分数据的整体形成一个完整的顺序表对象。
一体式结构整体性强,易于管理。但是由于数据元素存储区域是表对象的一部分,顺序表创建后,元素存储区就固定了。
图b为分离式结构,表对象里只保存与整个表有关的信息(即容量和元素个数),实际数据元素存放在另个独立的元素存储区里,通过链接与基本表对象关联。

元素存储区替换

​ 一体式结构由于顺序表信息区与数据区连续存储在一起,所以若想更换数据区,则只能整体搬迁,即整个顺序表对象(指存储顺序表的结构信息的区域)改变了。

分离式结构若想更换数据区,只需将表信息区中的数据区链接地址更新即可,而该顺序表对象不变。

​ 大部分情况下,比如我们开辟一个容量为五的空间,但后面不够了,得向系统重新申请空间,数据区很好处理,把原先的数据释放给它进行了;对于一体式结构,我们还得把表头数据也放到新的空间里,那这样表头的地址发生了改变,变量原先指的表头地址也得改变指向新的表头地址, 但采用分离式结构时,表头就不需要到新的空间,只需要把第三个存储的地址指向新的空间就可以,这样变量指的表头地址也不用改用

所以为了考虑以后动态数据的改变,都会采用分离式结构

元素存储区扩充

​ 采用分离式结构的顺序表,若将数据区更换为存储空间更大的区域,则可以在不改变表对象的前提下对其数据存储区进行了扩充,所有使用这个表的地方都不必修改。只要程序的运行环境(计算机系统)还有空闲存储,这种表结构就不会因为满了而导致操作无法进行。人们把采用这种技术实现的顺序表称为动态顺序表,因为其容量可以在使用中动态变化。

扩充的俩种方式
  • 每次扩充增加固定数目的存储位置,如每次扩充增加10个元素位置,这种策略可称为线性增长。

    特点:节省空间,但是扩充操作频繁,操作次数多。

  • 每次扩充容量加僧筒。如每次扩充增加一倍存储空间。

    特点:减少了扩充操作的执行次数,但可能会浪费空间资源。以空间换时间,推荐的方式。

链表

顺序表的构建需要预先知道数据大小来申请连续的存储空间,而在进行扩充时又需要进行数据的搬迁,所以使用起来并不是很灵活。

链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。

定义

链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是不像顺序表一样连续存储数据,而是在每一个节点(数据存储单元)里存放下一个节点的位置信息(即地址)。

在这里插入图片描述

单向链表

单向链表也叫单链表,是链表中最简单的一种形式,它的每个节点包含两个域,一个信息域(元素域)和一个链接域。这个链接指向链表中的下一个节点,而最后一个节点的链接域则指向一个空值。

在这里插入图片描述

  • ·表元素域elem用来存放具体的数据。
  • 链接域next用来存放下一个节点的位置(python中的标识)
  • ·变量指向链表的头节点(首节点)的位置,从p出发能找到表中的任意节点。
单链表的实现
  • is_empty() 链表是否为空
  • length() 链表长度
  • travel() 遍历整个链表
  • add(item) 链表头部添加元素
  • append(item) 链表尾部添加元素
  • insert(pos,item) 指定位置添加元素
  • remove(item) 别除节点
  • search(tem) 查找节点是否存在
class Node(object):
	'''	节点	'''
	def __init__(self,elem):
		#节点的存放数据
		self.elem=elem
		#下一个节点的标识
		self.next=None
		
class SingleLinkList(object):
	'''单链表'''
	def __init__(self,node=None):
		self._head = node
	
	def is_empty(self):
		'''判断链表是否为空'''
		return self._head==None
	
	def length(self):
		'''链表长度'''
		cur=self._head		#定义游标,用来遍历节点
		count=0
		while cur != None:
			count+=1
			cur=cur.next
		return count
	
	def travel(self):
		'''遍历整个链表'''
		cur=self._head
		while cur != None:
			print(cur.elem,end=' ')
			cur=cur.next
		print('')
	
	def add(self,item):
		'''在链表头部添加元素'''
		node=Node(item)
		node.next=self._head
		self._head=node
		
	def append(self,item):
		'''链表尾部添加元素'''
		node=Node(item)
		if self.is_empty():
			self._head=node
		else:
			cur=self._head
			while cur.next!=None:
				cur=cur.next
			cur.next=node
		
	def insert(self,pos,item):
		'''指定位置插入元素'''
		if pos<=0:
			self.add(item)
		elif pos>(self.length()-1):
			self.append(item)
		else:
			pre=self._head
			count=0
			while count<(pos-1):
				count+=1
				pre=pre.next
			#当循环退出,pre指向pro-1位置
			node=Node(item)
			node.next=pre.next
			pre.next=node
			
	def remove(self,item):
		'''删除节点'''
		cur=self_head
		pre=None
		while cur!=None:
			if cur.elem==item:
				#先判断此节点是否为头节点
				if cur==self._head:
					self._haed=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.elem == item:
				return True
			else:
				cur = cur.next
		return False
		
链表与顺序表对比

链表失去了顺序表随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大,但对存储空间的
使用要相对灵活。

操作链表顺序表
访问元素O(n)O(1)
在头部插入/删除O(1)O(n)
在尾部插入/删除O(n)O(1)
在中间插入/删除O(n)O(n)

注意虽然表面看起来复杂度都是O(n),但是链表和顺序表在插入和删除时进行的是完全不同的操作。链表的主要耗时操作是遍历查找,删除

和插入操作本身的复杂度是O(1)。顺序表查找很快,主要耗时的操作是拷贝覆盖。因为除了目标元素在尾部的特殊情况,顺序表进行插入

和删除时需要对操作点之后的所有元素进行前后移位操作,只能通过拷贝和覆盖的方法进行。

双向链表

一种更复杂的链表是“双向链表"”或“承面链表”。每个节点有两个链接:一个指向前一个节点,当此节点为第一个节点时,指向空值;而另一个指向下一个节点,当此节点为最后一个节点时,指向空值。

在这里插入图片描述

双向链表的实现
  • is_empty() 链表是否为空
  • length() 链表长度
  • travel() 遍历整个链表
  • add(item) 链表头部添加元素
  • append(item) 链表尾部添加元素
  • insert(pos,item) 指定位置添加元素
  • remove(item) 别除节点
  • search(tem) 查找节点是否存在
class Node(object):
	'''	节点	'''
	def __init__(self,elem):
		#节点的存放数据
		self.elem=elem
		#下一个节点的标识
		self.next=None
		#上一个节点的标识
		self.prev=None
		
class DoubleLinkList(object):
	'''双链表'''
	def __init__(self,node=None):
		self._head = node
	
	def is_empty(self):
		'''判断链表是否为空'''
		return self._head==None
	
	def length(self):
		'''链表长度'''
		cur=self._head		#定义游标,用来遍历节点
		count=0
		while cur != None:
			count+=1
			cur=cur.next
		return count
	
	def travel(self):
		'''遍历整个链表'''
		cur=self._head
		while cur != None:
			print(cur.elem,end=' ')
			cur=cur.next
		print('')
	
	def add(self,item):
		'''在链表头部添加元素'''
		node=Node(item)
		node.next=self._head
		self._head=node
		node.next.prev=node
		
	def append(self,item):
		'''链表尾部添加元素'''
		node=Node(item)
		if self.is_empty():
			self._head=node
		else:
			cur=self._head
			while cur.next!=None:
				cur=cur.next
			cur.next=node
			node.prev=cur
		
	def insert(self,pos,item):
		'''指定位置插入元素'''
		if pos<=0:
			self.add(item)
		elif pos>(self.length()-1):
			self.append(item)
		else:
			cur=self._head
			count=0
			while count<pos:
				count+=1
				cur=cur.next
			#当循环退出,cur指向pro位置
			node=Node(item)
			node.next=cur
			cur.prev.next=node
			cur.prev=node
			
	def remove(self,item):
		'''删除节点'''
		cur=self_head
		while cur!=None:
			if cur.elem==item:
				#先判断此节点是否为头节点
				if cur==self._head:
					self._haed=cur.next
					if cur.next:
					#判断链表是否只有一个节点
						cur.next.prev=cur.prev
				else:
					cur.prev.next=cur.next
					if cur.next:
						cur.next.prev=cur.prev
				break
			else:
				pre=cur
				cur=cur.next
		
	def search(self,item):
		'''查找节点是否存在'''
		cur=self._head
		while cur!=None:
			if cur.elem == item:
				return True
			else:
				cur = cur.next
		return False
		
单向循环链表与实现

单链表的一个变形是单向循环链表,链表中最后一个节点的next域不再为None,而是指向链表的头节点。

  • is_empty() 链表是否为空
  • length() 链表长度
  • travel() 遍历整个链表
  • add(item) 链表头部添加元素
  • append(item) 链表尾部添加元素
  • insert(pos,item) 指定位置添加元素
  • remove(item) 别除节点
  • search(tem) 查找节点是否存在
class Node(object):
	'''	节点	'''
	def __init__(self,elem):
		#节点的存放数据
		self.elem=elem
		#下一个节点的标识
		self.next=None
		
class SingleLinkList(object):
	'''单向循环链表'''
	def __init__(self,node=None):
		self._head = node
		if node:
			node.next=node
	
	def is_empty(self):
		'''判断链表是否为空'''
		return self._head==None
	
	def length(self):
		'''链表长度'''
		if self.is_empty():
			return 0
		cur=self._head		#定义游标,用来遍历节点
		count=1
		while cur != self._head:
			count+=1
			cur=cur.next
		return count
	
	def travel(self):
		'''遍历整个链表'''
		if self.is_empty():
			return
		cur=self._head
		while cur != self._head:
			print(cur.elem,end=' ')
			cur=cur.next
		#退出循环,cur指向尾节点,但尾节点的元素未打印
		print(cur.elem)
	
	def add(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
            #退出循环,cur指向尾节点
            node.next=self._head
            self._head=node
            cur.next=self._head
		
	def append(self,item):
		'''链表尾部添加元素'''
		node=Node(item)
		if self.is_empty():
			self._head=node
			node.next=node
		else:
			cur=self._head
			while cur.next!=None:
				cur=cur.next
			node.next=self._head
			cur.next=node
		
	def insert(self,pos,item):
		'''指定位置插入元素'''
		if pos<=0:
			self.add(item)
		elif pos>(self.length()-1):
			self.append(item)
		else:
			pre=self._head
			count=0
			while count<(pos-1):
				count+=1
				pre=pre.next
			#当循环退出,pre指向pro-1位置
			node=Node(item)
			node.next=pre.next
			pre.next=node
			
	def remove(self,item):
		'''删除节点'''
		if self.is_empty():
			return
		cur=self_head
		pre=None
		while cur.next != self._head:
			if cur.elem==item:
				#先判断此节点是否为头节点
				if cur==self._head:
					#是头节点情况
					rear=self._head
					while rear.next != self._head:
						rear = rear.next
					self._head = cur.next
					rear.next = self._head
				else:
					#中间节点
					pre.next=cur.next
				return
			else:
				pre=cur
				cur=cur.next
		#退出循环,cur指向尾节点
		if cur.elem == item:
			if cur == self._head:
				#链表只有一个节点
				self._head=None
			else:
				pre.next = cur.next
		
	def search(self,item):
		'''查找节点是否存在'''
		if self.is_empty():
			return False
		cur=self._head
		while cur.next != self._head
			if cur.elem == item:
				return True
			else:
				cur = cur.next
		#循环退出,cur指向尾节点
		if cur.elem == item:
			return True
		return False

栈 (stck) , 有些地方称为堆栈,是一种容器,可存入数据元素、访问元素、删除元素,它的特点在于只能允许在容器的一端(称为栈顶端

指标,英语:top)进行加入数据(英语:push)和输出数据(英语:pop) 的运算。没有了位置概念,保证任何时候可以访问、删除的元素

都是此前最后存入的那个元素,确定了一种默认的访问顺序。

由于栈数据结构只允许在一端进行操作,因而按照后进先出(LIFO,Last In First Out)的原理运作。

在这里插入图片描述

栈的结构与实现

栈可以用顺序表实现,也可以用链表实现。相当于基于线性表的二次开发。

在头端还是在尾端是我们自己决定的,但我们用顺序表实现时,选择尾端更好,因为顺序表维度操作是O(1),而链表是O(n);用链表实现时,选择头端更好,因为链表在头部操作是O(1),而顺序表是O(n)。

  • Stack() 创建一个新的空栈
  • push(item) 添加一个新的元素item到栈顶
  • pop() 弹出栈顶元素
  • peek() 返回栈顶元素
  • is_empty() 判断栈是否为空
  • size() 返回栈的元素个数
class Stack(object):
	'''栈'''
	def __init__(self):
		self.__list=[]
		
	def push(self,item):
		'''添加一个新的元素item到栈顶'''
		self.__list.append(item)
	
	def pop(self):
		'''弹出栈顶元素'''
		return self.__list.pop()
		
	def peek(self):
		'''返回栈顶元素'''
		if self.__list:
			return self.__list[-1]
		else:
			return None
		
	def is_empty(self):
    	'''判断栈是否为空'''
    	return self.__list == []
    	
    def size(self):
    	'''返回栈的个数'''
    	return len(self.__list)

队列

队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。

队列是一种先进先出的(First In First Out)的线性表,简称FFO。允许插入的一端为队尾,允许删除的一端为队头。队列不允许在中间部位进行操作!假设队列是q=(a1,a2,an),那么a1就是队头元素,而n是队尾元素。这样我们就可以删除时,总是从a1开始,而插入时,总是在队列最后。这也比较符合我们通常生活中的习惯,排在第一个的优先出列,最后来的当然排在队伍最后。

在这里插入图片描述

队列的实现

同栈一样,队列也可以用顺序表或者链表实现。

  • Queue() 创建一个空的队列
  • enqueue(item) 往队列中添加一个item元素
  • dequeue() 从队列头部删除一个元素
  • is_empty() 判断一个队列是否为空
  • size() 返回队列的大小
class Queue(object):
	'''队列'''
	def __init__(self):
		self.__list=[]
	
	def enqueue(self,item):
		'''往队列中添加一个item元素'''
		#self.__list.insert(0,item)    在头端添加元素
		self.__list.append(item)		#在尾端添加元素
	
	def dequeue(self):
		'''从队列头部删除一个元素'''
		return self.__list.pop(0)		#在头部取出元素
		
	def is_empty(self):
		'''判断一个队列是否为空'''
		return self.__list == []
		
	def size(self):
		'''返回队列大小'''
		return len(self.__list)

双端队列

双端队列(deque,全名double-ended queue),是一种具有队列和栈的性质的数据结构。

双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行。双端队列可以在队列任意一端入队和出队。

实现

  • 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.__list = []
	
	def add_front(self,item):
		'''从队头加入一个元素'''
		self.__list.insert(0,item)
	
	def add_rear(self,item):
		'''从队尾加入一个元素'''
		self.__list.append(item)
		
	def pop_front(self):
		'''从对头删除一个元素'''
		return self.__list.pop(0)
		
	def pop_rear(self):
		'''从队尾删除一个元素'''
		return self.__list.pop()
		
	def is_empty(self):
		'''判断一个队列是否为空'''
		return self.__list == []
		
	def size(self):
		'''返回队列大小'''
		return len(self.__list)

排序算法

排序算法,是一种能将一串数据依照特定的顺序进行排列的一种算法

排序的稳定性

稳定性 : 稳定排序算法会让原本有相等键值的纪录维持相对次序。也就是如果一个排序算法是稳定的,当有两个相等键值的纪录R和S,且

在原本的列表中R出现在S之前,在排序过的列表中R也将会是在S之前。

当相等的元素是无法分辨的,比如像是整数,稳定性并不是一个问题。然而,假设以下的数对将要以他们的
第一个数字来排序。

(4,1) (3,1) (3,7) (5,6)

在这个状况下,有可能产生两种不同的结果,一个是让相等键值的纪录维持相对的次序,而另外一个则没有:
(3,1) (3,7) (4,1) (5,6) 维持次序
(3,7) (3,1) (4,1) (5,6) 次序被该改变

不稳定排序算法可能会在相等的键值中改变纪录的相对次序,但是稳定排序算法从来不会如此。不稳定排序算法可以被特别地实现为稳

定。作这件事情的一个方式是人工扩充键值的比较,如此在其他方面相同键值的两个对象间之比较,(比如上面的比较中加入第二个标

准:第二个键值的大小)就会被决定使用在原先数据次序中的条目,当作一个同分决赛。然而,要记住这种次序通常牵涉到额外的空间负担。

冒泡排序

冒泡排序算法的运作如下:

  • 比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个。
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  • 针对所有的元素重复以上的步骤,除了最后一个。
  • 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

若是从小到大排,就是每循环一次,比较相邻的俩个,最后就会把里面最大的元素放在最右边,然后一次次循环,循环n-1次

def bubble_sort(alist):
	'''冒泡排序'''
	n=len(alist)
	for i in range(n-1):
		for j in range(0,n-1-j):
			if alist[j]>alist[j+1]:			#比较相邻俩个
				alist[j],alist[j+1] = alist[j+1],alist[i]

这个函数对于[1,2,3,4]或者[2,1,3,4]依旧执行一次的次数

进行优化:

def bubble_sort(alist):
	'''冒泡排序'''
	n=len(alist)
	for i in range(n-1):
		count=0
		for j in range(0,n-1-j):
			if alist[j]>alist[j+1]:
				alist[j],alist[j+1] = alist[j+1],alist[i]
				count+=1
		if count == 0:
			return

时间复杂度
  • 最优时间复杂度:O(n)(表示遍历一次发现没有任何可以交换的元素,排序结束)
  • 最坏时间复杂度:O(n^2)
  • 稳定性:稳定

选择排序

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找
到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)
元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择
排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对个元素的表进行排序总
共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一
种。

若是从小到大排序,先遍历一次,将最小的选出来与第一个位置交换,再继续遍历,选出第二个最小放到第二个位置上

def select_sort(alist):
	'''选择排序'''
	n=len(alist)
	for i in range(n-1):
	min_index=i
		for j in range(i+1,n):
			if alist[min_index] > alist[j]:
				min_index=j
		alist[i],alist[min_index] = alist[min_index],alist[i]
时间复杂度
  • 最优时间复杂度:O(n^2)
  • 最坏时间复杂度:O(n^2)
  • 稳定性:不稳定(如对于升序每次选择最大的元素放在最右边)
#例如[(23,2),(2,1),(7,1),(23,1),(8,1),(5,1)]用选择排序对它进行升序排序,相同的比较第二个
先遍历一遍,将最大的放在最右边
原本(23,2)是在(23,1)的左边,经过选择排序后,(23,2)在(23,1)的右边了,它们原本的顺序发生了改变

插入排序

插入排序(英语:Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从

后向前扫描,找到相应位置并插入。插入排序在实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供

插入空间。

插入排序就是从将列表分为俩块区,左边为有序区,右边为无序区,刚开始左边有序区只有第一个位置的元素,然后从第二个元素开始,开始遍历无序区,将无序区的每个元素跟有序区从右边到左边去挨个比较,插入到正确的位置。

def insert_sort(alist):
	'''插入排序'''
	n=len(alist)
	#从第二个位置,即下标为1的元素开始向前插入,指有多少个元素要执行这样过程
		for j in range(1,n):
			#从第i个元素开始向前比较,如果小于前一个就交换位置,否则就不用,内层循环
			i=j
			while i>0:
				if alist[i] < alist[i-1]
					alist[i],alist[i-1] = alist[i-1],alist[i]
					i-=1
				else:
					break
时间复杂度
  • 最优时间复杂度 :O(n)(升序排列,序列已经处于升序状态)
  • 最坏时间复杂度 :O(n^2)
  • 稳定性: 稳定

希尔排序

希尔排序的基本思想是:将数组列在一个表中并对列分别进行插入排序,重复这过程,不过每次用更长的列(步长更长了,列数更少了)来进行。最后整个表就只有一列了。将数组转换至表是为了更好地理解这算法,算法本身还是使用数组进行排序。

例如,假设有这样一组数[131494338225599465234527732539101,如果我们以步长为5开始进行排序,我们可以通过将这列表放在有5列的表中来更好地描述算法,这样他们就应该看起来是这样(竖着的元素是步长组成):

13 14 94 33 82
25 59 94 65 23
45 27 73 25 39
18

然后对每列进行排序

10 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45

将上述四行数字,依序接在一起时我们得到:[10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45]。这时10已经移至正确位置了,然后再以3为步长进行排序:

10 14 73
25 23 13
27 94 33
39 25 59
94 65 82
45

排序之后:

10 14 13
25 23 33
27 25 59
39 65 73
45 94 82
94
希尔排序实现
def shell_sort(alist):
	'''希尔排序'''
	n=len(alist)
	gap=n//2		#每个的间隙
	while gap>0:
		#插入算法与普通的插入算法的区别就是gap步长
        for i in range(gap,n):
            j=i
            while j>0:
                if alist[j]<alist[j-1]:
                    alist[j],alist[j-1] = alist[j-1],alist[j]
                    j-=gap
                else:
                    break
        #缩短步长
        gap//=2
时间复杂度
  • 最优时间复杂度 :根据步长序列的不同而不同,比如,可能第一次步长取9,第二次取2,第三次取1就可以了
  • 最坏时间复杂度:O(n^2)
  • 稳定性:不稳定

快速排序

快速排序(英语:Quicksort),又称划分交换排序,通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

步骤为:
1.从数列中挑出一个元素,称为“基准"(pivot),
2.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个 分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition).操作。
3.递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

先用一个变量s存储第一个元素,目的就是将后面的元素与第一个比较,将比它大的放在它右边,比它小的放在他的左边,然后在把左边跟右边分开执行相同的操作,这样往复下来,就排好序了。定义俩个游标low跟high,一个在最左边,一个在最右边,先动右边游标,若右边游标指的元素比变量s大或者相等的话,右边则就往左移,直到右边游标指的元素比变量s小时,就把这个元素给左边游标,然后就开始动左边游标,左边游标指的元素比变量s小的话,就往右移,直到左边游标指的元素比变量s大,就把这个元素给右边游标,然后就动右边游标,循环操作,直到左边游标与右边游标重合就结束,然后把变量s赋给游标,这样s的值的左边都比它小,右边的都比他大;然后把左边跟右边分别进行这个操作,用递归方式。

def quick_sort(alist,first,last):
	'''快速排序'''
	if first == last:				#递归结束条件,当子列表只有一个元素时,结束
		return
	mid_value = alist[first]		#先定义一个变量来存储第一个元素
	#定义俩个游标,一个最左边,一个最右边,俩个往中间靠拢
	low=first
	high=last
	while low<high:
		#high左移
		while low<high and alist[high]>=mid_value:  #有等号是为了把与mid_value元素相等的都放在右边,也可以放在左边
			high-=1						#让high游标往左边移动
		alist[low] = alist[high]		#不满足high游标指的元素比mid_value大时,就给low游标
		
        while low < high and alist[low] < mid_value:
            low+=1							#让low游标往右边移动
		alist[high] = alist[low]		#不满足low游标指的元素比mid_value小时,就给high游标
	#从循环退出时,low==high,俩个游标重合
	alist[low]=mid_value
	
	#对low左边的子列表执行快速排序
	quick_sort(alist,first,low-1)
	#对low右边的子列表进行快速排序
	quick_sort(alist,low+1,last)
	
时间复杂度
  • 最优时间复杂度 :O(nlogn)
  • 最坏时间复杂度: O(n^2)
  • 稳定性:不稳定

归并排序

归并排序是采用分治法的一个非常典型的应用。归并排序的思想就是先递归分解数组,再合并数组。

将数组分解最小之后,然后合并两个有序数组,基本思路是比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可

def merge_sort(alist):
	'''归并排序'''
	n=len(alist)
	if n<=1:
		return alist
	mid = n//2
	
	#left 采用归并排序后形成的有序的新列表
	left_li = merge_sort(alist[:mid])
	
	#right 采用归并排序后形成的有序的新列表
	right_li = merge_sort(alist[mid:])
	
	#将俩个有序的子序列合并为一个新的整体
	left_pointer,right_pointer = 0,0
	result = []
	
	while left_pointer < len(left_li) or right_pointer < len(right_li):
		if left_li[left_pointer] <= right_li[right_pointer]:
			result.append(left_li[left_pointer])
			left_pointer += 1
		else:
			result.append(right_li[right_pointer])
			right_pointer+=1
			
		result += left_li[left_pointer:]
		result += right_li[right_pointer:]
		
if __name__ == '__main__':
	li=	[54,26,93,17,77,31,44,55,20]
	sorted_li = merge_sort(li)
	print(sorted_li)
时间复杂度
  • 最优时间复杂度:O(nlogn)
  • 最坏时间复杂度 :O(nlogn)
  • 稳定性:稳定

常见排序算法效率比较

排序方法平均情况最好情况最坏情况辅助空间稳定性
冒泡排序O(n^2)O(n)O(n^2)O(1)稳定
选择排序O(n^2)O(n^2)O(n^2)O(1)不稳定
插入排序O(n^2)O(n)O(n^2)O(1)稳定
希尔排序O(nlogn)~O(n^2)O(n^1.3)O(n^2)O(1)不稳定
堆排序O(nlogn)O(nlogn)O(logn)O(1)不稳定
归并排序O(nlogn)O(nlogn)O(nlogn)O(1)稳定
快速排序O(nlogn)O(nlogn)O(n^2)O(logn)~O(n)不稳定

二分查找法

二分查找又称折半查找,优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除困难。因此,折半查找方法适用于不经常变动而查找频繁的有序列表。首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。

def binary_search(alist,item):
	'''二分查找法,递归'''
	n=len(alist)
	if n>0:
		mid=n//2
		if alist[mid] == item:
			return True
		elif item<alist[mid]:
			return binary_search(alist[:mid],item)
		else:
			return binary_search(alist[mid+1:],item)
	return False
def binary_search(alist,item):
	'''二分查找法,非递归'''
	n=len(alist)
	first = 0
	last = n-1
	while first <= last:
		mid=(first+last)//2
		if alist[mid] == item:
			return True
		elif item <alist[mid]:
			last = mid-1
		else:
			first = mid-1
	return Flase
时间复杂度
  • 最优时间复杂度:O(1)
  • 最坏时间复杂度:O(logn)

概念:树(英语:tree)是一种抽象数据类型(ADT)或是实作这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由n(>=1)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。

它具有以下的特点:

  • 每个节点有零个或多个子节点;
  • 没有父节点的节点称为根节点;
  • 每一个非根节点有并且只有一个父节点;
  • 除了根节点外,每个子节点可以分为多个不相交的子树;
树的种类
  • 无序树:树中任意节点的子节点之间没有顺序关系,这种树称为无序树,也称为自由树:

  • 有序树:树中任意节点的子节点之间有顺序关系,这种树称为有序树;

    • 二叉树:每个节点最多含有两个子树的树称为二叉树

      • 完全二叉树:对于一颗二叉树,假设其深度为d(>)。除了第d层外,其它各层的节点数目均已
        达最大值,且第层所有节点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树,其中
        满二叉树的定义是所有叶节点都在最底层的完全二叉树:
      • 平衡二叉树(AVL树):当且仅当任何节点的两棵子树的高度差不大于1的二叉树;
      • 排序二叉树(二叉查找树(英语:Binary Search Tree),也称二叉搜索树、有序二叉树)
    • 霍夫曼树(用于信息编码):带权路径最短的二叉树称为哈夫曼树或最优二叉树;

    • B树:一种对读写操作进行优化的自平衡的二叉查找树,能够保持数据有序,拥有多余两个子树。

常见的一些树的应用场景

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

二叉树

二叉树是每个节点最多有俩个子树的树结构

树其实是对链表的一个扩充

二叉树代码实现

二叉树每个节点最多只有俩个,我们添加元素时,就从根节点开始一层一层开始去检查,把跟节点放入列表[root],拿出根节点,看它左孩子是否为空,为空就把节点添加到这个位置,若是不为空,就把这个位置的节点追加到列表里[A];然后再看他的右孩子,若是为空,则就把节点添加这个位置,若是不为空,则把这个位置的节点追加到列表里[A,B];然后再从列表第一个位置取出节点,重复上述操作;这种就是一端追加,另一端取出,跟队列的方式是一样

class Node(object):
	def __init__(self,item):
		self.elem = item
		self.lchild = None
		self.rchild = None

class Tree(object):
	'''二叉树'''
	def __init__(self):
		self.root = None
	
	def add(self,item):
		'''添加节点'''
		node = Node(item)
		if self.root is None:
			self.root = node
			return 
		queue = [self.root]
		while queue:		
            cur_node = queue.pop(0)
            if cur_node.lchild is None:
                cur_node.lchild = node
                return
            else:
            	queue.append(cur_node.lchild)
            if cur_node.rchild is None:
            	cur_node.rchild = node
            	return
            else:
            	queue.append(cur_node.rchild)
        
     def breadth_travel(self):		
     	'''广度遍历,跟添加节点的方式一样,一层一层的把节点打印出来'''
     	if self.root is None:
     		return
     	queue = [self.root]
     	while queue:
     		cur_code = queue.pop(0)
     		print(cur_code,end='')
     		if cur_code.lchild is not None:
     			queue.append(cur_node.lchild)
     		if cur_code.rchild is not None:
     			queue.append(cur_node.rchild)
     			
     def preorder(self,node):
        '''先序遍历'''
        if node is None:
            return
        print(node.elem,end='')
        self.preorder(node.rchild)
        self.preorder(node.lchild)
	
    def inorder(self,node):
        '''中序遍历'''
        if node is None:
            return
        self.preorder(node.rchild)
        print(node.elem,end='')
        self.preorder(node.lchild)

    def postorder(self,node):
        '''后序遍历'''
        if node is None:
            return
        self.postorder(node.rchild)
        self.postorder(node.lchild)
        print(node.elem,end='')
        
if __name__ == '__main__':
	tree=Tree()
	tree.add(0)
	tree.add(1)
	tree.add(2)
	tree.add(3)
	tree.add(4)
	tree.add(5)
	tree.breadth_travel()
	tree.preorder(tree.root)
	tree.inorder(tree.root)
	tree.postorder(tree.root)
二叉树遍历

分为俩种:深度优先遍历广度优先遍历

深度优先遍历一般用遍历,广度优先遍历一般用队列。一般情况下能用递归实现的算法大部分也能用堆栈来实现。

广度优先遍历

一层一层从左到右去遍历

def breadth_travel(self):		
     	'''广度遍历,跟添加节点的方式一样,一层一层的把节点打印出来'''
     	if self.root is None:
     		return
     	queue = [self.root]
     	while queue:
     		cur_code = queue.pop(0)
     		print(cur_code,end='')
     		if cur_code.lchild is not None:
     			queue.append(cur_node.lchild)
     		if cur_code.rchild is not None:
     			queue.append(cur_node.rchild)
深度优先遍历

对于一颗二叉树,深度优先遍历是沿着树的深度遍历树的节点,尽可能深的搜索树的分支。

那么深度遍历有重要的三种方法。这三种方式常极用于访问树的节点,它们之间的不同在于访问每个节点的
次序不同。这三种遍历分别加叫做先序遍历,中序遍历后序遍历

  • 先序遍历 我们先访问根节点,然后递归使用先序遍历访问左子树,再递归使用先序遍历访问右子树

​ 根节点 > 左子树 > 右子树

  • 中序遍历 在中序遍历中,我们递归使用中序遍历访问左子树,然后访问根节点,最后再递归使用中序遍历访问右子树
    左子树>根节点>右子树

  • 后序遍历 在后序遍历中,我们先递归使用后序遍历访问左子树和右子树,最后访问根节点
    左子树>右子树>很节点

![联想截图_20240604204133](C:\Users\谢霄峰\Pictures\联想截图\联想截图_20240604204133.png)def preorder(self,node):
	'''先序遍历'''
	if node is None:
		return
	print(node.elem,end='')
	self.preorder(node.rchild)
	self.preorder(node.lchild)
	
def inorder(self,node):
	'''中序遍历'''
	if node is None:
		return
	self.preorder(node.rchild)
	print(node.elem,end='')
	self.preorder(node.lchild)
	
def postorder(self,node):
	'''后序遍历'''
	if node is None:
		return
	self.postorder(node.rchild)
	self.postorder(node.lchild)
	print(node.elem,end='')
tree.add(0)
tree.add(1)
tree.add(2)
tree.add(3)
tree.add(4)
tree.add(5)
tree.breadth_travel()
tree.preorder(tree.root)
tree.inorder(tree.root)
tree.postorder(tree.root)



#### 二叉树遍历

分为俩种:**深度优先遍历** 、**广度优先遍历**

深度优先遍历一般用遍历,广度优先遍历一般用队列。一般情况下能用递归实现的算法大部分也能用堆栈来实现。



##### 广度优先遍历

一层一层从左到右去遍历

def breadth_travel(self):
‘’‘广度遍历,跟添加节点的方式一样,一层一层的把节点打印出来’‘’
if self.root is None:
return
queue = [self.root]
while queue:
cur_code = queue.pop(0)
print(cur_code,end=‘’)
if cur_code.lchild is not None:
queue.append(cur_node.lchild)
if cur_code.rchild is not None:
queue.append(cur_node.rchild)




##### 深度优先遍历

对于一颗二叉树,深度优先遍历是沿着树的深度遍历树的节点,尽可能深的搜索树的分支。

那么深度遍历有重要的三种方法。这三种方式常极用于访问树的节点,它们之间的不同在于访问每个节点的
次序不同。这三种遍历分别加叫做**先序遍历**,**中序遍历**和**后序遍历**

+ **先序遍历** 我们先访问根节点,然后递归使用先序遍历访问左子树,再递归使用先序遍历访问右子树

​       根节点 > 左子树 > 右子树

+ **中序遍历** 在中序遍历中,我们递归使用中序遍历访问左子树,然后访问根节点,最后再递归使用中序遍历访问右子树
  左子树>根节点>右子树

+ **后序遍历** 在后序遍历中,我们先递归使用后序遍历访问左子树和右子树,最后访问根节点
  左子树>右子树>很节点

def preorder(self,node):
‘’‘先序遍历’‘’
if node is None:
return
print(node.elem,end=‘’)
self.preorder(node.rchild)
self.preorder(node.lchild)

def inorder(self,node):
‘’‘中序遍历’‘’
if node is None:
return
self.preorder(node.rchild)
print(node.elem,end=‘’)
self.preorder(node.lchild)

def postorder(self,node):
‘’‘后序遍历’‘’
if node is None:
return
self.postorder(node.rchild)
self.postorder(node.lchild)
print(node.elem,end=‘’)


![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/8bcd4d4046ec4d7db16a1793adc718fa.png#pic_center)




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值