四、数据结构与算法
40.1 数据结构是一种数据组织、管理和存储的格式。是指数据元素的集合或者数据对象的集合,以及元素之间的相互关系和构造方法。即相互之间存在一定关系的数据元素的集合。
40.2 数据结构可以的要三素:逻辑结构、物理结构、数据的运算。
逻辑结构包括集合结构(数据元素间没关系)、线性结构(数据元素间一对一关系)、树形结构(数据元素间一对多关系)和图形结构(数据元素间多对多关系)。
物理结构则是指数据的逻辑结构在计算机存储中的表示方式,所以物理结构也叫存储结构。
主要包括顺序存储结构和链式存储结构。顺序存储结构如数组,链式存储结构如链表。
顺序结构存储是指用一组连续的存储单元存放元素;链式存储结构是指用一组任意的存储单元存放元素。
数据的运算就是:根据逻辑结构来定义,根据存储结构来实现。
40.3 常用的八种数据结构:数组、栈、队列、链表、树、图、堆、散列表。
41.1 线性结构和非线性结构
线性结构:第一个元素只有一个“后继”和最后一个元素只有一个“前驱”,其它元素只有一个“前驱”元素和一个“后继”元素。
非线性结构:每个元素可以和多个元素“连接”,也就是每个元素“前驱”和“后继”的个数不确定。表示数据元素之间存在复杂的关系,通常不按线性方式排列。
线性结构数据元素之间存在一对一的关系,如数组、链表、栈和队列,树形结构如树,图形结构如图。
非线性结构则是指数据元素之间存在多对多关系的结构,常见的有树和图、二维数组、广义表、矩阵。
41.2 线性表是n个数据元素的有限序列。表中元素存在线性关系。根据它们之间的关系可以构成一个线性序列。线性表的存储方式有两种:顺序存储和链式存储。
顺序存储:即顺序表,用一组联系的存储单元一次存储线性表中的数据元素,使得元素在逻辑和物理上都是相邻。
链式存储:即链表,存储各数据元素的节点的地址并不要求连续,数据元素在逻辑上连续,物理上不连续。
42.1 顺序表:把线性表中的所有元素按照其逻辑顺序依次存储在一块连续的存储空间中,就得到顺序表。如一维数组。
访问和查询元素效率高,插入和删除效率低:
因为各个元素是连续储存,所以可以按下标随机访问,访问特定元素效率很高。但在删除或插入元素为保证整体的存储空间连续需要移动后面的其它元素, 因此删除、插入元素效率低。
静态顺序表:使用定长数组存储元素;动态顺序表:需要多少空间,就开辟多少空间的大小。
43.1 链表:各个元素存储在任意的地址空间,逻辑相邻的元素在物理内存中没有联系。结点由data(数据域)和link(指针域/链域)组成。
链表的首元结点地址通过头指针first找到,其余存储在前驱节点的link域中。
data(数据域,存放数据元素)和link(指针域/链域,存放用于记录下一个结点开始存储地址的指针)
访问和查询元素相对顺序表来说效率低,插入和删除相对顺序表来说效率高:
由于物理内存没联系,执行插入或者删除操作时,不需要移动其它元素,只需修改相关结点指针域即可,所以效率很高。
但因为物理内存没联系,所以只能通过前一个元素访问下一个元素,每一次访问元素都需从头节点开始遍历,所以访问特定元素或查找元素效率低。
链表根据链式结构特点可分为:单链表、循环链表、双向链表。
43.2 单链表:每个节点只有一个指针域,指向下一个节点,最后一个节点的指针域指向NULL,表示链表的结尾。
43.3 双向链表:节点有两个指针域,一个指向前一个节点,一个指向后一个节点,方便从两个方向遍历链表。
43.4 循环链表:单链表或双向链表的一种变形,尾节点的指针域指向表头节点,形成环形结构。循环链表在现实中的应用场景约瑟夫问题、拉丁方阵问题。
43.5 单说在链式结构中做查询对应位置的数值、查询数的位置、插入、删除操作时,查询数值效率是最高的。
因为四个操作都要从头节点开始做操作,找到对应位置。
查询数值只需要根据链找到对应的位置读取数;
查询数的位置读数后还需要比较大小;
插入读数后还要断开链表,插入块后再拼接链表;
删除在读数后还要断开链表,删除块后再拼接链表。
44.1 队列:遵循先进先出(First In First Out, FIFO)原则。这意味着最先被添加到队列中的元素将会是最先被移除的。如食堂打饭一样,先排队先打饭。
队列的头部,俗称前端或者队头。队列的尾部,俗称后端或者队尾。
出队:从队列的前端(front)移除一个元素。入队:在队列的后端(rear)添加一个新元素。
队列操作:查看队首元素:返回队列头部的元素但不移除它。查看队尾元素:返回队列尾部的元素但不移除它。判空(IsEmpty):检查队列是否为空。
44.2 双端队列是指允许两端都可以进行入队和出队操作的队列;逻辑结构仍是线性结构;
将队列的两端分别称为前端和后端,两端都可以入队和出队。
分为:
输出受限的双端队列:允许在一端进行插入和删除,但在另一端只允许插入的双端队列;
输入受限的双端队列:允许在一端进行插入和删除,但在另一端只允许删除的双端队列;
44.3 循环队列:是一种线性数据结构,其操作基于先进先出(FIFO)的原则,队尾连接到队首形成一个循环,从而充分利用向量空间,避免“假溢出”现象。
队首元素front和队尾元素rear都指向第一个结点(frontrear),就可以表示空。
如果循环队列不是空,会多开一个空余结点(开k+1个),此时判满则为rear->nextfront。
删除操作时front会往后移动front=front->next,被“删除”的数据也不用抹掉,因为后续再入数据给会他覆盖掉。如果删除空了则会出现frontrear。
如果是插入操作时,rear结点会往后移动一位rear=rear->next。(如果在尾部,则会移动到前面删除的第一个位置。)此时判满仍是rear->nextfront。。
循环队列长度计算公式为(Q.tail-Q.head)/size.tail是队尾元素,head是队首元素,size是循环队列的存储空间容量。
而队首的计算公式位(tail+1)/size。
45.1 栈(Stack)是一种特殊的线性表,其特殊性在于它只能在一端进行插入和删除操作,这一端被称为栈顶,另一端则被称为栈底。
栈遵循“后进先出”(Last In First Out,LIFO)的原则,即最后一个被放入栈中的元素总是第一个被取出。 先进后出、后进先出。
栈常用于实现函数调用、表达式求值、数制转换、迷宫求解等算法。
栈顶:允许插入删除的一端。 栈底:不允许插入删除的一端。 空栈:不包含任何元素的空表。
考试考法:元素按照a、b、c的次序进入栈,请尝试写出其所有可能的出栈序列。 所有可能的出栈序列为:abc、cba、bca、acb。
栈的出栈序列满足:正序排列前面不会存在比正序最后1位大的数。如312.(12为正序,3是正序前的数,这种在栈中就不可能存在)
如出栈序列可以为:123,132,231,213,321。而不会有312的出栈序列。
46.1 数组是一系列类型相同的元素组成的一个结合。
一维数组可以看做一个顺序表。
二维数组可以想成Excel中的由行列组成的单元格。
二维数组有两种存储方式:按行存储(逐行存满再存下一行)、按列存储(逐列存满再存下一行)。
偏移量:是相对于第一个元素,移动了多少个元素的位置;存储地址是:相对于第一个元素,移动了多少个地址。
如元素a[2][3]按行存储(每个元素占2个字节),下标0开始编号时的偏移量是13;存储地址是a(即a[0][0]地址)+132;
下标1开始编号时的偏移量是7;存储地址是a(即a[1][1]地址)+72;
46.2 矩阵分为特殊矩阵、稀疏矩阵。
特殊矩阵:矩阵中的元素分布有一定规律,如果对称矩阵、三角矩阵、对角矩阵。
稀疏矩阵:在矩阵中,非零元素的个数远远少于零元素的个数,且分布没有规律。存储方式按(行,列,值)的三元组结构存储。
46.3 广义表:是一种非线性结构,由n(n≥0)个元素a1,a2,…,ai,…,an的有限序列组成。
通过圆括号“()”括起来,通过逗号“,”隔开表中的各个数据元素。广义表中的数据元素可以是单个元素(原子),也可以是另一个广义表。
如,广义表A=(),B=(e),C=(a,(b,c,d)),D=(A,B,C),E=(a,E)分别表示空表、单个原子、包含原子和广义表的表、包含三个广义表的表以及递归定义的广义表。
广义表的第一个元素称为表头,其余元素组成的表称为表尾。广义表的长度是表中元素的个数,深度是通过括号的层数求得。
如,空广义表G=()的长度为0,深度为1;广义表C=(a,(b,c,d))的长度为2,深度为2。
46.4 哈希表(散列表):哈希表通过计算一个以记录的关键字为自变量的函数(哈希函数)来得到该记录的存储地址。
将一组关键字映射到一个连续的有限地址集(区间)上。
47.1 树是一种非线性的数据结构,它是由n(n>=0)个有限节点组成一个具有层次关系的集合。
把它叫做树是因为它看起来像一棵倒挂的树,也就是说它根朝上,而叶朝下。
结点的度:一个结点含有的子树的个数称为该结点的度,即直接子结点的个数;
叶子结点或终端结点:度为0的结点称为叶结点(即没有子结点);
非终端结点或分支结点(内部结点):度不为0的结点;
双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点;
孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点;
兄弟结点:具有相同父结点的结点互称为兄弟结点;
树的度:一棵树中,最大的结点的度称为树的度;
结点的层次:从根开始定义起,根为第1层,根的子结点为第2层,以此类推;
树的高度或深度:树中结点的最大层次;
堂兄弟结点:双亲在同一层的结点互为堂兄弟;
结点的祖先:从根到该结点所经分支上的所有结点;
子孙:以某结点为根的子树中任一结点都称为该结点的子孙;
森林:由m(m>0)棵互不相交的树的集合称为森林;多颗树就叫做森林;
如A(BCD),B(EF),C(G),D(HIJ),E,F(K,L),G,H(M)I,J,K,L,M 括号内表示为其子结点。
则结点A的度是3(3个子树BCD),结点B的度是2(2个子树EF),EGIJKLM都是叶子结点(没有子结点),ABCDFH都是分支结点(有子结点);
BCD是A的孩子结点,A是BCD的双亲结点,B和C是兄弟结点;该树的度是3(最大的度,BCD);C结点的层次是第2层;
树的高度为4(ABFK);L和M是堂兄弟结点(双亲在同一层);L的祖先有ABF;B的子孙有EFKL(子树中任一结点);
BCD组成森林。
有序(无序)树:如果一棵树中结点的各子树丛左到右是有次序的,即若交换了某结点各子树的相对位置,则构成不同的树,称这棵树为有序树;
反之,则称为无序树。
47.2 二叉树:每个节点最多只有2个子节点的树叫做二叉树。
满二叉树:每层都是满结点的。
完全二叉树:k-1层是满结点,第k层结点从左到右是满的。
线索二叉树:即加上线索的二叉树。通过利用二叉链表中的两个空指针域存放结点在某种遍历次序下的前驱和后继结点的指针,来提高查找效率。
最优二叉树:又称哈(霍)夫曼树。在所有具有相同叶子节点数的二叉树中,哈夫曼树的带权路径长度最小,即其总权重与路径长度的乘积之和最小。
是一类带权路径长度最短的二叉树,哈夫曼树广泛应用于数据压缩领域,特别是在数据压缩算法中。
二叉查找树:若其左子树非空,则左子树上所有结点的值均小于根节点的值;若其右子树非空,则右子树上所有结点的值均大于等于根节点的值;
其左右子树本身又各是一棵二叉查找树。这种有规律的排列的二叉树可以方便查找可插入数据。
平衡二叉树:也叫AVL树,它或是一颗空树,或它的左子树和左子树的高度之差(平衡因子)的绝对值不超过1,且它的左子树和右子树都是一颗平衡二叉树。
单枝树:是指非叶子节点只有一个孩子的特殊二叉树。
47.3 二叉树的特点:
1.若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有2的i-1次方个结点
2.若规定根结点的层数为1,则深度为h的二叉树的最大结点数是2的h次方-1
3.对任何一棵二叉树,如果度为0其叶结点个数为N0,度为2的分支结点个数为N2,则有N0=N2+1;
4.若规定根结点的层数为1,具有n个结点的满二叉树的深度,h=log2(n+1);
5.对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从0开始编号,则对于序号为i结点有:
(1)若i>0,i位置结点的双亲序号:(i-1)/2;i=0,i为根结点编号,无双亲结点
(2)若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子
(3)若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子
47.4 二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
顺序结构存储是指用一组连续的存储单元存放元素,如使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。
链式存储结构是指用一组任意的存储单元存放元素,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址。
链式结构又分为二叉链和三叉链。
47.5 二叉树的遍历:遍历的顺序–从上往下,从左往右。
前序遍历(DLR先序遍历,即根先遍历):先遍历根,再先序遍历左子树,最后先序遍历右子树。
中序遍历(LDR即根中间遍历):先中序遍历左子树,再遍历根,最后中序遍历右子树。
后序遍历(LRD即根最后遍历):先后序遍历左子树,再后序遍历右子树,最后遍历根。
深度优先遍历:对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次。
广度优先遍历:对每一层节点依次访问,访问完一层进入下一层,而且每个节点只能访问一次。
47.6 最优二叉树WPL:哈(霍)夫曼树。带权路径长度最短的二叉树。左子树的值小于右子树的值,左子树对应0,右子树对应1。
权:将树中的结点赋上一个有着某种意义的数值,即结点的值。
路径:从A结点道B结点所经过的分支序列。
路径长度:从A结点道B结点所经过的分支数目。
树的路径长度:根结点到底每个叶子结点之间的路径长度之和。
结点的带权路径长度:即结点到根结点之间路径长度乘以该结点的权值。
树的带权路径长度(树的代价)=树的所有子结点的带权路径长度之和。
哈夫曼树的求法:1.将n个结点作为n棵仅含有一个根结点的二叉树,构成森林F。
2.生成一个新结点,从F中找出根结点权值最小的两棵树作为它的左右子树(权值小的在左,大的在右),且新结点的权值为两棵子树根结点的权值之和
3.从F中删除这两个树,并将新生成的树加入到F中
4.重复2,3步骤,直到F中只有一棵树为止。
47.7 树和森林
树的存储结构:
双亲表示法:用一组联系的地址单元存储树的结点,并在每个结点中附带一个指示器,指出其双亲结点所在数组元素的下标。
孩子表示法:在存储结构中用指针指示出结点的每个孩子,为树中每个结点的孩子建立一个链表。
孩子兄弟表示法:又称二叉链表表示法,为每个存储结点设置两个指针域,分别指向该结点第一个孩子和下一个兄弟结点。
树和森林的遍历:
先根遍历:先访问根结点,再一次遍历根的各子树。
后根遍历:先访问各子树,最后访问根结点。
树转换为二叉树:
树中所有相邻兄弟之间加一条连线。
对树中的每个结点,只保留它与第一个孩子结点之间的连线,删去它与其它孩子结点之间的连线。
以树的根结点为轴心,将整棵树顺时针转动一定的角度,使之结构层次分明。
森林转换为二叉树:
将森林中的每棵树转换成相应的二叉树。
第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树根结点的右孩子,
当所有二叉树连起来后,此时所得到的二叉树就是由森林转换得到的二叉树。
47.8 堆在逻辑上是一颗完全二叉树(类似于一颗满二叉树只缺了右下角)。堆的物理结构是一个数组。
堆的实现用的是数组,用动态数组来存放元素,可以快速拓容也不会浪费空间,将这颗完全二叉树用层序遍历(即广度优先遍历)的方式储存在数组里的。
堆有两种分别是大根(顶)堆和小根(顶)堆 。
大根(顶)堆就是整个完全二叉树任意一个根节点的值都比左右子树的值大
小根(顶)堆表示整个完全二叉树任意一个根节点的值都比左右子树的值小。
我们可以用左右孩子节点和父节点的位置,来表示所有的节点。
leftchild = parent * 2 + 1;rightchild = parent * 2 + 2;parent = (child - 1) / 2;(child可以是左孩子,也可以是右孩子)。
堆插入一个元素时进行向上调整,把这个数放到合适的位置。堆删除一个元素时删除的是堆顶元素,即第一个元素。先让第一个元素和最后一个元素交换位置,移除原来的第一个元素;为了让新的数据成为堆,我们将新的第一个数据向下调整,使之变成一个新堆。
48.1 图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成的,通常表示为 G(V,E) ,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
它是一种比线性表和树更复杂的数据结构。线性表可以是空表,树可以是空树,但图不可以是空,即V一定是非空集。
简单图:1. 不存在重复边,2. 不存在顶点到自身的边。
简单路径:在路径序列中,顶点不重复出现的路径称为简单路径。
简单回路:除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。
路径:存在一条通路,可以从一个顶点到另一个顶点。有向图的路径是有方向的。
回路:第一个顶点和最后一个顶点相同的路径称为回路或环。
路径长度:路径上边的数目。
点到点的距离:从顶点u出发到顶点v的最短路径若存在,则此路径的长度称为从u到v的距离。若从u到v根本不存在路径,则记该距离为无穷。
边的权:在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值。
带权图/网:边上带有权值的图称为带权图,也称网。
带权路径长度:当图是带权图时,一条路径上所有边的权值之和,称为该路径的带权路径长度。
48.2 有向图与无向图:
有向图(Directed Graph):边是有方向的,用有序对 表示从顶点 指向顶点 的边,比如社交网络中,A 关注了 B,就可以用有向边表示。
无向图(Undirected Graph):边没有方向,用无序对 表示顶点 和 之间的边,像城市间的公路,双向通行,就可用无向边体现。
顶点的度、入度和出度:
度(Degree):在无向图中,与顶点 关联的边的数目称为 的度,例如一个顶点连接了 3 条边,那它的度就是 3。
入度(In-degree)和出度(Out-degree):在有向图中,以顶点 为终点的边的数目称为 的入度;以顶点 为起点的边的数目称为 的出度。比如在微博关注关系中,某用户被多少人关注就是入度,他关注了多少人就是出度。
无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的。若图G中任意两个顶点都是连通的,则称图G为连通图,否则称为非连通图。
有向图中,若从顶点v到顶点w和从顶点w到顶点v之间都有路径,则称这两个顶点是强连通的。若图中任何一对顶点都是强连通的,则称此图为强连通图。
对于n个顶点的无向图G,若G是连通图,则最少有 n-1 条边;若G是非连通图,则最多可能有(n-1)!/2条边;无向图中的极大连通子图称为连通分量。
对于n个顶点的有向图G,若G是强连通图,则最少有n条边(形成回路)。有向图中的极大强连通子图称为有向图的强连通分量。
连通图的生成树是包含图中全部顶点的一个极小连通子图(即要求边最少)。在非连通图中,连通分量的生成树构成了非连通图的生成森林。
无向完全图:无向图中任意两个顶点之间都存在边。
有向完全图:有向图中任意两个顶点之间都存在方向相反的两条弧。
有向树:一个顶点的入度为0,其余顶点的入度均为1的有向图,称为有向树。
48.3 图的存储结构
邻接矩阵法:
使用二维数组来表示图。数组的行和列分别对应图中的顶点,元素的值表示顶点之间是否存在边或弧。
在无向图中,如果两个顶点之间存在边,则对应位置的值为1(或其他非零值),否则为0。
在有向图中,如果两个顶点之间存在弧,则对应位置的值为1(或其他非零值),否则为0。弧的方向由行和列的顺序表示。
邻接表法(顺序+链式存储):
使用链表数组来表示图。数组的每个元素对应一个顶点,链表存储与该顶点相邻的顶点。
在无向图中,每个顶点的链表存储与该顶点直接相连的所有顶点。
在有向图中,每个顶点的链表存储从该顶点出发可以到达的所有顶点(即出度)。
十字链表法:
有向图的一种链式存储结构。在十字链表中,对应于有向图中的每条弧有一个结点,对应于每个顶点也有一个结点。
弧结点包含指向弧起点和终点的指针,以及指向下一条入边和出边的指针。
顶点结点包含指向该顶点的第一条入边和第一条出边的指针。
邻接多重表存储无向图:
无向图的链式存储结构。在邻接多重表中,每条边有一个结点,每个顶点也有一个结点。
边结点包含指向两个顶点的指针,以及指向下一条依附于这两个顶点的边的指针。
顶点结点包含指向第一条依附于该顶点的边的指针。
边集数组:
由两个一维数组构成。一个数组存储顶点信息,另一个数组存储边的信息。
边的信息包括边的起点、终点和权重(如果图带权)。
48.4 图的遍历:指从图的任意节点触发,沿着某条搜索路径对图中所有节点进行访问且只访问一次。
广度优先算法BFS:先访问一个顶点的所有邻接顶点,再依次访问器邻接顶点的所有邻接顶点,类似与层次遍历。
深度优先遍历DFS:先从任一顶点触发,遍历到底,直至返回,再选取任一其他结点触发,重复这个过程直到遍历完整个图。
48.5 若图中顶点数为n,则它的生成树含有 n-1 条边。最小生成树的边数 = 顶点数 –1。砍掉一条则不连通,增加一条边则会出现回路。
Prim算法(普里姆):从某一个顶点开始构建生成树;每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。适用于边稠密图。
从任一顶点触发,找出与其邻接的边权值最小的顶点加入树的集合,而后从新的树集合中找出集合中顶点与其邻接的边权值最小的顶点,同样加入树,递归直到图中所有顶点都加入树集合。
Kruskal 算法(克鲁斯卡尔):每次选择—条权值最小的边,使这条边的两头连通(原本已经连通的就不选)直到所有结点都连通。适合于边稀疏图。
依次取权值最小的边,直到囊括所有节点,但要注意选边后不能形成环路。即已有的结点不能再加入。
48.6 拓扑排序:
AOV,Activity On Vertex Network,即顶点活动网。一个工程常常会被分为多个小的子工程,这些子工程被称为活动,
在有向图中,若以顶点表示活动,有向边(也可以称为弧)表示活动之间的先后关系,这样的图简称为AOV网。
在工程的实施过程中,有些活动的开始是以它所有前序活动的结束为先决条件的,必须在其他有关活动完成之后才能开始;
有些活动没有先决条件,可以安排在任意时间开始。AOV网就是一种可以形象地反映出整个工程中各个活动之间的先后关系的有向图。
在顶点活动网(AOV,Acticity On Vertex Network)中,若不存在回路,则所有活动可排列成一个线性序列,使得每一个活动的所有前驱活动都排列在该活动的前面,我们把此序列叫做拓扑序列。
如:C1、C2的执行是没有先决条件的;C3必须要在C1、C2这两个活动都执行完毕之后才能够执行;C4必须要在C3执行完毕之后才可以执行。
则我们将这些活动的执行顺序输出为一个线性的排序:C1->C2->C3->C4,或者C2->C1->C3->C4。这种将AOV网中的活动进行排序就是拓扑排序。
可以看到,拓扑排序是可能会有多种答案的,并不是只有唯一的答案。
49.1 算法(Algorithm)是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。
算法是对特定问题求解步骤的一种描述,是指令的有限序列。程序 = 数据结构 + 算法; 算法必须是有穷的,而程序可以是无穷的。
49.2 算法的5个特性
有穷性:一个算法必须在有穷的时间内完成;即求解步骤有限,完成每一步的时间有限。
可行性:一个算法是可行的;即针对特定问题的求解,能在有限步和有限时间内完成。
确定性:在算法中的指令序列,每一个指令都有确定的含义,不存在二义性。同一个条件里执行多次得到的结果都会是相同的输出。
输入:一个算法可以有0~n个输入。
输出:一个算法一定有1~n个输出。
49.3 算法的时间复杂度和空间复杂度。
时间复杂度是指程序运行从开始到结束所需的时间,通常以数据集n为计量单位。O(1),O(log2n),O(n)等,从1遍历到n记为O(n).
空间复杂度指对一个算法在运行过程中临时占用存储空间大小的度量。如果从1遍历到n每次都要开辟空间则记为O(n)。
复杂度有上界(最坏情况:任意上输入规模的最大运行次数),下界(最好情况:任意输入规模的最小运行次数),平均情况:任意输入规模的期望运行次数。
紧致界:上界和下界之间的区域就是紧致界。
49.4 伪代码:介于自然语言和程序语言之间的,对算法实现的一种表现形式。
49.5 查找算法:
49.5.1 顺序查找:从待查找序列的第一个元素开始,依次将每个元素与目标值进行比较,直到找到匹配的元素或遍历完整个序列。如果遍历完整个序列仍未找到匹配元素,则查找失败。
是一种基本的查找技术,适用于无序和有序序列。时间复杂度: O(n)。空间复杂度: O(1)。
step1初始化:定义一个待查找的序列arr和一个目标值value。
step2遍历序列:从序列的第一个元素开始,逐个比较当前元素与目标值。
step3比较:如果当前元素与目标值相等,则返回该元素的位置;如果遍历完整个序列仍未找到匹配元素,则返回失败标志(通常是-1)。
49.5.2 折半查找:从数组的中间元素开始,通过比较目标值与中间元素的大小,决定是继续在左半部分查找还是右半部分查找,每次比较都将搜索范围缩小一半,直到找到目标元素或搜索范围为空。
是一种在有序数组中查找特定元素的算法。计算中间位置的索引mid=(low+high)/2。时间复杂度O(log2n)。空间复杂度: O(1)。
step1初始化:设置两个指针,low和high,分别指向数组的起始和结束位置。
step2比较:计算中间位置的索引mid = (low + high) / 2。如果a[mid] == m(m为目标元素),则查找成功。如果a[mid] < m,则调整low为mid + 1;如果a[mid] > m,则调整high为mid - 1。
step3重复:继续上述步骤,直到找到目标元素或low > high。
49.5.3 分块查找:是一种介于顺序查找和二分查找之间的查找方法,它通过将数据集合分成若干块并对每块建立索引表,然后通过索引表进行查找,最后在块内进行线性查找。
step1确定块的大小:根据数据量和机器性能合理分配块的大小。块越大,索引表中的元素越少,但块内部的查找效率会降低。
step2建立索引表:将数据集合按照固定块大小划分成多个块,并建立一个索引表。索引表的每个元素记录了对应块的最小值和最大值。
step3查找:首先在索引表中使用二分查找或顺序查找等方法确定待查找元素所在的块,然后在该块内部进行线性查找。如果某一块没有找到待查找元素,则返回不存在。
49.5.4 二叉查找树排序:二叉查找树是一种特殊的二叉树,其中每个节点的左子树中所有节点的值都小于该节点的值,右子树中所有节点的值都大于该节点的值。时间复杂度O(log2n)。
这种特性使得二叉查找树在查找、插入和删除操作中都能保持较高的效率。
查找:在二叉查找树中进行查找操作时,从根节点开始比较目标值与当前节点的值。
如果目标值等于当前节点的值,则查找成功;如果目标值小于当前节点的值,则继续在左子树中查找;如果目标值大于当前节点的值,则继续在右子树中查找。
如果遍历到空节点仍未找到目标值,则说明查找失败。
插入:当找到一个空位置时,将新节点插入该位置。具体步骤如下:从根节点开始比较要插入的值与当前节点的值。
如果要插入的值小于当前节点的值,则向左子树递归插入。
如果要插入的值大于当前节点的值,则向右子树递归插入。
当找到一个空位置时,将新值插入该位置。
删除:删除操作稍微复杂,分为三种情况:
删除叶子节点:直接删除该节点。
删除有一个子节的节点:将其子节点直接替代该节点。
删除有两个子节的节点:找到该节点的后继节点(右子树中最小的节点),用后继节点的值替代当前节点的值,然后删除后继节点。
49.5.5 平衡二叉树:
49.5.6 红黑树:
49.5.7 B树:
49.5.8 B+树:
49.5.9 哈希表:通过哈希函数将关键字映射到一个固定大小的数组(称为“桶”或“槽”)中。哈希函数将输入的关键字转换为一个固定的输出值(即哈希值),该值决定了数据在表中的存储位置。
通过这种方式,可以直接通过哈希值快速定位到数据的位置,从而加快查找速度。
哈希表的冲突解决机制:由于不同的关键字可能映射到同一个位置(称为冲突),哈希表通常采用以下几种方法解决冲突:
开放定址法:当发生冲突时,探测表中其他位置直到找到空位。
线性探测法:按物理地址顺序取下一个空闲的存储空间。
伪随机数法:将冲突的数据随机存入任意空闲地址中。
链地址法:在发生冲突的位置上使用链表存储所有具有相同索引的元素。
再哈希法:使用多个哈希函数,当第一个函数发生冲突时使用第二个函数。
49.6 排序算法:
直接插入排序:通过逐步比较和移动元素,将一个无序数组排序为有序数组。
step1从无序表中取出第一个元素,将其插入到有序表的合适位置;
step2然后,取出第二个元素,与前一个元素进行比较,找到合适的插入位置并插入;依次进行下去,直到整个无序表被排序完成。
希尔排序:通过选择一个增量序列,将数据分组进行插入排序。
step1首先,选择一个增量(gap),将数据分为多个子序列,每个子序列内的元素进行插入排序。
step2然后逐渐减小增量,直到增量为1,此时整个数据序列被视为一个整体进行一次插入排序。这个过程不断重复,直到所有元素有序。
简单(直接)选择排序:通过遍历数组,每次找出剩余元素中的最小(或最大)元素,并将其放到序列的起始位置。
step1:就是从第一个元素开始,遍历列表逐个比较,找到最小(或最大)的元素与第1位替换。
step2:然后再从第二位元素开始,遍历列表逐个比较,同样找到元素最小(或最大)的元素,与第二位替换,
step3:重复上述步骤,直到数组完全排序。
堆排序:堆是具有以下性质的完全二叉树,每个节点的值都大于或等于其左右孩子节点的值。
小根(顶)堆:每个结点的关键字都不大于其孩子结点的关键字。
大根(顶)堆:每个结点的关键字都不小于其孩子结点的关键字。把所有非终端结点都检查一遍,是否满足大根堆的要求,
如果不满足,则进行调整。若元素互换破坏了下一级的堆,则采用相同的方法继续往下调整(小元素不断“下坠”)。
step1:依据给出的待排序关键字建立初始堆;然后构造成一个大(小)顶堆;
step2:将堆顶元素与末尾元素进行交换,此时末尾就为最大(小)值;
step3:然后将剩余n-1个元素重新构造成一个大(小)顶堆,这样会得到n个元素的次大(小)值,将堆顶元素与末尾第二个元素进行交换。
如此反复执行,便能得到一个有序序列了。
冒泡排序:通过遍历数列,比较相邻元素的大小。如果顺序错误则交换它们的位置。每轮遍历后,未排序部分的最大元素会“浮”到已排序部分的顶端。
step1从数列的第一个元素开始遍历,每2个相邻元素比较,如果为逆序就交换位置,将最大的数冒泡到最后一位。
step2继续从数列的第一个元素开始遍历比较并执行冒泡(上次的最后1位元素不再参与比较)。
step3重复上述步骤,直到只剩第一个元素没冒泡为止。
快速排序:设置一个基准元素,基准为最左或最右元素。
step1:当基准为最左元素时,如果右侧比较元素低于基准则基准位置和右侧元素对换,且右侧位置变为基准元素。左侧比较元素位置+1.
若右侧比较元素高于基准,则基准元素位置不变,右侧比较元素位置-1;
step2:当基准为最右元素时,如果左侧比较元素高于基准则基准位置和右侧元素对换,且左侧位置变为基准元素。右侧比较元素位置-1.
若左侧比较元素低于基准,则基准元素位置不变,左侧比较元素位置+1;
step3:完成一次快速排序后,再对分出的两块区域按步骤1和2进行递归排序。直到所有的元素都排好顺序。
归并排序:指的是将两个已经排序的序列合并成一个序列的操作。先两两元素合并成为一组排序;再两两组合并为一组排序;递归一直到到合并成1个组。
基数排序:通过将整数按位数切割成不同的数字,然后按每位数字进行比较和排序。
外部排序:外部算法是指那些在数据量超过内存容量时,需要借助外部存储设备(如硬盘)进行数据排序的算法。外部排序算法主要包括置换平衡归并排序算法和置换选择排序算法等。
49.7 各排序算法的时间复杂度和空间复杂度;
直接插入排序:平均时间复杂度O(n2);最坏时间复杂度O(n2);空间复杂度O(1);稳定;插入第i个数据时,前面数据已经排好序了。
希尔插入排序:平均时间复杂度O(n1.3);最坏时间复杂度O(n2);空间复杂度O(1);不稳定;切分序列,进行直接插入排序。
直接选择排序:平均时间复杂度O(n2);最坏时间复杂度O(n2);空间复杂度O(1);不稳定;选择最小的换到前面去。
堆选择排序:平均时间复杂度O(nlog2n);最坏时间复杂度O(nlog2n);空间复杂度O(1);不稳定;利用堆排序。
冒泡交换排序:平均时间复杂度O(n2);最坏时间复杂度O(n2);空间复杂度O(1);稳定;逐个比较交换。
快速交换排序:平均时间复杂度O(nlog2n);最坏时间复杂度O(n2);空间复杂度O(log2n);不稳定;选定基准值,分为左右两部分,递归对子序列排序。
归并合并排序:平均时间复杂度O(nlog2n);最坏时间复杂度O(nlog2n);空间复杂度O(n);稳定;切分序列,逐个比较序列的值。
基数切割排序:平均时间复杂度O(r+n);最坏时间复杂度O(r+n);空间复杂度O(r+n);稳定;
49.8 算法按照设计方法分类:
分治法(Divide and Conquer,分而治之,分解、解决、合并):将问题分解为更小的子问题,分别解决后再合并结果。如递归(自己调用自己)、二分查找、快速排序、归并排序、青蛙爬楼梯问题。
动态规划(Dynamic Programming,全局最优):通过保存子问题的解来避免重复计算,从而提高效率。如求解斐波那契数列,最大子数组和。
贪心算法(Greedy Algorithms,局部最优):在每一步选择中都采取当前最优的选择,以期望得到全局最优解。如哈夫曼编码、最小生成树问题。
回溯法是(试探):一种通过深度优先搜索策略来解决问题的算法思想。通过不断尝试各种情况来寻找问题的解,当发现当前情况不满足求解条件时,会回退到上一步重新选择,
这种“走不通就退回再走”的技术就是回溯法。如求八皇后问题、装载问题、批量作业调度问题。