买了88元的数据结构课程:
1、别人敲代码的速度
2、别人写代码的思维逻辑
脚本语言:解释语言
类(class)=数据+方法
class中各种def函数
时间复杂度:
计算出循环的次数,然后取最高次幂对应的多项式即为时间复杂度(因此时间复杂度只是执行次数的一个大概的估算)
线性结构:内存连续,下标访问
array(很少用到),list
list的方法
append,pop(只操作了一个元素)时间复杂度为o(1)
insert,remove (操作多个元素),时间复杂度为o(n)
链式结构:不连续(一个指一个),无下标访问
链表
头指针的作用:为了使用方便,自己理解是为了更好找到链表的开头
为了使用方便,通常会加一个链表头指针,其数据字段不存放任何数据。如环形链表中头指针只是一个空的节点,其左指针指向链表的最后一个节点,右指针指向第一个节点
指针指向下一个节点的内存地址
链表头指针和第一个节点是有区别的
链表节点定义:
class Node():#这里的名字Node根据节点的真实含义取名,这里就是定义的节点的结构
def __init__(self):
self.val=None
self.next= None
head=Node()#建立链表头部。因为初始化中值为None,所以头节点中没有值,指向None
head.next= None
#添加一个指针ptr
ptr=head
#链表尾部添加元素(将ptr向后指)
new_data=Node()
'''加入新元素的值,这里需要自己输入数据字段的值,这与下面的遍历直接取值是不一样的'''
ptr.next=new_data
new_data.next=None
#将存储指针的位置向前移动一位
ptr=ptr.next
#遍历链表,加入while循环
while ptr!=None:
sum=sum+ptr.val#注意取出当前节点的值
'''当然这里也能统计已经遍历的节点个数 num+=1'''
ptr=ptr.next
链表头部,尾部,中间添加元素
#发现都是先考虑向后指 #链表头部添加元素,新节点的指针指向头指针first指向的内容,头指针first向前移动一位 newnode.next=head head=newnode #尾部添加元素 newnode.next=None #之前尾部节点指向newnode,这个自己不知道怎么表示?用ptr
ptr.next=newnode #中间添加元素 newnode.next=ptr.next ptr.next=newnode
将上面三种情况综合如下:
#这里自己有个疑问,在l链表中间插入值时,ptr此时所指位置怎么确定?
#当插入元素时,肯定会指定插入在那个元素的后面,这个时候就要把那个元素的位置找出来。通过什么找?通过节点中的数据字段找,如编号
def findnode(head,position):#找节点时需要把头节点和插入位置传进来
ptr=head
while ptr!=None:#错写为if ptr!=None:
if ptr.val==position:
return ptr
ptr=ptr.next#ptr向后移,ptr在前ptr.next在后
return ptr
position=int(input())
ptr=findnode(head,position)
#中间添加元素
newnode.next=ptr.next
ptr.next=newnode 按照书本补充完整
删除链表中的元素:
#删除头节点,引入ptr
head=Node()
ptr=head
ptr=ptr.next
#删除尾节点,这里需要找到删除节点的前一个节点
ptr.next=tail#tail需要定义
ptr.next = None
#删除中间元素(主要注意此情况),同样通过数据字段,如员工编号来确定删除节点的位置
remove=ptr.next#给中间元素添加个指针,这里引入中间元素remove
ptr.next=remove.next#再把ptr指向后面
将上面三种综合如下:
def delete_node(head,del_ptr):#传入要删除节点的指针
ptr=head#引入一个新的移动指针
if del_ptr.val==head.val:#如果确定要删除的节点的数据字段和头节点的数据字段相同,说明要删除头节点
ptr=ptr.next
else:
while ptr.next !=del_ptr:#错写成while ptr.next!=None:,这个while是要找出删除的节点的前一个节点ptr
ptr=ptr.next#不是删除头节点,就把移动指针向后移,直到找到要删除的节点
if del_ptr.next== None:
ptr.next=None
else:
ptr.next=del_ptr.next
return head#返回链表只需要返回链表头,这也是传进来的参数
#确定删除节点位置
while True:
delete_num=int(input())
ptr=head
find=0
while ptr!=None: #这一行循环写时漏掉
if ptr.val==delete_num:
head=delete_node(head,ptr)#这里传入的参数就是要删除的节点位置,返回值链表头直接给链表头,根据这个链表头,可以在下面打印出整个链表
find+=1
ptr=ptr.next#一直往后找,没找到,移动指针向后移动一位
if find==0:#说明一遍走完都没找到要删除的元素对应的节点
print("没有找到")
代码实现链表的反转
temp=temp.next是使temp指向下一结点。
temp.next=temp是使temp本身的next指针指向自己
反转链表注意以下三点:
三个指针last,ptr,ptr_next
每次只考虑当前指针指向前一个节点
新位置赋值给要移动的指针,将指针移动到新位置,如:last = ptr
class Solution(object):
def reverseLinkedList(self, head):
if head == None:#head表示原链表的头节点。如果没有头节点,反转仍为空,返回空
return []
last = None #添加一个指针,初始化为空,用于反转后的最后一个节点。同时会在while中不断后移
ptr= head #添加另一个指针
while (temp != None):
ptr_next = ptr.next#取出头节点的下一个节点
ptr.next = last
last = ptr
ptr= ptr_next#指向头节点的指针现在指向第二个节点
return last
①head == None: #如果头节点就是空的
判断是链表是否存在环
链表本身可能不只是一个单纯的环状结构
所在位置相等来判断是否走到相同位置:if slowPtr == fastPtr
#定义一个链表
class Node():
def __init__(self, val=None):
self.val = val
self.next = next
def findbeginofloop(head): # 判断是否为环结构并且查找环结构的入口节点
slowPtr = head # 将头节点赋予slowPtr
fastPtr = head # 将头节点赋予fastPtr
loopExist = False # 默认环不存在,为False
if head == None: # 如果头节点就是空的,那肯定就不存在环结构
return False
while fastPtr.next != None and fastPtr.next.next != None: # fastPtr的下一个节点和下下个节点都不为空
slowPtr = slowPtr.next # slowPtr每次移动一个节点
fastPtr = fastPtr.next.next # fastPtr每次移动两个节点
'''
计算环长时
slowstepnum += 1
faststepnum += 2
'''
if slowPtr == fastPtr: # 当fastPtr和slowPtr的节点相同时,也就是两个指针相遇了
loopExist = True
print("存在环结构")
break
# 环长 = fast走的步长 - slow走的步长
if loopExist == True:
slowPtr = head
while slowPtr != fastPtr:
fastPtr = fastPtr.next
slowPtr = slowPtr.next
return slowPtr#返回环的入口点
print("不是环结构")
return False
#请记完整程序
if __name__ == "__main__":
node1 = Node(1)
node2 = Node(2)
node3 = Node(3)
node4 = Node(4)
node5 = Node(5)
node1.next = node2
node2.next = node3
node3.next = node4
node4.next = node5
node5.next = node2
print(findbeginofloop(node1).val)
输出结果:
扩展:怎么在环状入口添加新节点
①首先需要找到入口节点,根据上面程序找(书本都是给出了环形链表的头节点,因为都是纯粹的环状。如果不是纯粹的环状也要知道怎么去找头节点)
②在入口节点前添加节点:
新节点指向环状链表的头节点
环状链表的尾节点指向新节点,尾节点需要遍历
头指针指向新节点
newnode.next=slow#slow为找出的头节点
#找尾节点
ptr=slow
while ptr.next!=slow:#错写成while ptr.next!=None:
ptr=ptr.next
#能跳出while循环,说明已经找到了尾节点。此时只需要将尾节点指向新加入的节点
ptr.next=newnode
#最后将头指针指向新节点(注意是将新位置赋值给头指针,左右不要写反了)
head=newnode
链表
1、单链表:火车,节点由两个元素组成,数据value(数据值可能多个,一个框多个数据值)+指针next,指针会指向下一个元素的内存地址
只能单向遍历
链表的方法:
append appendleft时间复杂度为o(1)
find remove时间复杂度为o(n)
向尾部添加节点步骤:
①给新节点分配内存空间(类名对象的创建new_data=student(),类中value值定义,包括姓名和分数)
class student:
def ___init__(self):
self.name=""
self.score=0
self.next=None
②链表尾部的指针next指向新元素所在的内存位置
③新节点的指针指向None
注:向单链表的头节点前插入新节点会多一个步骤:将原链表的头指针指向新的节点
遍历单向列表
定义一个结构指针ptr指向链表头部
ptr不断指向下一个节点,直到指向None结束(ptr=ptr.next)
2、双链表(烽火科技问到怎么向双端链表尾部插入元素):
既然是双向链表,左右都有None
为什么要使用双向链表:单向链表和环形链表只能单向遍历,如果某处断裂,后面的数据将无法获取。而双向链表左边的链断了就可以从右边开始遍历
优化find remove时间复杂度为o(n)的问题,优化为o(1)问题
如果一节点的链接断开,可由反向链表进行遍历,从而可重建出完整的链表
无论是添加还是删除操作,都是从左指针/右指针开始指
数据value+2个指针(关键点就是多了一个指针,右指针指向后面的节点,左指针指向前面的节点),多了一个prevalue值,这个值可以向前指
双端链表尾部插入元素步骤:
①将链表最后一个节点的右指针指向新节点
②新节点左指针指向链表最后一个节点,新节点右指针指向None
双端链表任意位置插入元素步骤:
①将新节点加入链表ptr节点后
②ptr右指针指向新节点,新节点左指针指向ptr节点;新节点右指针指向ptr下一个节点,ptr下一个节点左指针指向新节点
双端链表头部插入元素步骤:原链表的头指针指向新节点
新节点右指针指向原链表的第一个节点,第一个节点左指针指向新节点
注:将新节点加入到双链表的第一个节点前会多一个步骤:将原链表的头指针指向新节点
添加节点
ptr=head
#左指针引入的是llink,右指针是rlink
#双端链表头加入节点
head.llink=newnode
newnode.rlink=head
head=newnode#新位置赋值给头指针
#双端链表尾加入节点,假设尾节点对应的指针目前为ptr
ptr.rlink=newnode
newnode.llink=ptr
newnode.rlink=None
#双端链表中加入节点,假设加入的位置已知为ptr
ptr.next=newnode
newnode.llink=ptr
newnode.rlink=ptr.next
ptr.rlink.llink=newnode
删除节点
head=head.rlink
#注意此时head的位置已经改变
head.llink=None
#删除尾节点,假设删除节点位置为ptr
ptr.llink.rlink=None
#删除中间节点,假设删除节点位置为ptr
ptr.llink.rlink=ptr.rlink
ptr.rlink.llink=ptr.llink
3、环形链表:摩天轮,可以从任何一个节点遍历链表上的所有节点,需要多一个指正空间,增删节点慢,因为每个节点必须处理两个指针
头指针,头节点区别:头指针指向头节点
如果链表头指针被破坏或遗失,整个链表就会遗失,并且浪费整个链表内存空间,因此考虑使用环形链表
把链表的最后一个节点的指针指向链表头部,而不是指向None,从而不用担心链表头指针遗失问题,因为每一个节点都可以是链表头部
判断一个链表是否为环形(Python方法)
堆栈:一个指针top
#这里使用列表List实现的堆栈
max=100#设置堆栈的大小,表示可以存储100个值的堆栈
global stack#将堆栈初始化为全局变量
stack=[0]*max#构造列表,通过列表来实现的堆栈
top=-1#top被初始化为1
def isEmpty():#判断堆栈是否为空:通过top指针来判断
if top==-1:
return True
else:
return False
#将数据压入堆栈
def push(data):#push需要传入数据
global top
global max
global stack
if top>=max-1:#索引是从0开始的,这里必须判断一下还能否加入数据
print("堆栈已满")
else:
top=top+1
stack[top]=data#存入数据
def pop():#pop不需要传入数据
global top
global max
global stack
if isEmpty():#这里必须判断一下堆栈是否为空,否则无法弹出数据
print("堆栈是空")
else:
val=stack[top]
top=top-1
同样也可以用链表来实现堆栈:算法复杂,但可以动态改变链表长度
列表实现堆栈:简单,但是必须声明列表的大小。如果堆栈本身大小是变动的,而列表声明过大时会浪费空间
top指针指向堆栈顶端,空的堆栈顶端top指向-1,向其中push元素后top自加
设有编号1,2,3的三辆火车,顺序开入栈式结构的站台,则可能出栈序列有多少种?5种
总共有6种情况,123,132,213,231,312,321
而312的出栈方式不可能,3先出栈,则1,2必须一直在栈中,1,2顺序入栈后的出栈顺序必须是2先出栈,1后出栈
如132出栈过程:1入栈1出栈,23同时入栈,32出栈 ,简单画个图即可
TOP(PUSH(i,s))将元素i压如堆栈中,再返回堆栈租顶端的元素
堆栈的特殊应用:
结合堆栈计算算术表达式的值(包含赋值的字母,括号,运算符号,数字)
汉诺塔问题:堆栈的应用,移动n个盘子所需的最小移动次数为2^n-1
递归算法:堆栈的应用,程序如下:
def dif2(x):
if x:
dif1(x)
def dif1(y):
if y>0:
dif2(y-3)
print(y,end=" ")
dif1(21)
print()输出结果为:3 6 9 12 15 18 21
结果解释:每调一次dif1都会打印一次y值,递归后最先打印的是最后面的元素
队列:两个指针front和rear
同样也可以用列表和链表实现队列
与堆栈只需要一个top指针指向堆栈顶端不同的是:队列必须使用两个指针front和rear(队列尾)分别指向队列的前端和末尾
#这里使用列表List实现的队列
max=100#表示可以存储100个值的队列
queue=[None]*max#设置队列的大小
front=-1
rear=-1
def enqueue(data):
global max
global front
global rear
if rear==max-1: #在队列中加入数据是在rear后加入
print("队列已满")
else:
rear+=1
queue[rear]=data
def dequeue():
global max
global front
global rear
if front==rear:#通过两端指针是否指向同一个位置判断队列是否为空
print("队列为空")
else:
front+=1
val=queue[front]#取出队首元素
双端队列:队列的任意一端都能加入或者删除元素
优先队列:不必要遵守队列先进先出的特性。其中的每一个元素都赋予一个优先级,最高优先级的元素最先输出
当各个元素按输入先后次序为优先级时(优先级大的先输入队列),就是一般的队列;如果按输入先后次序的倒序作为优先级(低优先级先输入队列),此优先队列为一个堆栈
图
图(G(V,E))的遍历,从某一个顶点V1开始,遍历访问G中的其它顶点,直到全部的顶点遍历完毕
如V表示所有顶点的组合V={A,B,C,D,E},E表示所有边的组合E={(A,B)(A,E)(B,C)(C,D)(C,E)(D,E)},注意E表示的是边组合
图遍历方法:深度优先搜索/遍历(DFS,deep-first search)和广度优先遍历(BFS,breadth-first search)
①深度优先遍历:从图的某一顶点开始遍历,被访问的顶点做上已 访问记号,接着遍历此顶点相连且未访问过的顶点中的任意一个顶点,继续做上标记
用到的数据结构:
堆栈,取栈顶元素,弹出的点即为标记点
递归
算法实现:书本231,注意两个遍历流程图区别
②广度优先遍历
用到的数据结构:
队列,取队列头的元素,取出的元素即为标记点
递归
DFS:类似前序遍历,将与根节点相连的节点压入堆栈,弹出栈顶元素(从上图横条右边取),并把与弹出元素相连且未访问的元素压入栈中
BFS:将与根相连的节点加入到队列中,取出队首元素(从上图横条左边取),并把与对手元素相连且未访问的元素压加入到队列中
DFS的另一种结果:1354276(是否正确?)