数据结构是指相互之间具有(存在)一定联系(关系)的数据元素的集合
一、数据结构的存储方式
顺序存储结构: 用数据元素在存储器中的相对位置来表示数据元素之间的逻辑结 构(关系)
链式存储结构: 在每一个数据元素中增加一个存放另一个元素地址的指针 (pointer ),用该指针来表示数据元素之间的逻辑结构(关系)
二、逻辑结构和物理结构
逻辑结构: 元素之间的相互联系(关系)
四种基本类型
集合: 结构中的数据元素除了“同属于一个集合”外,没有其它关系
线性结构: 结构中的数据元素之间存在一对一的关系
树型结构: 结构中的数据元素之间存在一对多的关系
图状结构或网状结构: 结构中的数据元素之间存在多对多的关系
三、数据结构的运算
数据结构的主要运算包括: 建立(Create); 销毁(Destroy); 插入(Insert); 删除(Delete); 修改(Modify); 查找(Search); 访问(Access);排序(Sort);
四、线性表
1、线性结构
是最常用、最简单的一种数据结构
基本特点是线性表中的数据元素是有序且有限的
在这种结构中:
① 存在一个唯一的被称为“第一个”的数据元素;
② 存在一个唯一的被称为“最后一个”的数据元素;
③ 除第一个元素外,每个元素均有唯一一个直接前驱;
④ 除最后一个元素外,每个元素均有唯一一个直接后继
2、线性表的顺序存储
使用数组来描述顺序表
3、线性表的链式存储
用一组任意的存储单元存储线性表中的数据元素,用这种方法存储的线性表简称线性链表
存储链表中结点的一组任意的存储单元以是连续的或不连续的,甚至是零散分布 在内存中的任意位置
为了正确表示结点间的逻辑关系,在存储每个结点值的同时,还必须存储指示其 直接后继结点的地址(或位置),称为指针(pointer)或链(link),这两部分组成了链表中的结点结构
链表是通过每个结点的指针域将线性表的 n 个结点按其逻辑次序链接在一起的。 每一个结点只包含一个指针域的链表,称为单链表
为操作方便,总是在链表的第一个结点之前附设一个头指针 head 指向第一个结点
4、双向链表
双向链表(Double Linked List) 指的是构成链表的每个结点中设立两个指针域,一 个指向其直接前趋的指针域 prior,一个指向其直接后继的指针域 next,这样形 成的链表中有两个方向不同的链,故称为双向链表
和单链表类似,双向链表一般增加头指针也能使双链表的某些运算变得方便
将头结点和尾结点链接起来也能构成循环链表,并称之为双向循环链表
双向链表是为了克服单链表的单向性的缺陷而引入的
5、双向链表的结点及其类型定义
五、队列和栈
栈和队列是两种应用非常广泛的数据结构,它们都来自线性表数据结构,都是“操作受限”的线性表
栈在计算机的实现有多种方式:
硬堆栈: 利用 CPU 中的某些寄存器组或类似的硬件或使用内存的特殊区域来实现,这类堆栈容量有限,但速度很快
软堆栈: 这类堆栈主要在内存中实现。实现方式又有动态方式和静态方式两种
1、栈的基本概念
栈(Stack): 是限制在表的一端进行插入和删除操作的线性表,又称为后进先出LIFO (Last In First Out)或先进后出 FILO (First In Last Out)线性表
栈顶(Top): 允许进行插入、删除操作的一端,又称为表尾。 用栈顶指针(top)来指
示栈顶元素
栈底(Bottom): 是固定端,又称为表头
空栈: 当表中没有元素时称为空栈
2、栈的顺序存储表示
栈的顺序存储结构简称为顺序栈,和线性表相类似,用一维数组来存储栈,根据数组是否根据需要增大,又分为静态顺序栈和动态顺序栈
静态顺序栈实现简单,但不能根据需要增大栈的存储空间
动态顺序栈根据需要增大栈的存储空间,但实现稍为复杂
3、栈的链式存储表示
栈的链式存储结构称为链栈,是运算受限的单链表,其插入和删除操作只能在表 头位置进行,因此,链栈没有必要像单链表那样附加头结点,栈顶指针 top 就是 链表的头指针
4、栈的应用
由于栈具有的“后进先出”的固有特性,因此,栈成为程序设计中常用的工具和数据结构,中缀表达式变后缀表达式就是栈应用的例子
后缀表达式计算
中缀表达式变后缀表达式
栈与递归调用的实现
5、队列
队列(Queue): 也是运算受限的线性表。是一种先进先出(First In First Out ,简称 FIFO)的线性表,只允许在表的一端进行插入,而在另一端进行删除
队首(front) : 允许进行删除的一端称为队首
队尾(rear) : 允许进行插入的一端称为队尾
队列中没有元素时称为空队列。在空队列中依次加入元素 a1, a2, …, an 之后,a1 是队首元素,an 是队尾元素。显然退出队列的次序也只能是 a1, a2, …, an ,即队列的修改是依先进先出的原则进行的
6、循环队列
为充分利用向量空间,克服“假溢出”现象,将队列分配的向量空间看成为一个首 尾相接的圆环,并称这种队列为循环队列(Circular Queue)
7、队列的链式表示和实现
队列的链式存储结构简称为链队列,它是限制仅在表头进行删除操作和表尾进行插入操作的单链表
需要两类不同的结点: 数据元素结点,队列的队首指针和队尾指针的结点
六、树
树型结构是一类非常重要的非线性结构,直观地,树型结构是以分支关系定义的层次结构
树在计算机领域中也有着广泛的应用,例如在编译程序中,用树来表示源程序的语法结构;在数据库系统中,用树来组织信息;在分析算法的行为时,用树来描述其执行过程等等
1、树的定义
树(Tree)是 n(n≧0)个结点的有限集合 T,若 n=0 时称为空树,否则: 有且只有一个特殊的称为树的根(Root)结点
若 n>1 时,其余的结点被分为 m(m>0)个互不相交的子集 T1, T2, T3…Tm,其中每 个子集本身又是一棵树,称其为根的子树(Subtree)
这是树的递归定义,即用树来定义树,而只有一个结点的树必定仅由根组成
2、树的基本术语
结点(node): 一个数据元素及其若干指向其子树的分支
结点的度(degree) 、树的度: 结点所拥有的子树的棵数称为结点的度,树中结 点度的最大值称为树的度
叶子(left)结点、非叶子结点
树中度为 0 的结点称为叶子结点(或终端结点),相对应地,度不为 0 的结点称为 非叶子结点(或非终端结点或分支结点),除根结点外,分支结点又称为内部结点
孩子结点、双亲结点、兄弟结点
一个结点的子树的根称为该结点的孩子结点(child)或子结点,相应地,该结点是其孩子结点的双亲结点(parent)或父结点
森林(forest): 是 m(m≧0)棵互不相交的树的集合,显然,若将一棵树的根结点删除,剩余的子树就构成了森林
3、树的表示形式
倒悬树是最常用的表示形式
嵌套集合: 是一些集合的集体,对于任何两个集合,或者不相交,或者一个集合包含另一个集合
广义表形式
4、二叉树
二叉树(Binary tree) 是 n(n≥0)个结点的有限集合,若 n=0 时称为空树,否则: 有且只有一个特殊的称为树的根(Root)结点
若 n>1 时,其余的结点被分成为二个互不相交的子集 T1,T2,分别称之为左、右 子树,并且左、右子树又都是二叉树
由此可知,二叉树的定义是递归的
二叉树在树结构中起着非常重要的作用,因为二叉树结构简单,存储效率高,树 的操作算法相对简单,且任何树都很容易转化成二叉树结构,上节中引入的有关 树的术语也都适用于二叉树
5、二叉树的基本形态
6、二叉树的本质
性质 1:在非空二叉树中,第 i 层上至多有 2i 个结点(i≧1)
性质 2:深度为 k 的二叉树至多有 2k-1 个结点(k≧1)
性质 3:对任何一棵二叉树,若其叶子结点数为 n0,度为 2 的结点数为 n2,则 n0=n2+1
7、满二叉树
一棵深度为 k 且有 2k-1 个结点的二叉树称为满二叉树(Full Binary Tree)
满二叉树的特点:
(1)每一层上的结点数总是最大结点数
(2)所有的支结点都有左、右子树
(3)可对满二叉树的结点进行连续编号,若规定从根结点开始,按“自上而下、自左 至右”的原则进行
8、完全二叉树
完全二叉树(Complete Binary Tree):如果深度为 k,由 n 个结点的二叉树,当且仅 当其每一个结点都与深度为 k 的满二叉树中编号从 1 到 n 的结点一一对应,该二叉树称为完全二叉树
或深度为 k 的满二叉树中编号从 1 到 n 的前 n 个结点构成了一棵深度为 k 的完全二叉树
其中 2k-1 ≦ n≦2k-1
完全二叉树是满二叉树的一部分,而满二叉树是完全二叉树的特例
完全二叉树的特点是,若完全二叉树的深度为 k ,则所有的叶子结点都出现在 第 k 层或 k-1 层,对于任一结点,如果其右子树的最大层次为 l,则其左子树的 最大层次为 l 或 l+1
9、二叉树的存储结构
顺序存储结构
用一组地址连续的存储单元依次“自上而下、自左至右”存储完全二叉树的数据元素
对于完全二叉树上编号为 i 的结点元素存储在一维数组的下标值为 i-1 的分量中 对于一般的二叉树,将其每个结点与完全二叉树上的结点相对照,存储在一维数 组中
10、链式存储结构
设计不同的结点结构可构成不同的链式存储结构
二叉链表结点,有三个域:一个数据域,两个分别指向左右子结点的指针域
三叉链表结点,除二叉链表的三个域外,再增加一个指针域,用来指向结点的父结点
11、二叉树的链式存储形式
有一棵一般的二叉树,以二叉链表和三叉链表方式存储的结构图
12、遍历二叉树及其应用
遍历二叉树(Traversing Binary Tree)是指按指定的规律对二叉树中的每个结点访问 一次且仅访问一次
所谓访问是指对结点做某种处理,如:输出信息、修改结点的值等
二叉树是一种非线性结构,每个结点都可能有左、右两棵子树,因此,需要寻找 一种规律,使二叉树上的结点能排列在一个线性队列上,从而便于遍历
二叉树的基本组成:根结点、左子树、右子树。若能依次遍历这三部分,就是遍 历了二叉树
若以 L、D、R 分别表示遍历左子树、遍历根结点和遍历右子树,则有六种遍历方案:DLR、LDR、LRD、DRL、RDL、RLD,若规定先左后右,则只有前三种情况三种情况,分别是:
DLR——先(根)序遍历
LDR——中(根)序遍历
LRD——后(根)序遍历
13、构建二叉搜索树
又名二叉排序树
用递归的方法构建树
实现对树的增删改查功能
七、查找算法
1、顺序查找
从表中的第一个元素开始,将给定的值与表中逐个元素的关键字进行比较,直到两者相符,查到所要找的元素为止,否则就是表中没有要找的元素,查找不成功,平均要与表中一半以上元素进行比较,最坏情况下需要比较 n 次
如果线性表为无序表,则不管是顺序存储结构还是链式存储结构,都只能用顺序查找
即使是有序线性表,如果采用链式存储结构,也只能用顺序查找
2、二分法查找
二分查找又称折半查找,它是一种效率较高的查找方法
算法思想: 首先,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位
置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表
重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功
优缺点: 折半查找法的优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除困难,因此,折半查找方法适用于不经常变
动而查找频繁的有序列表
代码实现:
def binary_chop(alist, data):
"""
递归解决二分查找
:param alist:
:return:
"""
n = len(alist)
if n < 1:
return False
mid = n // 2 #
if alist[mid] > data:
return binary_chop(alist[0:mid], data)
elif alist[mid] < data:
return binary_chop(alist[mid + 1:], data)
else:
return True
八、排序算法
排序是将一批(组)任意次序的记录重新排列成按关键字有序的记录序列的过程,其定义为:
给定一组记录序列:{R1 , R2 ,…, Rn},其相应的关键字序列是{K1 , K2 ,…, Kn},确定 1, 2, … n 的一个排列 p1 , p2 ,…, pn,使其相应的关键字满足如下非递减(或非递增)关系: Kp1≤Kp2 ≤…≤Kpn 的序列{Kp1 ,Kp2 , …,Kpn} ,这种操作称为
排序
关键字 Ki 可以是记录 Ri 的主关键字,也可以是次关键字或若干数据项的组合
Ki 是主关键字:排序后得到的结果是唯一的
Ki 是次关键字:排序后得到的结果是不唯一的
1、冒泡排序
冒泡排序(英语:Bubble Sort)是一种简单的排序算法。它重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成,这个算
法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端
比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这步做完后,最后的元素会是最大的数
针对所有的元素重复以上的步骤,除了最后一个
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较
交换过程图示(第一次):
那么我们需要进行 n-1 次冒泡过程,每次对应的比较次数如下图所示:
代码实现:
# 冒泡排序
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]
li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
bubble_sort(li)
print(li)
2、插入排序
插入排序(英语:Insertion Sort)是一种简单直观的排序算法。它的工作 原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描, 找到相应位置并插入。插入排序在实现上,在从后向前扫描过程中,需要反复 把已排序元素逐步向后挪位,为最新元素提供插入空间
插入排序分析:
代码实现:
# 选择排序
def insert_sort(alist):
# 从第二个位置,即下标为 1 的元素开始向前插入
for i in range(1, len(alist)):
# 从第 i 个元素开始向前比较,如果小于前一个元素,交换位置
for j in range(i, 0, -1):
if alist[j] < alist[j - 1]:
alist[j], alist[j - 1] = alist[j - 1], alist[j]
alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]
insert_sort(alist)
print(alist)
3、选择排序
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未 排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序 元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元 素均排序完毕
选择排序的主要优点与数据移动有关,如果某个元素位于正确的最终位置上,则它不会 被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对 n 个元素的表进行排序总共进行至多 n-1 次交换。在所有的完全依靠交换去移动元 素的排序方法中,选择排序属于非常好的一种
选择排序分析
排序过程:
代码实现:
# 选择排序
def selection_sort(alist):
n = len(alist)
# 需要进行 n-1 次选择操作
for i in range(n - 1):
# 记录最小位置
min_index = i
# 从 i+1 位置到末尾选择出最小数据
for j in range(i + 1, n):
if alist[j] < alist[min_index]:
min_index = j
# 如果选择出的数据不在正确位置,进行交换
if min_index != i:
alist[i], alist[min_index] = alist[min_index], alist[i]
alist = [54, 226, 93, 17, 77, 31, 44, 55, 20]
selection_sort(alist)
print(alist)
4、快速排序
快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),通 过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部 分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过 程可以递归进行,以此达到整个数据变成有序序列
步骤为:
(1)从数列中挑出一个元素,称为"基准"(pivot),
(2)重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆 在基准的后面(相同的数可以到任一边),在这个分区结束之后,该基准就处于数列 的中间位置,这个称为分区(partition)操作
(3)递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了,虽然一 直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一 个元素摆到它最后的位置去
快速排序的分析
代码实现:
# # 快速排序
@print_execute_time
def quick_sort(alist, start, end):
"""快速排序"""
# 递归的退出条件
if start >= end:
return
# 设定起始元素为要寻找位置的基准元素
mid = alist[start]
# low 为序列左边的由左向右移动的游标
low = start
# high 为序列右边的由右向左移动的游标
high = end
while low < high:
# 如果 low 与 high 未重合,high 指向的元素不比基准元素小,则 high 向左移动
while low < high and alist[high] >= mid:
high -= 1
# 将 high 指向的元素放到 low 的位置上
alist[low] = alist[high]
# 如果 low 与 high 未重合,low 指向的元素比基准元素小,则 low 向右移动
while low < high and alist[low] < mid:
low += 1
# 将 low 指向的元素放到 high 的位置上
alist[high] = alist[low]
# 退出循环后,low 与 high 重合,此时所指位置为基准元素的正确位置
# 将基准元素放到该位置
alist[low] = mid
# 对基准元素左边的子序列进行快速排序
quick_sort(alist, start, low - 1)
# 对基准元素右边的子序列进行快速排序
quick_sort(alist, low + 1, end)
alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]
quick_sort(alist, 0, len(alist) - 1)
print(alist)