数据结构与算法

一 数据结构概论

数据结构与抽象数据类型

结构:实体+关系
数据结构:按照逻辑关系组织起来的一批数据。按照一定的存储方法把它存储在计算机,在这些数据上定义了一个运算的集合。

逻辑结构

逻辑结构分线性结构与非线性结构:
线性结构:表,栈,队列,串等
非线性结构:树、图(有向,无向)

存储结构

存储结构是逻辑结构到物理存储空间的映射,存储结构包含四类:顺序、链接、索引、散列。
散列:一种特殊的索引结构。通多所要查的关键码的一个映射,能够在整个散列表里快速地找到它的存储空间,然后把相对应的数值找出来。

抽象数据类型(ADT):定义了一组运算的数学模型,与存储结构无关,使软件系统建立在数据之上(面向对象)
实际中,我们着重考虑的是以什么样的逻辑结构去达到我们期待的目标,同时这种结构支持什么运算。而存储结构更多是为了我们所做的可以在计算机上执行。
在抽象数据数据类型中,数据元素一对一是线性结构,一对多是树结构,多对多是图形结构。

算法

目标:解决问题
问题:从输入到输出的一个映射。本质上就是一个函数
算法:解决问题的方法。对特定问题求解过程的描述,是指令的有限序列。
具有通用性、有效性、确定性、有限性。
程序:采用某种编程语言实现的算法。

算法分析主要是从计算资源消耗的角度来评断和比较算法。
算法分析的目标是找出问题规模会怎么影响一个算法的执行时间。

二 基本结构

线性结构

包括栈、队列、双端队列和列表,它们的共同点在于数据项之间只存在先后的次序关系。
栈:先进后出,后进先出
创建一个栈的空对象

from pythonds.basic.stack import Stack

队列:数据项加入队列,首先出现在队尾,随着队首数据项的移除,逐渐接近队首。(先进先出)
队列仅有一个入口和一个出口,不允许数据项的中间插入或移除。

from pythonds.basic.queue import Queue

双端队列:两端都可以作为“首”,“尾”,某种意义上说,双端队列集成了栈和队列的能力。但如果用双端队列来模拟栈或列,需要由使用者自行维护操作的一致性。

from pythonds.basic.deque import Deque

无序表:List。为了实现无序表数据结构,可以采用链接表的方案。(不要求数据项依次存放在连续的存储空间)
链表实现的最基本元素是节点Node。(至少包含数据项本身以及下一个节点的引用信息)
链表的第一个和最后一个节点最重要。第一个节点的引用为head。随着数据项的加入,无序表的head始终指向链条中的第一个节点。
注意:无序表对象本身并不包含数据项,数据项是在节点中。其中包含的head只是对首个节点Node的引用
有序表:OrderedList.有序表与无序表相比,只是add操作与serach操作有一些变化,因为需要保证链接的正确性完整性。
链表实现的List与python内部内置的链表数据类型在有些相同方法的实现上的时间复杂度不同,主要是因为python内置的列表数据类型是基于顺序存储来实现的,并进行了优化。

递归

递归算法三定律:
1、必须有一个基本结束条件(最小规模问题的直接解决);
2、必须能改变状态向基本结束条件演进(减小问题规模);
3、必须能调用自身(解决减小了规模的相同问题)。
调试递归算法时,如果没有设置基本结束条件,或者向基本结束条件演进太慢,导致层数太多,调用栈溢出,会碰到RecursionError。
在python内置的sys模块中可以获取和调整最大递归深度:

import sys
sys.getrecursionlimit()
sys.setrecursionlimit(num)

应用范围:排序,查找,遍历,求值等等。

三 排序与查找

顺序查找

如果数据项保存在如列表这样的集合中,则称这些数据项具有线性或者顺序关系。

二分法查找

二分查找,当比对次数足够多时,比对范围内就会仅剩余1个数据项。
二分法查找的算法复杂度是O(log n)。
二分查找的思想是分而治之,递归算法也是一种典型的分治策略算法,二分法也适合用递归算法来实现。
但是如果使用递归去进行二分查找,会使用到列表切片,而切片操作的复杂度为O(n),这样会使整个算法的时间复杂度稍有增加。另外,在使用二分查找时也考虑到对数据项进行排序的开销。

冒泡排序与选择排序

对无序表多次比较交换,每次包括了多次两两比较,并将逆序的数据项互换位置,每一轮后的最后一个数据项是最大项。
时间复杂度O(n)。
冒泡排序通常作为时间较差的排序算法,作为其他算法的对比基准。
但优势在于:冒泡排序无需任何额外的存储空间开销。
选择排序对冒泡排序进行了改进,保留了其基本的多趟比对思路,但对交换进行了削减,每次仅进行1次交换,记录最大项所在位置,最后再跟本次最后一项交换。
选择排序的时间复杂度比冒牌排序稍优:
比对次数不变,依旧是O(n^2),
交换次数则减少为O(n)。

插入排序法

插入排序维持一个已经排好序的子列表,其位置始终在列表的前部,然后逐步扩大这个字列表直到全表

谢尔排序法

引用:https://blog.csdn.net/qq_36772866/article/details/89671717?ops_request_misc=&request_id=&biz_id=102&utm_term=%E8%B0%A2%E5%B0%94%E6%8E%92%E5%BA%8F%E6%B3%95&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-0-89671717

归并排序法

属于递归算法,思路是将数据表持续分裂成两半,对两半分别进行归并排序。
分裂过程是对数复杂度,时间复杂度为O(log n),归并的过程是相对于分裂的每个部分,其所有数据项都会被比较和放置一次,时间复杂度是O(n)。综合考虑总的时间复杂度是O(n log n)。
最后,要注意,排序算法时归并时使用了额外1倍的存储空间。

快速排序法

引用:https://blog.csdn.net/qq_40941722/article/details/94396010?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase

散列

散列表:也称哈希表,是一种数据集,其中数据项的存储方式尤其有利于将来快速的查找定位。散列表中的每一个存储位置,称为槽,可以用来保存数据项,每个槽都有唯一的名称。
实现从数据项到存储槽名称的转换的,称为散列函数。
完美散列函数:给定一组数据项,一个散列函数能把每个数据项映射到不同的槽中。
获得完美的散列函数的一种方法是扩大散列表的容量,大到所有可能出现的数据项都能够占据不同的槽,但这种方法并不适用且很浪费存储空间。
好的散列函数的特性:冲突最少,计算难度低(额外开销少)、充分分散数据项(节约空间)
在这里插入图片描述
最著名的近似完美散列函数是MD5和SHA系列函数。
MD5将任何长度的数据变换为固定长为128位(16字节)的“摘要”
SHA系列包括:
SHA-0和SHA-1输出散列值为160位;
SHA0256/SHA-224分别输出为256位和224位
SHA-512/SHA-384分别输出为512位和384位。
散列函数的应用:
区块链:一种分布式数据库,通过网络连接的节点,每个节点都保存着整个数据库所有数据,任何地点存入的数据都会完成同步。本质特征是“去中心化”。

散列函数的设计

折叠法:将数据项按照位数分为若干段,再将几段数字相加,最后对散列表大小求余,得到散列值。
平方取中法:将数据项做平方运算,然后取平方数的中间两位,再对散列表的大小求余。
非数项:对非数字的数据项进行散列,把字符串中的每个字符看作ASCII码,再把这些整数累加,对散列表大小求余。

散列函数设计冲突解决方案

一、再找一个开放的空槽来保存。最简单的就是从冲突的槽开始往后扫描,直到碰到了一个空槽。如果散列表尾部还未找到,则从首部接着扫描。
这种寻找空槽的技术称为“开放定址”。

抽象数据类型映射:ADT Map

键-值关联的无序组合。
key具有唯一性,通过key可能确定唯一一个数值。
在这里插入图片描述

总结

在无序表或有序表山的顺序查找,其时间复杂度为O(n)
在有序表上进行二分查找,其最差时间复杂度为O(log n)
散列表可以实现常数级时间的查找
冒泡、选择和插入排序是O(n^2)的算法
希尔排序在插入排序的基础上进行改进,时间复杂度在O(n)和O(n^2)之间
归并排序的时间复杂度是O(n log n),但归并过程需要额外的存储空间
快速排序最好的时间复杂度是O(n log n),也不需要额外的存储空间,最坏情况下退化到O(n^2)。

特点:1、层次化 2、一个节点的子节点与另一个节点的子节点相互之间是隔离、独立的。 3、每一个节点都具有唯一性。

嵌套列表法实现

递归的嵌套列表实现二叉树,由具有3个元素的列表实现:
第一个元素为根节点的值;
第二个元素是左子树(也是一个列表);
第三个元素是右子树(也是一个列表)。
[root, left, right]

节点链接法

每个节点保存根节点的数据项,以及指向左右子树的链接

树的遍历

对一个数据集中的所欲数据项进行访问。
线性数据结构中,对其所有数据项的访问比较简单直接
树的非线性特点,使得遍历操作作为复杂。

前序遍历

先访问根节点,再递归地前序访问左子树,最后前序访问右子树
在这里插入图片描述

中序遍历

先递归地中序访问左子树,再访问根节点,最后中序访问右子树
生成全括号的中缀表达式。

后序遍历

先递归地后序访问左子树,再后序访问右子树,最后访问根节点。
解析树后,求值可用迭代的后序遍历。

优先队列

与队列一样,从队首出队,但在优先队列内部,数据项的次序是由“优先级”来确定。
实现优先队列的经典方案是采用二叉堆数据结构,能把入队和出队复杂度都保持在O(log n)。
二叉堆的逻辑结构上类似二叉树,但用的是非嵌套列表实现的。
在这里插入图片描述

from pythonds.trees.binheap import BinHeap

为了使堆操作保持在对数水平上,必须采用二叉树结构;
如果要使操作始终保持在对数数量,必须始终保持二叉树的平衡(原因?)

完全二叉树:叶节点最多只出现在最底层和次底层,而且最底层的叶节点都连续集中在最左边,每个内部节点都有两个子节点(最多可以有1个节点例外)
堆次序:任何一个节点x,其父节点p中的key均小于x中的key,这样,符合“堆”性质的二叉树,其中任何一条路径,均是一个已排序数列,根节点的key最小、

二叉查找树

ADT MAP实现方案有:
1、有序表数据结构+二分搜索算法
2、散列表数据结构+散列及冲突解决算法
3、二叉查找树

二叉查找树的性质:比父节点小的key都出现在左子树,比父节点大的key都出现在右子树。
注意:插入顺序不同,生成的BST也不同。

图是比树更为一般的结构,也是由节点和边构成。
实质上树是一种具有特殊性质的图。
顶点(Vertex)也称节点,具有名称标识key,也可以携带数据项val
边(edge)也称弧,边连接两个顶点,可以是有向或者无向
权重(Weight)
一个图可以定义为G=(V,E),V是顶点集合,E是边的集合。

图的抽象数据类型

在这里插入图片描述
ADT Graph的实现方法有两种主要形式:
邻接矩阵:优点是简单,可以很容易得到顶点是如何相连;缺点在于如果边数很少则效率低下,很容易成为稀疏矩阵。
邻接列表:存储空间更加紧凑高效

广度优先搜索(BFS)

给定图G,以及起点S,搜索所有从s可到达的顶点的边,在达到更远的距离k+1的顶点之前,BFS会找到所有距离为k的顶点
为了寻找顶点的重复,需要给顶点增加3个属性:
1、起始顶点到此顶点的距离
2、前驱顶点
3、颜色:标记该顶点是未被发现、已经发现、已经完成探索
还需要用一个队列Queue来为已发现的顶点排列(队列内无已经完成探索的点)。
可用启发式规则对其进行改进,即对移动的目标排序进行更改:具有最少合法移动目标的格子优先搜索。

深度优先搜索(DFS)

在图上进行尽量深的搜索,连接到尽量多的顶点,必要时可进行分支
需要用到顶点的“前驱”属性来构建树或森林,另外需要设置发现时间和结束时间,

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值