链表
介绍
链表是由结点组成的,每个节点包含2个域,一个元素域,存放元素数据,一个链接域,通过next指针指向下一个结点,最后一个节点的链接域指向None
1.特点
不需要连续的存储空间
2.链表结构
单链表(单向链表)是链表的一种形式,它的每个节点包含两个域一个元素域和一个链接域。这个链接指向链表中的下一个节点,而最后一个节点的链接域则指向一个空值。
- 表元素域elem用来存放具体的数据
- 链接域next用来存放下一个节点的位置
- 变量head指向链表的头节点(首节点)的位置,从head出发能找到表中的任意节点
代码实现
通过append(item)方法不断添加元素,最终实现单链表
is_empty() 链表是否为空
length() 链表长度
travel() 遍历整个链表
add(item) 链表头部添加元素
append(item) 链表尾部添加元素
insert(pos, item) 指定位置添加元素
remove(item) 删除结点
serach(item) 查找结点是否存在
# 如果node是一个结点:
# 获取结点元素:node.item
# 获取下一个结点:node.next
class SingleNode(object):
"""单链表的结点"""
def __init__(self, item):
# item元素存放位置
self.item = item
# next是下一个节点的标识
self.next = None
class SingLinkList(object):
# 单链表的实现
def __init__(self, node): # node指上面类实例化的对象
# 首结点
self.head = node
# 判断链表是否为空
def is_empty(self):
# 主要判断头结点是否为空,为空则判链表为空
if self.head is None:
return True
else:
return False
# 获取链表的长度
def length(self):
cur = self.head # 游标记录当前所在的位置,head不能改变位置,所以需要辅助游标
count = 0 # 记录链表长度
while cur is not None: # 通过判断当前结点是否为空
cur = cur.next # 不为空时,则将下一个结点变为当前节点,然后数量加1,直到当前节点为None
count += 1
return count
# 遍历链表
def travel(self):
cur = self.head
while cur is not None:
print(cur.item)
cur = cur.next
# 链表增加节点的三种情况
# 情况1
"""
1.add(item) 链表头部添加结点
如何在头部添加一个新节点?
首先创建一个新节点new_node,将该节点的next指向老的头结点,然后将head变量指向现在的new_node节点
new_node.next = head
head = new_node
"""
def add(self, item):
# 新节点
node = SingleNode(item)
node.next = self.head
self.head = node
# 情况2
"""
2.append(item)链表尾部添加节点
只需要将尾节点的next指向新节点即可
尾结点.next = 新节点
如何找到尾结点?
可以通过判断cur.next是否为空来找到尾结点
"""
def append(self, item):
# 新节点
new_node = SingleNode(item)
# 判断是否是空链表
if self.is_empty():
# 将新节点=头结点
self.head = new_node
else:
# 辅助游标
cur = self.head
# 通过判断游标指向的下一个节点是否为空来判断下一个节点
while cur.next is not None:
cur = cur.next
# 将尾节点的next指针指向新节点
cur.next = new_node
# 情况3
"""
3.insert(pos, item)指定位置添加节点
假设pos=2,则表示要在下标为2的位置上插入一个新节点
操作步骤
1.首先将新节点的next指向游标的下一个节点
node.next = cur.next
2.然后再将游标的下一个节点指向新节点
cur.next = node
3.找到插入位置的前一个节点
count = 0
while count < pos-1:
cur = cur.next
count += 1
"""
def insert(self, pos, item):
"""
pos: 位置
item: 元素
"""
# pos小于等于0,则在头结点插入新节点
if pos <= 0:
self.add(item)
# pos大于等于链表长度,则在尾部添加新节点
elif pos >= self.length():
self.append(item)
else:
# 新节点
node = SingleNode(item)
# 游标
cur = self.head
# 计数
count = 0
# 找到插入位置的前一个节点
while count < pos-1:
cur = cur.next
count += 1
# 完成新节点的插入
node.next = cur.next
cur.next = node
# 删除节点
"""
1.remove(item)删除节点
情况1.要删除的元素在头部,直接将head指向下一个节点
head = cur.next
"""
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
# 查找节点是否存在
"""
2.search(item)查找节点是否存在
"""
def search(self, item):
cur = head
while cur is not None:
# 找到指定元素
if cur.item = item:
return True
cur = cur.next
return False
栈
什么是栈?
栈它是一种运算受限的线性表,其限制是仅允许在表的一端进行插入和删除运算,这一端被称为栈顶,相对的,把另一端称为栈底,同时栈的结构特点让它在处理数据的时候符合了先进后出的特点。
栈的作用
计算机里面的栈其实有着举足轻重的作用,栈是计算机系统里面CPU结构的一部分。
栈到底有什么用呢?
函数里面有可能要使用到局部变量,不能总是用全局变量,而局部变量在函数使用完毕之后就销毁了,那么局部变量存储到哪既能不浪费空间又能及时销毁。
代码实现
class Stack(object):
"""栈:先进后出"""
def __init__(self):
self.__items = []
def push(self, item):
"""进栈"""
self.__items.append(item)
def pop(self):
"""出栈"""
self.__items.pop()
队列
什么是队列
队列是一种特殊的线性表,特殊之处在于它只允许在表的头部(front)进行删除操作,而在表的尾部(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头
队列的作用
任务处理类的系统:
先把用户发起的任务请求接收过来存到队列中,然后后端开启多个应用从队列中取任务进行处理,队列起到了缓冲压力的作用
代码实现
class Queue(object):
def __init__(self):
self.items = []
# 尾部添加数据
def enqueue(self, item):
self.items.append(item)
# 头部删除数据
def dequeue(self):
self.items.pop(0)
# 判断队列是否为空
def is_empty(self):
return self.items == []
# 返回队列的大小
def size(self):
return len(self.items)
双端队列
什么是双端队列
双端队列是一种具有队列和栈的性质的数据结构
双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行
双端队列可以在队列任意一端入队和出队
代码实现
class Deque(object):
"""双端队列"""
def __init__(self):
self.items = []
# 为空判断
def is_empty(self):
return self.items == []
# 队列大小
def size(self):
return len(self.items)
# 头部添加数据
def add_front(self, item):
self.items.insert(0, item)
# 尾部添加数据
def add_rear(self, item):
self.items.append(item)
# 头部删除数据
def remove_front(self):
self.items.pop(0)
# 尾部删除数据
def remove_rear(self):
self.items.pop()
排序算法
冒泡排序
重复的走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。走访元素的工作是重复的进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成
冒泡排序:
最差时间复杂度:O(n^2)
最优时间复杂度:O(n)
算法稳定性:稳定算法
代码实现
a = [20, 2, 56, 33, 66, 1, 3, 78, 6]
for i in range(len(a)-1):
# 计数,主要是为了防止该数列本来就是有序的,但是还是要进行很多次循环比较
count = 0
for j in range(len(a) - 1 - i):
if a[j] > a[j + 1]:
a[j], a[j + 1] = a[j + 1], a[j]
count += 1
if count == 0:
break
print(a)
选择排序
什么是选择排序
第一次从该待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从该待排序的未排序元素中寻找到最小(大)元素,然后放到已排序的序列末尾,依次类推,直到全部待排序的数据元素的个数为零。
代码实现
def select_sort(alist):
n = len(alist)
for j in range(0, n-1):
# 假定最小值的下标
min_index = j
for i in range(j+1, n):
if alist[i] < alist[min_index]:
min_index = i
if min_index != j:
alist[j], alist[min_index] = alist[min_index], alist[j]
插入排序
什么是插入排序
插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的,个数加一的有序数据,算法适用于少量数据的排序
代码实现
def insert_sort(alist):
n = len(alist)
for j in range(1, n):
for i in range(j,0,-1): #跟前一个进行比较
if alist[i] < alist[i-1]:
alist[i],alist[i-1] = alist[i-1],alist[i]
else:
break
快速排序
什么是快速排序
基本思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
快速排序算法通过多次比较和交换来实现排序,其排序流程如下:
1.首先设定一个分界值,通过该分界值将数组分成左右两部分
2.将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边
此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值
3.然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也做类似处理
4.重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左,右两个部分各数据排序完成后,整个数组的排序也就完成了。
代码实现
def quick_sort(alist, start, end):
# 结束条件
if start >= end:
return
# 界限值
mid = alist[start]
# 左右游标
left = start
right = end
while left < right:
# 从右边开始找寻小于mid的值,归类到左边
while alist[right] >= mid and left < right:
right -= 1
alist[left] = alist[right]
# 从左边开始找寻大于mid的值,归类到右边
while alist[left] < mid and left < right:
left += 1
alist[right] = alist[left]
# 循环一旦结束了,证明找到了mid应该在的位置
alist[left] = mid
# 递归操作
quick_sort(alist, start, left-1)
quick_sort(alist, right+1, end)
二分查找-递归版本
什么是二分查找
二分查找又称折半查找,它是一种效率较高的查找方法
原理:将数组分为三部分,依次是中值前,中值,中值后
将要查找的值与中值进行比较,若小于中值则在中值前面找,若大于中值则在中值后面找,等于中值时直接返回
要求:必须采用顺序存储结构,必须按关键字大小有序排列
代码实现
# 递归版本
def binary_search(alist, item):
# 数列的长度
n = len(alist)
# 递归的结束条件
if n == 0:
return False
# 中间值
mid = n//2
if item == alist[mid]:
return True
elif item < alist[mid]:
return binary_search(alist[0:mid], item)
elif item > alist[mid]:
return binary_search(alist[mid+1:], item)
# 非递归版本
def binary_search(alist, item):
# 设置起始位置,获取中间值
start = 0
end = len(alist) - 1
while start <= end:
# 获取中间值
mid = (start+end)//2
if item == alist[mid]:
return True
elif item < alist[mid]:
end = mid -1
elif item > alist[mid]:
start = mid +1
树
什么是树
树就是一种非线性结构
它是用来模拟具有树状结构性质的数据集合,它是由n(n>=1)个有限节点组成一个具有层次关系的集合。把它叫做"树"是因为它看起来像一颗倒挂的树,也就是说它是根朝上,而叶朝下的,它具有以下的特点:
1.每个节点有零个或多个子节点
2.没有父节点的节点称为根节点
3.每一个非根节点有且只有一个父节点
4.除了根节点外,每个子节点可以分为多个不相交的子树
树的术语
节点的度:一个节点含有的子节点的个数称为该节点的度
树的度:一颗树中,最大的节点的度称为树的度
叶节点或终端节点:度为零的节点
父亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点
兄弟节点:具有相同父节点的节点互称为兄弟节点
节点的层次:从根开始定义起,根为第一层,根的子节点为第2层,依次类推
树的高度或深度:树中节点的最大层次
堂兄弟节点:父节点在同一层的节点互为堂兄弟
节点的祖先:从根到该节点所经分支上的所有节点
子孙:以某节点为根的子树中任一节点都称为该节点的子孙
森林:由m(m>=0)颗互不相交的树的集合称为森林
树的种类
无序树:树中任意节点的子节点之间没有顺序关系,这种树称为无序树,也称为自由树
有序树:树中任意节点的子节点之间有顺序关系,这种树称为有序树
有序树:
霍夫曼树:带权路径最短的二叉树称为哈夫曼树或最优二叉树
B树:一种对读写操作进行优化的自平衡的二叉查找树,能够保持数据有序,拥有多于两个的子树
二叉树:每个节点最多含有两个子树的树称为二叉树
二叉树的种类
完全二叉树:对于一颗二叉树,假设其深度为d(d>1)。除了第d层外,其他各层的节点数目均已达到最大值,且第d层所在节点从左向右连续的紧密排列,这样的二叉树被称为完全二叉树,其中满二叉树的定义是所有叶节点都在最底层个的完全二叉树
平衡二叉树:当且仅当任何节点的两颗子树的高度差不大于1的二叉树
排序二叉树:也称为二叉搜索树,有序二叉树
排序二叉树的要求:
1.若左子树不空,则左子树上所有节点的值均小于它的根节点的值
2.若右子树不空,则右子树上所有节点的值均大于它的根节点的值
3.左,右子树分别为二叉排序树
排序二叉树包含空树
二叉树的存储
顺序存储:将数据结构存储在固定的数组中,虽然在遍历速度上有一定的优势,但因所占空间比较大,是非主流二叉树存储方式,二叉树通常以链式存储
链式存储:由于对节点的个数无法掌握,常见树的存储表示都转换成二叉树进行处理,子节点个数最多为2
树的应用场景
1.xml,html等,那么编写这些东西的解析器的时候,不可避免用到树
2.路由协议就是使用了树的算法
3.mysql数据库索引
4.文件系统的目录结构
5.所以很多经典的AI算法其实都是树搜索,此外机器学习中的decision tree也是树结构
二叉树的概念
二叉树是每个节点最多有两个子树的树结构
通常子树被称为"左子树"和“右子树”
二叉树的性质
性质1:在二叉树的第i层上至多有2^i-1个节点(i<0)
性质2:深度为k的二叉树至多有2^k-1个结点(k>0)
性质3:对于任意一颗二叉树,如果其叶结点数为N0,而度数为2的结点总数为N2,则N0=N2+1
性质4:最多有n个结点的完全二叉树的深度必为log2(n+1)
性质5:对完全二叉树,若从上至下,从左至右编号,则编号为i的结点,其左孩子编号必为2i,其右孩子编号必为2i+1,其父节点的编号必为i//2(i=1时为根除外)
代码实现
class Node(object):
def __init__(self, item):
self.item = item
self.lchild = None
self.rchild = None
class BinaryTree(object):
"""完全二叉树"""
def __init__(self, node=None):
self.root = node
# 添加节点
def add(self):
if self.root == None:
self.root = Node(item)
return
# 创建队列
queue = []
# 从尾部添加数据
queue.append(self.root)
while True:
# 从头部取出数据
node = queue.pop(0)
# 判断左节点是否为空
if node.lchild == None:
node.lchild = Node(item)
return
else:
queue.append(node.lchild)
if node.rchild == None:
node.rchild = Node(item)
return
else:
queue.append(node.rchild)
# 广度优先遍历
def breadh_travel(self):
if self.root == None:
return
# 队列
queue = []
# 添加数据
queue.append(self.root)
while len(queue) >0 :
# 取出数据
node = queue.pop(0)
print(node.item, end="")
# 判断左右子节点是否为空
if node.lchild is not None:
queue.append(node.lchild)
if node.rchild is not None:
queue.append(node.rchild)
二叉树的三种深度优先遍历
先序遍历:0 1 3 7 8 4 9 2 5 6 根 左 右
中序遍历:7 3 8 1 9 4 0 5 2 6 左 根 右
后序遍历:7 8 3 9 4 1 5 6 2 0 左 右 根
代码实现
class Node(object):
def __init__(self, item):
self.item = item
self.lchild = None
self.rchild = None
class BinaryTree(object):
"""完全二叉树"""
def __init__(self, node=None):
self.root = node
...
def preorder_travel(self, root):
"""先序遍历 根 左 右"""
if root is not None:
print(root.item, end="")
self.preorder_travel(root.lchild)
self.preorder_travel(root.rchild)
def inorder_travel(self, root):
"""中序遍历 左 根 右"""
if root is not None:
print(root.item, end="")
self.inorder_travel(root.lchild)
self.inorder_travel(root.rchild)
def postorder_travel(self, root):
"""后序遍历 根 左 右"""
if root is not None:
print(root.item, end="")
self.postorder_travel(root.lchild)
self.postorder_travel(root.rchild)