一、数据结构与算法简介
数据结构:是储存和组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集
合。
算法:用来实现业务目标的方法和思路。
二者的关系:数据结构 + 算法 = 程序,算法是为了解决实际问题而设计的,数据结构是算法
需要处理问题的载体。
二、算法特性
1.算法的独立性
算法是独立存在的一种解决问题的方法和思想,不依附于编程语言存在。
2.算法的五大特性
①有输入:算法具有0个或多个输入;
②有输出:算法至少有1个或多个输出;
③有穷性:算法在有限的步骤之后会自动结束而不会无限循环,并且每一个步骤可以在可接
受的时间内完成;
④确定性:算法中的每一步都有确定的含义,不会出现二义性;
⑤可行性:算法的每一步都是可行的,也就是说每一步都能够执行有限的次数完成。
3.算法的稳定性和不稳定性
具有相同关键字的纪录经过排序后,相对位置保持不变,这样的算法是稳定性算法。
不稳定的排序算法: 选择排序、快速排序、希尔排序、堆排序;
稳定的排序算法: 冒泡排序、插入排序、归并排序和基数排序 。
①冒泡排序
比较相邻两个元素大小,前面的元素如果大于后面的元素,则交换位置,否则前面元素不
动,继续比较后面元素与它的下一个相邻元素,直到没有元素需要换位为止。
②选择排序
从第一位元素开始,用它比较后面每一个元素,根据需要排序。(例如需要从小到大排序,
给定一个标假定现在第一位元素是最小值,与其后面的数据比较,每遇到一个比它小的元素,最小
值的标就转移给对应元素,到比较完一轮,将最小值标指向的元素与第一位元素换位,然后开始第
二位元素,以此类推,直至结束。)
三、时间和空间复杂度
1.算法时间效率衡量
代码执行总时间(T) = 操作步骤数量 * 操作步骤执行时间
2.时间复杂度
时间复杂度表示一个算法随着问题规模不断变化的最主要趋势,通常用来衡量一个算法的优
劣,通俗点来说时间复杂度可以衡量一个“算法的量级”。
3.大O记法(表示时间、空间复杂度)
大O记法即为算法的复杂度随数据量变化的关系曲线 , 通常由最高次项决定,忽略了次要项和常数项。是对时间或者空间的问题规模渐进的一种表示方式。
计算方式:①基本操作:时间复杂度O(1)
②顺序结构:时间复杂度按加法计算
③循环结构:时间复杂度按乘法计算
④分支结构:时间复杂度取最大值
注意:在没有特殊说明时,所有时间复杂度都按照最坏时间复杂度。
常见大O记法(消耗时间从小到大):
O(1) < O(logn) < O(n) < O(n²) < O(n³)
常数阶 < 对数阶 < 线性阶 < 平方阶 < 立方阶
时间复杂度越低,效率越高。
最优复杂度:算法完成工作最少需要多少基本操作,最乐观最理想的情况,没有参考价值;
最坏复杂度:算法完成工作最多需要多少基本操作,是算法的一种保证,表明算法在此种程度的基
本操作中一定能完成工作。
4.空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的度量
常见大O记法(消耗空间从小到大):
O(1) < O(logn) < O(n) < O(n²) < O(n³)
四、数据结构
1.内存的存储结构
内存是以字节为基本存储单位的, 每个基本存储空间都有自己的地址
整型(int):占4个字节 字符(char):占1个字节
但在python中整数占28字节,字符占76字节,原因是python的数据实际上是一个对象,它包
含其他元数据,会占用较多内存。
2.线性结构
线性结构就是数据结构中各个结点具有线性关系。
特点:线性结构是非空集,线性结构所有结点都最多只有一个直接前驱结点和一个直接后继
结点。(栈,队列等)
①顺序表
将元素顺序地存放在一块连续的存储区里,元素间的顺序关系由它们的存储顺序自然表示。
Ⅰ.储存结构和方式
信息区:即元素存储区的容量和当前表中已有的元素个数
数据区:存储数据的区域
储存方式:一体式储存 和 分离式储存
无论一体式结构还是分离式结构,顺序表在获取数据的时候直接通过下标偏移就可以找到数
据所在空间的地址 , 而无需遍历后才可以获取地址 . 所以顺序表在获取地址操作时的时间复杂度是
O(1) 。
Ⅱ.扩充策略
· 每次扩充固定数目的位置(线性增长)
特点:节省空间,但是扩充操作频繁,操作次数多。
· 每次扩充容量加倍
特点:减少了扩充操作的执行次数,但可能会浪费空间资源 , 以空间换时间,推荐的方式。
Ⅲ.增删元素
· 增加元素
a . 尾端加入元素,时间复杂度为O(1) b . 非保序的加入元素(不常见),时间复杂度为O(1) c . 保序的元素加入,时间复杂度为O(n)
· 删除元素
a . 删除表尾元素,时间复杂度为O(1) b . 非保序的元素删除(不常见),时间复杂度为O(1) c . 保序的元素删除,时间复杂度为O(n)
Ⅳ.模拟实践(栈和队列)
***直接使用内置模块collections***
对于栈来说,特点是数据先进后出。
步骤:导包:from collections import deque;
创建对象栈,对象名.append() 添加数据;
对象名.pop() 弹出栈顶元素。
对于队列来说,特点是数据先进先出。
步骤:导包:from collections import deque;
创建对象队列,对象名.append()添加数据;
对象名.popleft()弹出队列首个元素。【使用内置模块queue也也可以实现】
***自定义类实现***
先定义类,在类内定义压栈和出栈函数,根据类创建对象,然后使用对象调用函数实现。
②链表
将元素存放在通过链接构造起来的一系列存储块中 , 存储区是非连续的。
Ⅰ.储存结构
元素域:元素域存储当前节点的数据元素
链接域:链接域存储链表中的下一个节点的位置
链表结构:单向链表 和 双向链表
注意:
第一、变量head指向链表的头节点(首节点)的位置,从head出发能找到表中的任意节
点。如果链是空的,就用head指向None。
第二、如果节点没有放到链中或者链中尾节点,链接默认是None;如果放到链中,则记录的
是下一个节点的地址。
Ⅱ.模拟实践
第一、判断链表是否为空
第二、获取链表长度
第三、定义遍历链表的方法
第四、定义头部添加节点的方法(只有此方法无需遍历操作,时间复杂度是O(1))
第五、定义尾部添加节点的方法
第六、定义插入节点到指定索引位置的方法
第七、定义删除节点的方法
第八、定义查询指定节点的方法
③顺序表和链表的比较
链表失去了顺序表随机读取的优点,链表由于增加了结点的连接域,空间开销比较大,但对
存储空间的使用要相对灵活。
3.非线性结构
非线性结构就是数据结构中各个结点之间具有多个对应关系。
特点:非线性结构是非空集,非线性结构的一个结点可能有多个直接前驱结点和多个直接后
继结点。(树等)
①树
它是用来模拟具有树状结构性质的数据集合. 它是由n(n>=1)个有限节点组成一个具有层次
关系的集合。
1.多叉树特点:①每个节点有零个或多个子节点 ②没有父节点的节点称为根节点 ③每一个非根节
点有且只有一个父节点 ④除了根节点外,每个子节点可以分为多个不相交的子树。
2.二叉树特点:①每个结点有0、1、2 个子节点 ②没有父节点的结点成为根节点 ③每一个非根节
点有且只有一个父节点 ④除了根节点外,每个子节点可以分为多个互不相交的子树
②树的相关术语
节点的度:一个节点含有的子节点的个数称为该节点的度
树的度:一棵树中,最大的节点的度称为树的度
叶节点或终端节点:度为零的节点
父亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点
兄弟节点:具有相同父节点的节点互称为兄弟节点
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推
树的高度或深度:树中节点的最大层次
堂兄弟节点:父节点在同一层的节点互为堂兄弟
节点的祖先:从根到该节点所经分支上的所有节点
子孙:以某节点为根的子树中任一节点都称为该节点的子孙
森林:由m(m>=0)棵互不相交的树的集合称为森林
③树的种类
无序树:树中任意节点的子节点之间没有顺序关系,这种树称为无序树,也称为自由树。
有序树:树中任意节点的子节点之间有顺序关系,这种树称为有序树。
有序树分为:Ⅰ.霍夫曼树(用于信息编码):带权路径最短的二叉树称为哈夫曼树或最优二叉树
Ⅱ.B树:一种对读写操作进行优化的自平衡的二叉查找树,能够保持数据有序,拥有
多于两个的子树。
二叉树:每个节点最多含有两个子树的树称为二叉树。
完全二叉树:对于一颗二叉树,假设其深度为d(d>1)。除了第d层外,其它各层的节点数目均已达
最大值,且第d层所有节点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树,其中满二
叉树的定义是所有叶节点都在最底层的完全二叉树。(满二叉树的作用:层次存储的时候,从左到
右控制节点产生)
平衡二叉树(AVL树):当且仅当任何节点的两棵子树的高度差不大于1的二叉树。(能够防止树
退化成链表)
排序二叉树(二叉查找树,也称二叉搜索树、有序二叉树):要求:1.若左子树不空,则左子树上
所有节点的值均小于它的根节点的值 2.若右子树不空,则右子树上所有节点的值均大于它的根节
点的值 3.左、右子树也分别为二叉排序树。排序二叉树包含空树。(作用:对数据排序,检索起
来速度快)
④二叉树的储存
存储是要存储树节点的数据和节点的关系。
顺序存储:将二叉树存储在固定的数组中,虽然在遍历速度上有一定的优势,但因所占空间比较
大,是非主流二叉树 存储方式.二叉树通常以链式存储。(优点:直接存数据,没有其他冗余信
息,节省空间)
链式存储:由于对节点的个数无法掌握,常见树的存储表示都转换成二叉树进行处理,子节点个数
最多为2。(优点:很容易找到子节点、父节点关系)
⑤树的应用场景
Ⅰ. xml,html等,那么编写这些东西的解析器的时候,不可避免用到树
Ⅱ. 路由协议就是使用了树的算法
Ⅲ. mysql数据库索引
Ⅳ. 文件系统的目录结构
Ⅴ. 所以很多经典的AI算法其实都是树搜索,此外机器学习中的decision tree也是树结构
⑥二叉树的概念和性质
概念:二叉树是每个节点最多有两个子树的树结构 通常子树被称作“左子树”(left subtree)和“右
子树”(right subtree)
性质:性质1: 在二叉树的第i层上至多有个结点(i>0)
性质2: 深度为k的二叉树至多有个结点(k>0)
性质3: 对于任意一棵二叉树,如果其叶结点数为N_0,而度数为2的结点总数为N_2 ,则
N_0 = N_2 + 1
性质4: 最多有n个结点的完全二叉树的深度必为
性质5: 对完全二叉树,若从上至下、从左至右编号,则编号为i 的结点,其左孩子编号必为
2i,其右孩子 编号必为2i+1 , 其父节点的编号必为i//2(i=1 时为根,除外)
⑦广度优先遍历和深度优先遍历
添加节点的方法(后续广度和深度遍历都基于以下代码)
# 1.先定义类
# 定义节点类
class Node:
def __init__(self, item):
self.item = item
self.lchild = None
self.rchild = None
# 定义树类
class TwoXTree:
def __init__(self):
self.root = None
# 定义添加节点方法
# 注意: 只要添加成功就结束当前函数
def add(self, item):
# 1.先判断树的根节点是否为空
if self.root is None:
self.root = Node(item) # A
print(f'添加节点位置1,添加了{item}')
return
# 2.如果根节点存在了,后续需要再添加元素,需要判断左右
# 提前创建临时列表作为队列使用,以后从此队列中取出节点判断存放位置
queue = [self.root]
# 3.遍历队列,从队列中取出第一个元素,直到队列为空
# 注意: 边取边判断,同时再把最新节点放到队列中
while True:
# 取出队列中第一个元素(默认第一次是根节点,后面就是根节点的孩子们了...)
node = queue.pop(0)
# 如果当前节点左孩子为空,就把item所在节点添加到左孩子中,结束当前函数
if node.lchild is None:
node.lchild = Node(item) # B D F H J
print(f'添加节点位置2,添加了{item}')
return
# 如果当前节点右孩子为空,就把item所在节点添加到右孩子中,结束当前函数
elif node.rchild is None:
node.rchild = Node(item) # C E G I
print(f'添加节点位置3,添加了{item}')
return
else:
queue.append(node.lchild)
queue.append(node.rchild)
Ⅰ.广度优先遍历:
广度优先可以找到最短路径:相当于层次遍历,先把第1层给遍历完,看有没有终点;再把第
2层遍历完,看有没有终点。
def breadth_travel(self):
# 1.先判断根节点是否为空,如果为空就没有遍历的必要了
if self.root is None:
print('对不起,二叉树为空,不能遍历')
return
# 创建队列,把根节点放入队列
queue = [self.root] #
# 遍历队列,直到队列为空
while len(queue) > 0:
# 取出队列第一个元素
node = queue.pop(0)
# 打印元素
print(node.item, end=' ') # A B C D E F G H I J
# 判断左孩子是否存在,存在就放入队列
if node.lchild is not None:
queue.append(node.lchild)
if node.rchild is not None:
queue.append(node.rchild)
# 换行
print()
Ⅱ.深度优先遍历:
深度优先往往可以很快找到搜索路径:比如:先找一个结点看看是不是终点,若不是继续往
深层去找,直到找到终点。先序,中序,后序属于深度优先算法。
先序(根左右):
def pre_travel(self, root):
# 注意:首次root是根节点,后续就是它的孩子们...
if root is not None:
# 根
print(root.item, end=' ')
# 左
self.pre_travel(root.lchild)
# 右
self.pre_travel(root.rchild)
中序(左根右):
def in_travel(self, root):
# 注意:首次root是根节点,后续就是它的孩子们...
if root is not None:
# 左
self.in_travel(root.lchild)
# 根
print(root.item, end=' ') # H D I B J E
# 右
self.in_travel(root.rchild)
后序(左右根):
def back_travel(self, root):
# 注意:首次root是根节点,后续就是它的孩子们...
if root is not None:
# 左
self.back_travel(root.lchild)
# 右
self.back_travel(root.rchild)
# 根
print(root.item, end=' ')
if __name__ == '__main__':
# 2.再根据类创建对象
tree = TwoXTree()
# 3.使用对象
# 测试添加功能
tree.add('A')
tree.add('B')
tree.add('C')
tree.add('D')
tree.add('E')
tree.add('F')
tree.add('G')
tree.add('H')
tree.add('I')
tree.add('J')
print('---------------------------------')
# 测试广度优先遍历功能
tree.breadth_travel() # 结果 : A B C D E F G H I J
print('---------------------------------')
# 测试深度优先遍历功能
# 中序(左根右): HDIBJEAFCG
tree.in_travel(tree.root)
print()
# 后序(左右根): HIDJEBFGCA
tree.back_travel(tree.root)
print()
# 先序(根左右): ABDHIEJCFG
tree.pre_travel(tree.root)
print()