数据结构及算法

数据结构与算法
一、算法概念
算法是独⽴存在的⼀种解决问题的⽅法和思想。
算法的五⼤特性:输⼊、输出、有穷性、确定性、可⾏性
1、时间复杂度
1.1 时间复杂度与“⼤O记法”
“⼤O记法”用来表示算法的时间效率。
“⼤O记法”:对于单调的整数函数f,如果存在⼀个整数函数g和实常数c>0,使得对于充分⼤的n总有f(n)<=c*g(n),就说函数g是f的⼀个渐近函数(忽略常数),记为f(n)=O(g(n))。也就是说,在趋向⽆穷的极限意义下,函数f的增⻓速度受到函数g的约束,亦即函数f与函数g的特征相似。
时间复杂度:假设存在函数g,使得算法A处理规模为n的问题示例所⽤时间为T(n)=O(g(n)),则称O(g(n))为算法A的渐近时间复杂度,简称时间复杂度,记为T(n)
1.2 时间复杂度的⼏条基本计算规则:

  1. 基本操作,即只有常数项,认为其时间复杂度为O(1)
  2. 顺序结构,时间复杂度按加法进⾏计算
  3. 循环结构,时间复杂度按乘法进⾏计算
  4. 分⽀结构,时间复杂度取最⼤值
  5. 判断⼀个算法的效率时,往往只需要关注操作数量的最⾼次项,其它次要项和常数项可以忽略
  6. 在没有特殊说明时,我们所分析的算法的时间复杂度都是指最坏时间复杂度

1.3 空间复杂度
空间复杂度(SpaceComplexity)是对⼀个算法在运⾏过程中临时占⼤⼩的量度。算法的时间复杂度和空间复杂度合称为算法的复杂
1.4 常见的时间复杂度:
在这里插入图片描述
1.5 常⻅时间复杂度之间的关系
在这里插入图片描述
2、Python内置类型性能分析
2.1 timeit模块
timeit模块可以⽤来测试⼀⼩段Python代码的执⾏速度。
class timeit.Timer(stmt=‘pass’, setup=‘pass’, timer=)
Timer是测量⼩段代码执⾏速度的类。
stmt参数是要测试的代码语句(statment);
setup参数是运⾏代码时需要的设置;
timer参数是⼀个定时器函数,与平台有关。
timeit.Timer.timeit(number=1000000)
Timer类中测试语句执⾏速度的对象⽅法。number参数是测试代码时的测试
次数,默认为1000000次。⽅法返回执⾏代码的平均耗时,⼀个float类型的
秒数。
2.2 list内置操作的时间复杂度
在这里插入图片描述
2.3 dict内置操作的时间复
在这里插入图片描述
3、数据结构
3.1 概念
数据是⼀个抽象的概念,将其进⾏分类后得到程序设计语⾔中的基本类型。
如:int,float,char等。数据元素之间不是独⽴的,存在特定的关系,这些
关系便是结构。数据结构指数据对象中数据元素之间的关系。
Python给我们提供了很多现成的数据结构类型,这些系统⾃⼰定义好的,不
需要我们⾃⼰去定义的数据结构叫做Python的内置数据结构,⽐如列表、元
组、字典。⽽有些数据组织⽅式,Python系统⾥⾯没有直接定义,需要我们
⾃⼰去定义实现这些数据的组织⽅式,这些数据组织⽅式称之为Python的扩
展数据结构,⽐如栈,队列等。
3.2 算法与数据结构的区别
数据结构只是静态的描述了数据元素之间的关系。
⾼效的程序需要在数据结构的基础上设计和选择算法。
程序 = 数据结构 + 算法
总结:算法是为了解决实际问题⽽设计的,数据结构是算法需要处理的问题
载体
3.3抽象数据类型(Abstract Data Type)
抽象数据类型(ADT)的含义是指⼀个数学模型以及定义在此数学模型上的⼀
组操作。即把数据类型和数据类型上的运算捆在⼀起,进⾏封装。引⼊抽象
数据类型的⽬的是把数据类型的表示和数据类型上运算的实现与这些数据类
型和运算在程序中的引⽤隔开,使它们相互独⽴。
最常⽤的数据运算有五种:插⼊、删除、修改、查找、排序

二、顺序表
根据线性表的实际存储⽅式,分为两种实现模型:
1、顺序表,将元素顺序地存放在⼀块连续的存储区⾥,元素间的顺序关系
由它们的存储顺序⾃然表示。
2、链表,将元素存放在通过链接构造起来的⼀系列存储块中。
1、顺序表的基本形式
分为两种:连续存储的顺序表和元素外置的顺序表
2、顺序表的结构与实现
⼀个顺序表的完整信息包括两部分,⼀部分是表中的元素集合,另⼀部分是
为实现正确操作⽽需记录的信息,即有关表的整体情况的信息,这部分信息
主要包括元素存储区的容量和当前表中已有的元素个数两项。
顺序表的两种基本实现⽅式:⼀体式结构和分离式结构。
2.1元素存储区替换
一体式结构:整体搬迁,即整个顺序表对象(指存储顺序表的结构信息的区
域)改变了。
分离式结构:将表信息区中的数据区链接地址更新即可,⽽该顺序表对象不变。
2.2元素存储区
扩充的两种策略:
a、每次扩充增加固定数⽬的存储位置,特点:节省空间,但操作次数多
b、每次扩充容量加倍,特点:减少了扩充操作的执⾏次数,但可能会浪费空间资源。以空间换时间,推荐的⽅式。
3、顺序表的操作
3.1 增加元素
a. 尾端加⼊元素,时间复杂度为O(1)
b. ⾮保序的加⼊元素(不常⻅),时间复杂度为O(1)
c. 保序的元素加⼊,时间复杂度为O(n)
3.2删除元素
a. 删除表尾元素,时间复杂度为O(1)
b. ⾮保序的元素删除(不常⻅),时间复杂度为O(1)
c. 保序的元素删除,时间复杂度为O(n)
4、Python中的顺序表
Python中的list和tuple两种类型采⽤了顺序表的实现技术
list是⼀种采⽤分离式技术实现的动态顺序表。
三、链表
链表结构可以充分利⽤计算机内存空间,实现灵活的内存动态管理。
链表的定义
链表(Linked list)是⼀种常⻅的基础数据结构,是⼀种线性表,但是不像顺
序表⼀样连续存储数据,⽽是在每⼀个节点(数据存储单元)⾥存放下⼀个
节点的位置信息(即地址)。
1、单向链表
单向链表也叫单链表,是链表中最简单的⼀种形式,它的每个节点包含两个
域,⼀个信息域(元素域)和⼀个链接域。这个链接指向链表中的下⼀个节
点,⽽最后⼀个节点的链接域则指向⼀个空值。
在这里插入图片描述
单链表的操作
is_empty() 链表是否为空
length() 链表⻓度
travel() 遍历整个链表
add(item) 链表头部添加元素
append(item) 链表尾部添加元素
insert(pos, item) 指定位置添加元素
remove(item) 删除节点
search(item) 查找节点是否存在

1.1链表与顺序表的对⽐
链表失去了顺序表随机读取的优点,同时链表由于增加了结点的指针域,空
间开销⽐较⼤,但对存储空间的使⽤要相对灵活。
链表与顺序表的各种操作复杂度如下所示:

操作 链表 顺序表
访问元素 O(n) O(1)
在头部插⼊/删除 O(1) O(n)
在尾部插⼊/删除 O(n) O(1)
在中间插⼊/删除 O(n) O(n)
2、双向链表
⼀种更复杂的链表是“双向链表”或“双⾯链表”。每个节点有两个链接:⼀个指
向前⼀个节点,当此节点为第⼀个节点时,指向空值;⽽另⼀个指向下⼀个
节点,当此节点为最后⼀个节点时,指向空值。
在这里插入图片描述
操作
is_empty() 链表是否为空
length() 链表⻓度
travel() 遍历链表
add(item) 链表头部添加
append(item) 链表尾部添加
insert(pos, item) 指定位置添加
remove(item) 删除节点
search(item) 查找节点是否存在
3、单向循环链表
单链表的⼀个变形是单向循环链表,链表中最后⼀个节点的next域不再为
None,⽽是指向链表的头节点。
在这里插入图片描述
四、栈
由于栈数据结构只允许在⼀端进⾏操作,因⽽按照后进先出(LIFO, Last In
First Out)的原理运作。
在这里插入图片描述
1、栈结构实现
栈可以⽤顺序表实现,也可以⽤链表实现。
2、栈的操作
Stack() 创建⼀个新的空栈
push(item) 添加⼀个新的元素item到栈顶
pop() 弹出栈顶元素
peek() 返回栈顶元素
is_empty() 判断栈是否为空
size() 返回栈的元素个数
五、队列
队列是⼀种先进先出的(First In First Out)的线性表,简称FIFO。
1、队列的实现
同栈⼀样,队列也可以⽤顺序表或者链表实现。
操作
Queue() 创建⼀个空的队列
enqueue(item) 往队列中添加⼀个item元素
dequeue() 从队列头部删除⼀个元素
is_empty() 判断⼀个队列是否为空
size() 返回队列的⼤⼩
2、双端队列
双端队列中的元素可以从两端弹出,其限定插⼊和删除操作在表的两端进
⾏。双端队列可以在队列任意⼀端⼊队和出队。
操作
Deque() 创建⼀个空的双端队列
add_front(item) 从队头加⼊⼀个item元素
add_rear(item) 从队尾加⼊⼀个item元素
remove_front() 从队头删除⼀个item元素
remove_rear() 从队尾删除⼀个item元素
is_empty() 判断双端队列是否为空
size() 返回队列的⼤⼩
六、排序与搜索
排序算法的稳定性
稳定性:稳定排序算法会让原本有相等键值的纪录维持相对次序。
1、冒泡排序
1.1冒泡排序算法的运作如下:
1、⽐较相邻的元素。如果第⼀个⽐第⼆个⼤(升序),就交换他们两个。
2、对每⼀对相邻元素作同样的⼯作,从开始第⼀对到结尾的最后⼀对。这
步做完后,最后的元素会是最⼤的数。
3、针对所有的元素重复以上的步骤,除了最后⼀个。
4、持续每次对越来越少的元素重复上⾯的步骤,直到没有任何⼀对数字需
要⽐较。
1.2代码:
def bubble_sort(alist):
“”“冒泡排序”""
n = len(alist)
# 外层循环控制从头走到尾的次数
for j in range(n-1):
# j [0, 1, 2, 3, …, n-2]
count = 0
# 内层循环控制从头走到尾的遍历
# i [0,1,2,…,n-2-j]
for i in range(0,n-1-j):
if alist[i] > alist[i+1]:
alist[i],alist[i+1] = alist[i+1],alist[i]
count += 1
if count == 0:
break

if name == ‘main’:
li = [3,5,6,3,7,77,44,22,55,2]
print(li)
bubble_sort(li)
print(li)

1.3时间复杂度
最优时间复杂度:O(n) (表示遍历⼀次发现没有任何可以交换的元素,排序结束。)
最坏时间复杂度:O(n^2 )
稳定性:稳定
2、选择排序
2.1它的⼯作原理如下:
⾸先在未排序序列中找到最⼩(⼤)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最⼩(⼤)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

2.2代码:
def xuanze(li):
for min in range(len(li) - 1): # min 代表把第一个认为是最小的
for i in range(min + 1, len(li)): # i 代表是无序中所指元素
# min与i所指向的元素依次进行比较,当遇到比min小的时候,交换,直到本次比较循环结束,一次循环在无序数列中找到一个最小的
if li[min] > li[i]: # min与i所指向的元素依次进行比较,当遇到比min小的时候,交换,直到本次比较循环结束
li[min], li[i] = li[i], li[min]
print(li)

if name == ‘main’:
xuanze([4, 2, 5, 7, 2, 4, 8, 44, 21, 6, 88, 1])

2.3时间复杂度
最优时间复杂度:O(n^2 )
最坏时间复杂度:O(n ^2)
稳定性:不稳定(考虑升序每次选择最⼤的情况
3、插⼊排序
3.1工作原理
是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插⼊。插⼊排序在实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插⼊空间。
3.2 代码
def insert(li):
“”“插入排序”""
# 从第二个位置,即下标为1的元素开始向前插入
for j in range(1, len(li)): # j 是 i 的后一位
# 从第j个元素开始向前比较,如果小于前一个元素,交换位置
for i in range(j, 0, -1): # i 的值依次是j,j-i,j-2,j-3,…,1
if li[i] < li[i - 1]: # 以此和前一位比较大小
li[i], li[i - 1] = li[i - 1], li[i]
else:
break

if name == ‘main’:
list1 = [5,2,7,6,9,3,2,8,1]
print(list1)
insert(list1)
print(list1)
3.3时间复杂度
最优时间复杂度:O(n) (升序排列,序列已经处于升序状态)
最坏时间复杂度:O(n^2)
稳定性:稳定
4、希尔排序
希尔排序(Shell Sort)是插⼊排序的⼀种。也称缩⼩增量排序,是直接插⼊排序算法的⼀种更⾼效的改进版本。希尔排序是⾮稳定排序算法。该⽅法因DL.Shell于1959年提出⽽得名。 希尔排序是把记录按下标的⼀定增量分组,对每组使⽤直接插⼊排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减⾄1时,整个⽂件恰被分成⼀组,算法便终⽌。
4.1代码
def shell_sort(li):
“”“希尔排序”""
n = len(li)
gap = n // 2
while gap >= 1:
for j in range(gap, n): # gap 是无序子序列第一个需要处理的
i = j
while i - gap >= 0:
if li[i] < li[i - gap]:
li[i], li[i - gap] = li[i - gap], li[i]
i -= gap
else:
break
gap //= 2

if name == ‘main’:
li = [88, 55, 35, 76, 34, 67, 23, 45,54]
print(li)
shell_sort(li)
print(li)
4.2时间复杂度
最优时间复杂度:根据步⻓序列的不同⽽不同
最坏时间复杂度:O(n^2 )
稳定性:不稳定
5、快速排序
快速排序(英语:Quicksort),⼜称划分交换排序(partition-exchangesort),通过⼀趟排序将要排序的数据分割成独⽴的两部分,其中⼀部分的所有数据都⽐另外⼀部分的所有数据都要⼩,然后再按此⽅法对这两部分数据分别进⾏快速排序,整个排序过程可以递归进⾏,以此达到整个数据变成有序序列。
5.1步骤为:

  1. 从数列中挑出⼀个元素,称为"基准"(pivot),
  2. 重新排序数列,所有元素⽐基准值⼩的摆放在基准前⾯,所有元素⽐基准值⼤的摆在基准的后⾯(相同的数可以到任⼀边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  3. 递归地(recursive)把⼩于基准值元素的⼦数列和⼤于基准值元素的⼦数列排序。
    递归的最底部情形,是数列的⼤⼩是零或⼀,也就是永远都已经被排序好了。虽然⼀直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它⾄少会把⼀个元素摆到它最后的位置去。
    5.2 代码
    def quick_sort(li, start, end):
    “”“快速排序”""
    if start < end:
    left = start
    right = end
    mid = li[start] # 把left(start)所指的值赋给mid,此时left可以修改(接收其他值)
    # left 与right未重合,就向中间移动
    while left < right:
    while left < right and li[right] >= mid: # 从最右边开始与mid比较,只要满足条件,right向左走
    right -= 1
    # 退出循环,把right所指的元素赋给left
    li[left] = li[right] # left接到值
    while left < right and li[left] < mid: # 开始从最左边与mid比较,只要条件满足,left向右走
    left += 1
    # 退出循环,把left所指的值赋给right
    li[right] = li[left]
    # 从循环退出后,left 与 right相遇,即 left == right
    li[left] = mid
    # 左边部分进行快速排序
    quick_sort(li, start, left - 1)
    # 右边部分快速排序
    quick_sort(li, left + 1, end)

if name == ‘main’:
li = [88, 55, 35, 76, 34, 67, 23, 45]
print(li)
quick_sort(li,0,(len(li)-1))
print(li)
5.3时间复杂度
最优时间复杂度:O(nlogn)
最坏时间复杂度:O(n^2)
稳定性:不稳定
6、归并排序
归并排序是采⽤分治法的⼀个⾮常典型的应⽤。归并排序的思想就是先递归分解数组,再合并数组。
将数组分解最⼩之后,然后合并两个有序数组,基本思路是⽐较两个数组的最前⾯的数,谁⼩就先取谁,取了后相应的指针就往后移⼀位。然后再⽐较,直⾄⼀个数组为空,最后把另⼀个数组的剩余部分复制过来即可。
6.1 代码
def merge_sort(li):
“”“归并排序”""
n = len(li)
if 1 == n:
return li
# 下面是else的内容
mid = n // 2 # 将原列表分为两部分
# 左边部分用变量left_li 接收
left_li = merge_sort(li[:mid])
# 右边部分用变量left_li 接收
right_li = merge_sort(li[mid:])
# 定义两个变量left,right分别表示左右两个列表的索引,初始值为0,代表从第一个元素开始
left, right = 0, 0
# 定义两个变量,接收左右列表的长度
left_n = len(left_li)
right_n = len(right_li)

# 定义一个新列表,用于接收排序后的数据
new_list = list()
while left < left_n and right < right_n:
    if left_li[left] <= right_li[right]:
        new_list.append(left_li[left])
        left += 1
    else:
        new_list.append(right_li[right])
        right += 1
# 退出循环,表示left或者right的指向没有了元素,下面将左右列表剩余的元素加入到新列表中
new_list = new_list + left_li[left:]
new_list = new_list + right_li[right:]

# 函数返回值(新列表)
return new_list

if name == ‘main’:
li = [88, 55, 35, 76, 34, 67, 23, 45]
print(li)
merge_sort(li)
print(li)
new_li = merge_sort(li)
print(new_li)
6.2时间复杂度
最优时间复杂度:O(nlogn)
最坏时间复杂度:O(nlogn)
稳定性:稳定
七、树与树的算法
1、树的术语
节点的度:⼀个节点含有的⼦树的个数称为该节点的度;
树的度:⼀棵树中,最⼤的节点的度称为树的度;
叶节点或终端节点:度为零的节点;
⽗亲节点或⽗节点:若⼀个节点含有⼦节点,则这个节点称为其⼦节点的⽗节点;
孩⼦节点或⼦节点:⼀个节点含有的⼦树的根节点称为该节点的⼦节点;
兄弟节点:具有相同⽗节点的节点互称为兄弟节点;
节点的层次:从根开始定义起,根为第1层,根的⼦节点为第2层,以此类推;
树的⾼度或深度:树中节点的最⼤层次;
堂兄弟节点:⽗节点在同⼀层的节点互为堂兄弟;
节点的祖先:从根到该节点所经分⽀上的所有节点;
⼦孙:以某节点为根的⼦树中任⼀节点都称为该节点的⼦孙。
森林:由m(m>=0)棵互不相交的树的集合称为森林;
常⻅的⼀些树的应⽤场景
1.xml,html等,那么编写这些东⻄的解析器的时候,不可避免⽤到树
2.路由协议就是使⽤了树的算法
3.mysql数据库索引
4.⽂件系统的⽬录结构
5.所以很多经典的AI算法其实都是树搜索,此外机器学习中的decision tree也是树结构
2、⼆叉树
⼆叉树是每个节点最多有两个⼦树的树结构。通常⼦树被称作“左⼦树”(leftsubtree)和“右⼦树”(right subtree)
⼆叉树的性质(特性)
性质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 时为根,除外)
3、⼆叉树的遍历
深度优先遍历和⼴度优先遍历,深度优先⼀般⽤递归,⼴度优先⼀般⽤队列。

深度优先遍历有三种:
1、 先序遍历 :根节点->左⼦树->右⼦树
2、中序遍历 :左⼦树->根节点->右⼦树
3、后序遍历 :左⼦树->右⼦树->根节点

实现代码:
class Node():
“”“节点类”""

def __init__(self, item):
    self.item = item
    self.lchild = None
    self.rchild = None

class BinaryTree():
“”“树类”""

def __init__(self, node=None):
    self.root = node

def add(self, item):
    """广度优先遍历方式为树添加节点"""
    # 如果树是空的,则对根节点赋值
    if self.root == None:
        self.root = Node(item)
    else:
        queue = list()
        queue.append(self.root)
        # 对已有的节点进行层次遍历
        while queue:  # 条件一直成立就行
            # 弹出队列的第一个元素
            node = queue.pop(0)
            if node.lchild is None:
                node.lchild = Node(item)
                return
            else:
                queue.append(node.lchild)
            if node.rchild is None:
                node.rchild = Node(item)
                return
            else:
                queue.append(node.rchild)

def breadh_travel(self):
    """广度优先遍历"""
    if self.root is None:
        return
    queue = list()
    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)

def preorder_travel(self, root):
    """先序遍历,根 左 右"""
    if root:
        print(root.item, end=" ")
        self.preorder_travel(root.lchild)
        self.preorder_travel(root.rchild)

def inorder_travel(self, root):
    """中序遍历,左 根 右"""
    if root:
        self.inorder_travel(root.lchild)
        print(root.item, end=" ")
        self.inorder_travel(root.rchild)

def posorder_travel(self, root):
    """后序遍历,左 右 根 """
    if root:
        self.posorder_travel(root.lchild)
        self.posorder_travel(root.rchild)
        print(root.item, end=" ")

if name == ‘main’:
t1 = BinaryTree()
t1.add(0)
t1.add(1)
t1.add(2)
t1.add(3)
t1.add(4)
t1.add(5)
t1.add(6)
t1.add(7)
t1.add(8)
t1.add(9)
t1.breadh_travel() # 0 1 2 3 4 5 6 7 8 9
print()
t1.preorder_travel(t1.root) # 0 1 3 7 8 4 9 2 5 6
print()
t1.inorder_travel(t1.root) # 7 3 8 1 9 4 0 5 2 6
print()
t1.posorder_travel(t1.root) # 7 8 3 9 4 1 5 6 2 0

猜你喜欢

转载自blog.csdn.net/gxz987/article/details/89059994