数据结构
文章目录
数据结构基本概念
什么是数据结构?
- 数据
数据即信息的载体,是能够输入到计算机中并且能被计算机识别、存储和处理的符号总称。
- 数据元素
数据元素是数据的基本单位,又称之为记录(Record)。一般数据元素由若干基本项组成。
- 数据结构
数据结构指的是数据元素及数据元素之间的相互关系,或组织数据的形式。
数据之间的结构关系
- 逻辑结构
表示数据之间的抽象关系(如邻接关系、从属关系等),按每个元素可能具有的直接前趋数和直接后继数将逻辑结构分为“线性结构”和“非线性结构”两大类。
- 存储结构
逻辑结构在计算机中的具体实现方法,分为顺序存储方法、链接存储方法、索引存储方法、散列存储方法。
逻辑结构(关系)
- 特点:
- 只是描述数据结构中数据元素之间的联系规律
- 是从具体问题中抽象出来的数学模型,是独立于计算机存储器的(与机器无关)
- 逻辑结构分类
- 线性结构
对于数据结构课程而言,简单地说,线性结构是n个数据元素的有序(次序)集合。
- 集合中必存在唯一的一个"第一个元素";
- 集合中必存在唯一的一个"最后的元素";
- 除最后元素之外,其它数据元素均有唯一的"后继";
- 除第一元素之外,其它数据元素均有唯一的"前驱"。
- 树形结构(层次结构):链式存储
树形结构指的是数据元素之间存在着“一对多”的树形关系的数据结构,是一类重要的非线性数据结构。在树形结构中,树根结点没有前驱结点,其余每个结点有且只有一个前驱结点。叶子结点没有后续结点,其余每个结点的后续节点数可以是一个也可以是多个。
- 图状结构(网状结构):链式存储
图是一种比较复杂的数据结构。在图结构中任意两个元素之间都可能有关系,也就是说这是一种多对多的关系。
- 其他结构
除了以上几种常见的逻辑结构外,数据结构中还包含其他的结构,比如集合等。有时根据实际情况抽象的模型不止是简单的某一种,也可能拥有更多的特征。
存储结构(关系)
- 特点:
- 是数据的逻辑结构在计算机存储器中的映象(或表示)
- 存储结构是通过计算机程序来实现的,因而是依赖于具体的计算机语言的。
- 基础存储结构
- 顺序存储
顺序存储(Sequential Storage):将数据结构中各元素按照其逻辑顺序存放于存储器一片连续的存储空间中。
- 链式存储
链式存储(Linked Storage):将数据结构中各元素分布到存储器的不同点,用记录下一个结点位置的方式建立它们之间的联系,由此得到的存储结构为链式存储结构。
线性表
线性表的定义是描述其逻辑结构,而通常会在线性表上进行的查找、插入、删除等操作。
线性表作为一种基本的数据结构类型,在计算机存储器中的存储一般有两种形式,一种是顺序存储,一种是链式存储。
线性表的顺序存储
- 定义
若将线性表L=(a0,a1, ……,an-1)中的各元素依次存储于计算机一片连续的存储空间,这种机制表示为线性表的顺序存储结构。
- 特点
- 逻辑上相邻的元素 ai, ai+1,其存储位置也是相邻的;
- 存储密度高,方便对数据的遍历查找。
- 对表的插入和删除等运算的效率较差。
- 程序实现
在Python中,list存放于一片单一连续的内存块,故可借助于列表类型来描述线性表的顺序存储结构,而且列表本身就提供了丰富的接口满足这种数据结构的运算。
>>>L = [1,2,3,4]
>>>L.append(10) #尾部增加元素
L
[1, 2, 3, 4, 10]
>>>L.insert(1,20) #插入元素
L
[1, 20, 2, 3, 4, 10]
>>>L.remove(3) #删除元素
L
[1, 20, 2, 4, 10]
>>>L[4] = 30 #修改
L
[1, 20, 2, 4, 30]
>>>L.index(2) #查找
2
线性表的链式存储
- 定义
将线性表L=(a0,a1,……,an-1)中各元素分布在存储器的不同存储块,称为结点,每个结点(尾节点除外)中都持有一个指向下一个节点的引用,这样所得到的存储结构为链表结构。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ouB5zSDZ-1573733293234)(./img/data2.png)]
- 特点
- 逻辑上相邻的元素 ai, ai+1,其存储位置也不一定相邻;
- 存储稀疏,不必开辟整块存储空间。
- 对表的插入和删除等运算的效率较高。
- 逻辑结构复杂,不利于遍历。
- 程序实现
代码实现: day1/linklist.py
"""
linklist.py
功能: 实现单链表的构建和操作
重点代码
"""
# 创建节点类
class Node:
"""
思路 : *自定义类视为节点类,类中的属性为数据内容
*写一个next属性,用来和下一个 节点建立关系
"""
def __init__(self,val,next = None):
"""
val: 有用数据
next: 下一个节点引用
"""
self.val = val
self.next = next
# 链式线性表操作类
class LinkList:
"""
思路 : 生成单链表,通过实例化的对象就代表一个链表
可以调用具体的操作方法完成各种功能
"""
def __init__(self):
# 链表的初始化节点,没有有用数据,但是便于标记链表的开端
self.head = Node(None)
# 初始化链表,添加一组节点
def init_list(self,list_):
p = self.head # p 作为移动变量
for i in list_:
# 遍历到一个值就创建一个节点
p.next = Node(i)
p = p.next
# 遍历链表
def show(self):
p = self.head.next # p代表第一个有值的节点
while p is not None:
print(p.val)
p = p.next # p向后移动
# 判断链表为空
def is_empty(self):
if self.head.next is None:
return True
else:
return False
# 清空链表
def clear(self):
self.head.next = None
# 尾部插入
def append(self,val):
p = self.head
# p移动到最后一个节点
while p.next is not None:
p = p.next
p.next = Node(val) # 最后添加节点
# 头部插入
def head_insert(self,val):
node = Node(val)
node.next = self.head.next
self.head.next = node
# 指定位置插入
def insert(self,index,val):
# 设置个p 移动到待插入位置的前一个
p = self.head
for i in range(index):
# 如果index超出了最大范围跳出循环
if p.next is None:
break
p = p.next
# 插入节点
node = Node(val)
node.next = p.next
p.next = node
# 删除节点
def remove(self,val):
p = self.head
# p 移动,待删除节点上一个
while p.next is not None and p.next.val != val:
p = p.next
if p.next is None:
raise ValueError("x not in linklist")
else:
p.next = p.next.next
# 获取某个节点的值 (通过索引获取)
def search(self,index):
if index < 0:
raise IndexError("index out of range")
p = self.head.next
# 循环移动p
for i in range(index):
if p is None:
raise IndexError("index out of range")
p = p.next
return p.val
if __name__ == "__main__":
# 想有一个链表
link = LinkList()
# 初始化一组数据
l = [1,2,3,4]
link.init_list(l)
link.clear()
print(link.search(0))
# 链表遍历
# link.show()
# link.insert(2,88)
# link.show()
# link.clear()
# print(link.is_empty())
# Abby = Node((1,'Abby',18,'w'))
# Emma = Node((2,'Emma',17,'w'))
# Alex = Node((3,'Alex',19,'m'))
#
# Abby.next = Emma
# Emma.next = Alex
栈和队列
栈
- 定义
栈是限制在一端进行插入操作和删除操作的线性表(俗称堆栈),允许进行操作的一端称为“栈顶”,另一固定端称为“栈底”,当栈中没有元素时称为“空栈”。
- 特点:
- 栈只能在一端进行数据操作
- 栈模型具有先进后出或者叫做后进先出的规律
- 栈的代码实现
栈的操作有入栈(压栈),出栈(弹栈),判断栈的空满等操作。
顺序存储代码实现: day2/sstack.py
"""
sstack.py 栈模型的顺序存
重点代码
思路 :
1. 顺序存储可以使用列表实现,但是列表功能丰富,不符合栈模型要求
2. 将列表功能封装,实现顺序栈的类,只提供栈的操作功能
功能: 出栈, 入栈,判断栈空,查看栈顶元素
"""
# 自定义异常
class StackError(Exception):
pass
# 顺序栈
class SStack:
def __init__(self):
# 空列表就是栈的存储空间
# 列表的最后一个元素作为栈顶元素
self.__elems = []
# 入栈
def push(self,val):
self.__elems.append(val)
# 判断栈空
def is_empty(self):
return self.__elems == []
# 出栈
def pop(self):
if self.is_empty():
raise StackError("pop from empty stack")
return self.__elems.pop()
# 查看栈顶
def top(self):
if self.is_empty():
raise StackError("pop from empty stack")
return self.__elems[-1]
if __name__ == '__main__':
st = SStack()
st.push(10)
st.push(20)
st.push(30)
while not st.is_empty():
print(st.pop())
st.pop()
链式存储代码实现: day2/lstack.py
"""
lstack.py 栈的链式模型
重点代码
思路:
1. 通过节点存储数据达到链式存储的目的
2. 封装方法,实现栈的基本操作(入栈,出栈,栈空,查看栈顶)
3. top为栈顶,在链表的头作为栈顶位置 (不许要遍历)
"""
# 自定义异常
class StackError(Exception):
pass
# 节点类
class Node:
def __init__(self,val,next = None):
self.val = val
self.next = next
# 链式栈模型
class LStack:
def __init__(self):
# top作为栈顶的标记
self.__top = None
def is_empty(self):
return self.__top is None
# 入栈
def push(self,val):
self.__top = Node(val,self.__top)
# node = Node(val)
# node.next = self.__top
# self.__top = node
# 出栈
def pop(self):
if self.__top is None:
raise StackError("pop from empty stack")
data = self.__top.val
self.__top = self.__top.next
return data
# 查看栈顶元素
def top(self):
if self.__top is None:
raise StackError("pop from empty stack")
return self.__top.val
if __name__ == '__main__':
ls = LStack()
ls.push(10)
ls.push(20)
ls.push(30)
print(ls.pop())
print(ls.pop())
队列
- 定义
队列是限制在两端进行插入操作和删除操作的线性表,允许进行存入操作的一端称为“队尾”,允许进行删除操作的一端称为“队头”。
- 特点:
- 队列只能在队头和队尾进行数据操作
- 队列模型具有先进先出或者叫做后进后出的规律
- 队列的代码实现
队列的操作有入队,出队,判断队列的空满等操作。
顺序存储代码实现: day2/squeue.py
"""
squeue.py 队列的顺序存储
思路 :
1. 基于列表完成数据存储
2. 对列表功能进行封装
3. 列表的头部作为队头,尾部作为队尾
功能: 入队(enqueue),出队(dequeue),判断队列为空
"""
# 自定义异常
class QueueError(Exception):
pass
class SQueue:
# 设置空列表作为队列存储空间
def __init__(self):
self.__elems = []
# 判断队列是否为空
def is_empty(self):
return self.__elems == []
# 入队
def enqueue(self,val):
self.__elems.append(val)
# 出对
def dequeue(self):
if not self.__elems:
raise QueueError("Queue is empty")
return self.__elems.pop(0)
if __name__ == '__main__':
sq = SQueue()
sq.enqueue(10)
sq.enqueue(20)
sq.enqueue(30)
while not sq.is_empty():
print(sq.dequeue())
链式存储代码实现: day2/lqueue.py
"""
lqueue.py 链式队列
重点代码
思路:
1. 基于链表构建队列模型
2. 链表的开端作为队头, 结尾作为队尾
3. 对头队尾分别添加标记,避免每次插入数据都遍历链表
4. 队头和队尾重叠时认为队列为空
"""
# 自定义异常
class QueueError(Exception):
pass
# 节点类
class Node:
def __init__(self,val,next = None):
self.val = val
self.next = next
# 队列操作
class LQueue:
def __init__(self):
# 定义队头,队尾
self.front = self.rear = Node(None)
def is_empty(self):
return self.front == self.rear
# 如队 rear动
def enqueue(self,val):
self.rear.next = Node(val)
self.rear = self.rear.next
# 出队 front动
def dequeue(self):
if self.front == self.rear:
raise QueueError("Queue is empty")
# front移动到的节点已经出队
self.front = self.front.next
return self.front.val
if __name__ == '__main__':
lq = LQueue()
lq.enqueue(10)
lq.enqueue(20)
lq.enqueue(30)
print(lq.dequeue())
树形结构
基础概念
- 定义
树(Tree)是n(n≥0)个节点的有限集合T,它满足两个条件:有且仅有一个特定的称为根(Root)的节点;其余的节点可以分为m(m≥0)个互不相交的有限集合T1、T2、……、Tm,其中每一个集合又是一棵树,并称为其根的子树(Subtree)。
- 基本概念
- 一个节点的子树的个数称为该节点的度数,一棵树的度数是指该树中节点的最大度数。
- 度数为零的节点称为树叶或终端节点,度数不为零的节点称为分支节点。
- 一个节点的子树之根节点称为该节点的子节点,该节点称为它们的父节点,同一节点的各个子节点之间称为兄弟节点。一棵树的根节点没有父节点,叶节点没有子节点。
- 节点的层数等于父节点的层数加一,根节点的层数定义为一。树中节点层数的最大值称为该树的高度或深度。
二叉树
定义与特征
- 定义
二叉树(Binary Tree)是n(n≥0)个节点的有限集合,它或者是空集(n=0),或者是由一个根节点以及两棵互不相交的、分别称为左子树和右子树的二叉树组成。二叉树与普通有序树不同,二叉树严格区分左孩子和右孩子,即使只有一个子节点也要区分左右。
- 二叉树的特征
-
二叉树第i(i≥1)层上的节点最多为 2 i − 1 2^{i-1} 2i−1个。
-
深度为k(k≥1)的二叉树最多有 2 k - 1 2^k-1 2k-1个节点。
-
在任意一棵二叉树中,树叶的数目比度数为2的节点的数目多一。
-
满二叉树 :深度为k(k≥1)时有 2 k - 1 2^k-1 2k-1个节点的二叉树。
二叉树的遍历
遍历 :沿某条搜索路径周游二叉树,对树中的每一个节点访问一次且仅访问一次。
先序遍历: 先访问树根,再访问左子树,最后访问右子树;
中序遍历: 先访问左子树,再访问树根,最后访问右子树;
后序遍历: 先访问左子树,再访问右子树,最后访问树根;
层次遍历: 从根节点开始,逐层从左向右进行遍历。
递归思想和实践
- 什么是递归?
所谓递归函数是指一个函数的函数体中直接调用或间接调用了该函数自身的函数。这里的直接调用是指一个函数的函数体中含有调用自身的语句,间接调用是指一个函数在函数体里有调用了其它函数,而其它函数又反过来调用了该函数的情况。
- 递归函数调用的执行过程分为两个阶段
递推阶段:从原问题出发,按递归公式递推从未知到已知,最终达到递归终止条件。
回归阶段:按递归终止条件求出结果,逆向逐步代入递归公式,回归到原问题求解。
- 优点与缺点
优点:递归可以把问题简单化,让思路更为清晰,代码更简洁
缺点:递归因系统环境影响大,当递归深度太大时,可能会得到不可预知的结果
递归示例: day3/recursion.py
"""
求一个数的阶乘 n!
"""
def fun(n):
result = 1
for i in range(1,n + 1):
result *= i
return result
def recursion(n):
if n <= 1:
return 1
return n * recursion(n - 1)
print(recursion(5))
二叉树的代码实现
二叉树顺序存储
二叉树本身是一种递归结构,可以使用Python list 进行顺序存储。但是如果二叉树的结构比较稀疏的话浪费的空间是比较多的,难以处理。
- 空结点用None表示
- 非空二叉树用包含三个元素的列表[d,l,r]表示,其中d表示根结点,l,r左子树和右子树。
['A',['B',None,None
],
['C',['D',['F',None,None],
['G',None,None],
],
['E',['H',None,None],
['I',None,None],
],
]
]
二叉树链式存储
二叉树遍历: day3/bitree.py
"""
bitree.py 二叉树的遍历实践
思路分析:
1. 使用链式结构存储二叉树的节点数据
2. 节点中存储 数据, 左孩子链接,右孩子链接 三个属性
"""
from day02.squeue import *
# 二叉树节点类
class Node:
def __init__(self,val,left=None,right=None):
self.val = val
self.left = left
self.right = right
# 二叉树遍历方法
class Bitree:
def __init__(self,root):
self.root = root
# 先序遍历
def preOrder(self,node):
if node is None:
return
print(node.val)
self.preOrder(node.left)
self.preOrder(node.right)
# 中序遍历
def inOrder(self, node):
if node is None:
return
self.inOrder(node.left)
print(node.val)
self.inOrder(node.right)
# 后序遍历
def postOrder(self, node):
if node is None:
return
self.postOrder(node.left)
self.postOrder(node.right)
print(node.val)
# 层次遍历
def levelOrder(self,node):
"""
node先入队,循环判断,队列不为空时,出队表示遍历,
同时让出队元素的左右孩子入队
"""
sq = SQueue()
sq.enqueue(node)
while not sq.is_empty():
node = sq.dequeue()
print(node.val) # 遍历元素
if node.left:
sq.enqueue(node.left)
if node.right:
sq.enqueue(node.right)
if __name__ == '__main__':
b = Node('B')
f = Node('F')
g = Node('G')
d = Node('D',f,g)
h = Node('H')
i = Node('I')
e = Node('E',h,i)
c = Node('C',d,e)
a = Node('A',b,c) # 整个树根
bt = Bitree(a) # 把a作为根节点进行遍历
bt.preOrder(bt.root)
print("========================")
bt.inOrder(bt.root)
print("========================")
bt.postOrder(bt.root)
print("========================")
bt.levelOrder(bt.root)
算法基础
基础概念特征
- 定义
算法是一个有穷规则(或语句、指令)的有序集合。它确定了解决某一问题的一个运算序列。对于问题的初始输入,通过算法有限步的运行,产生一个或多个输出。
数据的逻辑结构与存储结构密切相关:
- 算法设计: 取决于选定的逻辑结构
- 算法实现: 依赖于采用的存储结构
- 算法的特性
- 有穷性 —— 算法执行的步骤(或规则)是有限的;
- 确定性 —— 每个计算步骤无二义性;
- 可行性 —— 每个计算步骤能够在有限的时间内完成;
- 输入 ,输出 —— 存在数据的输入和出输出
- 评价算法好坏的方法
- 正确性:运行正确是一个算法的前提。
- 可读性:容易理解、容易编程和调试、容易维护。
- 健壮性:考虑情况全面,不容以出现运行错误。
- 时间效率高:算法消耗的时间少。
- 储存量低:占用较少的存储空间。
时间复杂度计算
算法效率——用依据该算法编制的程序在计算机上执行所消耗的时间来度量。“O”表示一个数量级的概念。根据算法中语句执行的最大次数(频度)来 估算一个算法执行时间的数量级。
计算方法:
写出程序中所有运算语句执行的次数,进行加和
如果得到的结果是常量则时间复杂度为1
如果得到的结果中存在变量n则取n的最高次幂作为时间复杂度
排序和查找
排序
排序(Sort)是将无序的记录序列(或称文件)调整成有序的序列。排序方法有很多种,下面举例说明:
- 冒泡排序
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
- 快速排序
步骤:
从数列中挑出一个元素,称为 “基准”(pivot),
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
常见排序代码实现: day3/sort.py
"""
sort.py 排序方法训练
"""
def bubble(l):
n = len(l)
# 外层循环来确定比较多少轮
for i in range(n - 1):
# 内存循环确定每轮两两比较多少次
for j in range(n - 1 - i):
if l[j] > l[j + 1]:
l[j],l[j+1]=l[j+1],l[j]
# 一轮交换
def sub_sort(l,low,high):
# 选定基准
x = l[low]
while low < high:
# 后面的数向前甩
while l[high] > x and high > low:
high -= 1
l[low] = l[high] # 将比基准小的数放到前面
# 前面的数往后甩
while l[low] <= x and low < high:
low += 1
l[high] = l[low] # 将比基准大的数放到后面
l[low] = x # 将基准数插入
return low
# 快速排序
def quick(l,low,high):
if low < high:
key = sub_sort(l,low,high)
quick(l,low,key - 1)
quick(l,key+1,high)
l = [95, 3, 44, 5, 56, 4537, 657, 345]
# l = [4,9,3,1,2,5,8,4]
# bubble(l)
quick(l,0,len(l)-1)
print(l) # 有序
查找
查找(或检索)是在给定信息集上寻找特定信息元素的过程。
二分法查找
当数据量很大适宜采用该方法。采用二分法查找时,数据需是排好序的。
二分查找代码实现: day3/search.py
def search(l,val):
low,high = 0,len(l) - 1 # 查找范围的开始和结束索引位
# 循环查找,每次去除一半
while low <= high:
mid = (low + high) // 2 # 中间数索引
if l[mid] < val:
low = mid + 1
elif l[mid] > val:
high = mid - 1
else:
return mid
l = [1,2,3,4,5,6,7,8,9,10]
print("Key index:",search(l,666))