数据结构和算法

1. 数据结构基础知识

数据结构是指数据元素的集合及元素间的相互关系和构造方法,结构就是元素之间的关系。

  • 逻辑结构:元素之间的相互关系。按照逻辑关系的不同将数据结构分为线性结构和非线性结构,其中,线性结构包括线性表、栈、队列、串,非线性结构主要包括树和图。
  • 存储结构:也称物理结构,数据元素及元素之间关系的存储形式,主要有顺序存储和链式存储两种基本方式。

1.1 线性表

线性结构的特点是数据集合中的元素之间是一种线性关系,数据元素“一个接一个地排列”,也就是一个序列。

1)线性表基本概念

线性表是指一个序列,一个线性表是n个元素的有限序列(n≥0),通常表示为 ( a 1 , a 2 , . . . , a n ) (a_1,a_2,...,a_n) (a1,a2,...,an),其特点是在非空的线性表中:

  • 存在唯一的一个称作“第一个”的元素。
  • 存在唯一的一个称为“最后一个”的元素。
  • 除第一个元素外,序列中的每个元素均只有一个直接前驱。
  • 除最后一个元素外,序列中的每个元素均只有一个直接后继。

2)线性表的存储结构

常用的存储结构有顺序存储和链式存储,主要的基本操作是插入、删除和查找。

顺序存储是指用一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻。在这种存储方式下,元素间的逻辑关系无需占用额外的空间来存储。

  • 优点:可以随机存取表中的元素,按序号查找元素的速度很快。
  • 缺点:插入和删除操作需要移动元素,插入元素前要移动元素以挪出空的存储单元,然后再插入元素;删除元素时同样需要移动元素,以填充被删除的元素空出来的存储位置。

链式存储是用结点来存储数据元素,元素的结点地址可以连续,也可以不连续,因此,存储数据元素的同时必须存储元素之间的逻辑关系。另外,结点空间只有在需要的时候才申请,无须事先分配。

结点结构通常有数据域指针域组成。结点中的数据域用于存储数据元素的值,指针域则存储当前元素的直接前驱或直接后继元素的位置信息,指针域中所存储的信息称为指针(或链)。

根据结点中指针信息的实现方式,有单链表、双向链表、循环链表和静态链表等链表结构。

  • 单链表:也称线性链表,n个结点通过指针连成一个链表,结点中只有一个指针域。
  • 双向链表:每个结点包含两个指针,分别指明当前元素的直接前驱和直接后继信息,可在两个方向上遍历链表中的元素。
  • 循环链表:表尾结点的指针指向表中的第一个结点,可从表中任意结点开始遍历整个链表。
  • 静态链表:借助数组来描述线性表的链式存储结构。

线性表采用链表作为存储结构时,只能顺序地访问元素,而不能对元素进行随机存取,所需空间与结点个数成正比。但其优点是插入和删除操作不需要移动元素。 缺点是查找慢。

1.2 队列和栈

栈和队列的逻辑结构和线性表相同,有以下两个特点:

  • 栈按后进先出的规则进行修改。
  • 队列按先进先出的规则进行修改。
1.2.1 栈

1)栈的定义及基本运算

栈是只能通过访问它的一端来实现数据存储和检索的一种线性数据结构。栈又称为先进后出(FILO)或后进先出(LIFO)的线性表。在栈中进行插入和删除操作的一端称为栈顶(top),相应地,另一端称为栈底(bottom)。不含数据元素的栈称内空栈。

栈的基本运算如下:

(1)初始化栈initStack(S):创建一个空栈S。

(2)判栈空isEmpty(S):当栈S为空栈时返回“真”值,否则返回“假”值。

(3)入栈push(S,x):将元素x 加入栈顶,并更新栈顶指针。

(4)出栈pop(S):将栈顶元素从栈中删除,并更新栈顶指针。若需要得到栈顶元素的值,可将pop(S)定义为 一个函数,它返回栈顶元素的值。

(5)读栈项元素top(S):返回栈顶元素的值,但不修改栈顶指针。

2)栈的存储结构

栈的顺序存储。栈的顺序存储是指用一组地址连续的存储单元依次存储自栈顶到栈底的数据元素,同时附设指针 top 指示栈顶元素的位置。采用顺序存储结构的栈也称为顺序栈。在顺序存储方式下,需要预先定义或申请栈的存储空间,也就是说栈空间的容量是有限的。因此在顺序栈中,当一个元素入栈时,需要判断是否栈满(栈空间中没有空闲单元),若栈满,则元素入栈会发生上溢现象。

栈的链式存储。为了克服顺序存储的栈可能存在上溢的不足,可以用链表存储栈中的元素。用链表作为存储结构的栈也称为链栈。由于栈中元素的插入和删除仅在栈顶一端进行,因此不必设置头结点,链表的头指针就是栈顶指针。

栈的典型应用包括表达式求值、括号匹配等,在计算机语言的实现以及将递归过程转变为非递归过程的处理中,栈有重要的作用。

1.2.2 队列

1)队列的定义及基本运算

队列是一种先进先出(FIFO)的线性表,它只允许在表的一端插入元素,而在表的另一端删除元素。在队列中,允许插入元素的一端称为队尾(rear),允许删除元素的一端称为队头(front)。

队列的基本运算如下:

(1)初始化队列initQueue(Q):创建一个空的队列Q。

(2)判队空isEmpty(Q):当队列为空时返回“真”值,否则返回“假”值。

(3)入队enQueue(Q,x):将元素x加入到队列Q的队尾,并更新队尾指针。

(4)出队deQueue(Q):将队头元素从队列Q中删除,并更新队头指针。

(5)读队头元素frontQueue(Q):返回队头元素的值,但不更新队头指针。

2)队列的存储结构

队列的顺序存储。队列的顺序存储结构又称为顺序队列,它也是利用一组地址连续的存储单元存放队列中的元素。由于队中元素的插入和删除限定在表的两端进行,因此设置队头指针和队尾指针,分别指示出当前的队首元素和队尾元素。

在顺序队列中,为了简化运算,元素入队时,只修改队尾指针;元素出队时,只修改队头指针。当队尾指针达到其上限时,就不能只通过修改队尾指针来实现新元素的入队操作了,这时可以称之为循环队列

队列的链式存储。队列的链式存储也称为链队列。为了便于操作,可给链队列添加一个头结点,并令头指针指向头结点列为空的判定条件是头指针和尾指针的值相同,且均指向头结点。

队列常用于处理需要排队的场合,如操作系统中处理打印任务的打印队列、离散事件的计算机模拟等。

示例.元素按照a、b、c的次序进入栈,请尝试写出其所有可能的出栈序列。

答:先进后出原则有abc、acb、bac、bca、cba

1.3 串

广义表是n个表元素组成的有限序列,是线性表的推广。通常用递归的形式进行定义,记做: L S = ( a 0 , a 1 , . . . , a n ) LS=(a_0,a_1,...,a_n) LS=(a0,a1,...,an)

注:其中LS是表名, a i a_i ai是表元素,它可以是表(称做子表),也可以是数据元素(称为原子)。其中n是广义表的长度(也就是最外层包含的元素个数),n=0的广义表为空表;而递归定义的重数就是广义表的深度,直观地说,就是定义中所含括号的重数(原子的深度为0,空表的深度为1)。

示例.有广义表 LS1 = (a,(b,c),(d,e)),则其长度为多少,深度为多少。

答:长度为3,也就是去掉最外层括号逗号加1;深度为2,每个元素所拥有括号的最大个数。

1)串的定义及运算

字符串是一串文字及符号的简称,是一种特的线性表。字符串的基本数据元素是字符,计算机中非数值问题处理的对象经常是字符串数据,如在汇编和高级语言的编译程序中,源程序和目标程序都是字符串数据;在事务处理程序中,姓名、地址等一般也是作为字符串处理的。另外,串还具有自身的特性,常常把一个串作为一个整体来处理。

串是仅由字符构成的有限序列,是取值范围受限的线性表。一般记为 S = ′ a 1 a 2 . . . a n ′ S='a_1a_2...a_n' S=a1a2...an,其中S是串名,单引号括起来的字符序列是串值。

  • 串长:串的长度,指字符串中的字符个数。
  • 空串:长度为0的串,空串不包含任何字符。
  • 空格串:由一个或多个空格组成的串。虽然空格是一个空白符,但它也是一个字符, 计算串长度时要将其计算在内。
  • 子串:由串中任意长度的连续字符构成的序列称为子串。含有子串的串称为主串 。子串在主串中的位置指子串首次出现时,该子串的第一个字符在主串的位置。空串是任意串的子串。
  • 串相等:指两个串长度相等且对应位置上的字符也相同。
  • 串比较:两个串比较大小时以字符的ASCII码值作为依据。比较操作从两个串的第一个字符开始进行,字符的ASCII码值大者所在的串为大;若其中 一个串先结束,则以串长较大者为大。

大多数的程序语言在其开发资源包中都提供了字符串的赋值(拷贝)、连接、比较、求串长、求子串等基本运算,利用它们就可以实现关于串的其他运算。

2)串的存储结构

  • 顺序存储。该方式是用一组地址连续的存储单元来存储串值的字符序列。由于串中的元素为字符,所以可通过程序语言提供的字符数组定义串的存储空间(即存储空间的容量固定),也可以根据串长的需要动态申请字符串的空间 (即存储空间的容量可扩充或缩减)。
  • 链式存储。字符串也可以采用链表作为存储结构,当用链表存储串中的字符时,每个结点中可以存储 一个字符,也可以存储多个字符,需要考虑存储密度问题。

在链式存储结构中,结点大小的选择和顺序存储方法中数组空间大小的选择一样重要,它直接影响对串处理的效率。

1.4 数组和矩阵

1)数组

数组可看作是线性表的推广,其特点是多维数组的数据元素仍然是一个表。

一维数组是长度固定的线性表,数组中的每个数据元素类型相同。n 维数组是定长线性表在维数上的扩张,即线性表中的元素又是一个线性表。

如二维数组 A [ m ] [ n ] A[m][n] A[m][n],可以看成一个定长的线性表,其每个元素也是定长线性表。
A m ∗ n = [ a 11 a 12 ⋯ a 1 n − 1 a 1 n a 21 a 22 ⋯ a 2 n − 1 a 2 n ⋮ ⋮ ⋱ ⋮ ⋮ a m 1 a m 2 ⋯ a m n − 1 a m n ] A_{m*n} = \begin{bmatrix} a_{11} & a_{12} & \cdots & a_{1n-1} &a_{1n}\\ a_{21} & a_{22} & \cdots & a_{2n-1}&a_{2n}\\ \vdots & \vdots &\ddots & \vdots&\vdots\\ a_{m1} & a_{m2} & \cdots & a_{mn-1} &a_{mn}\\ \end{bmatrix} Amn= a11a21am1a12a22am2a1n1a2n1amn1a1na2namn
A A A看作一个行向量形式的线性表:
A m ∗ n = [ [ a 11 a 12 ⋯ a 1 n ] [ a 21 a 22 ⋯ a 2 n ] ⋯ [ a m 1 a m 2 ⋯ a m n ] ] A_{m*n} = [[a_{11}a_{12}\cdots a_{1n}][a_{21}a_{22}\cdots a_{2n}]\cdots [a_{m1}a_{m2}\cdots a_{mn}]] Amn=[[a11a12a1n][a21a22a2n][am1am2amn]]
也将 A A A看作一个列向量形式的线性表:
A m ∗ n = [ [ a 11 a 21 ⋯ a m 1 ] [ a 12 a 22 ⋯ a m 2 ] ⋯ [ a 1 n a 2 n ⋯ a m n ] ] A_{m*n} = [[a_{11}a_{21}\cdots a_{m1}][a_{12}a_{22}\cdots a_{m2}]\cdots [a_{1n}a_{2n}\cdots a_{mn}]] Amn=[[a11a21am1][a12a22am2][a1na2namn]]
数组结构的特点如下:

  • 数据元素数目固定。一旦定义了一个数组结构,就不再有元素的增减变化。
  • 数据元素具有相同的类型。
  • 数据元素的下标关系具有上下界的约束且下标有序。

数组通常做两种操作:

  • 取值操作。给定一组下标,读其对应的数据元素。
  • 赋值操作。给定一组下标,存储或修改与其相对应的数据元素。

几乎所有的程序设计语言都提供了数组类型。在语言中把数组看成是具有共同名字的同一类型多个变量的集合。但要注意不能对数组进行整体的运算,只能对单个数组元素进行运算。

数组一般不作插入和删除运算,也就是说,一旦定义了数组,则结构中的数据元素个数和元素之间的关系就不再发生变动,因此数组适合于采用顺序存储结构。一旦确定了它的维数和各维的长度,便可为它分配存储空间。从0开始,数组的存储地址计算如下:

数组存储地址计算

示例.已知5行5列的二维数组a起始地址为 a 0 a_0 a0,数组中各元素占两个字节,求元素a[2][3]按行优先存储的存储地址是多少?

答: a 0 a_0 a0 + (2*5+3)*2 = a 0 a_0 a0 + 26

2)矩阵

矩阵是很多科学与工程计算问题中研究的数学对象。

常见的特殊矩阵有对称矩阵、三角矩阵和对角矩阵等。对于特殊矩阵,由于其非零元的分布都有一定的规律,所以可将其压缩存储在一维数组中,并建立起每个非零元在矩阵中的位置与其在一维数组中的位置之间的对应关系。

若矩阵 A n × n A_{n\times n} An×n中的元素有 a i j = a j i ( 1 ≤ i , j ≤ n ) a_{ij} = a_{ji} (1 \leq i,j \leq n) aij=aji(1i,jn)的特点,则称为对称矩阵。

对角矩阵是指矩阵中的非零元素都集中在以主对角线为中心的带状区域中,即除了主对角线上和直接在对角线上、下方若干条对角线上的元素外,其余的矩阵元素都为零。

若非零元素的个数远远少于零元素的个数,且非零元素的分布没有规律, 则称之为稀疏矩阵。

稀疏矩阵的三元组表构成一个线性表,其顺序存储结构称为三元组顺序表,其链式存储结构称为十字链表。

1.4 树和图

1.4.1 树

1)树

树结构是一种非常重要的非线性结构,该结构中一个数据元素可以有两个或两个以上的直接后继元素,可以用来描述客观世界中广泛存在的层次关系。

树的定义是递归的,它表明了树本身的固有特性,也就是一棵树由若干棵子树构成,而子树又由更小的子树构成。从数据结构的逻辑关系角度来看,树中元素之间有明显的层次关系。

树
常见的层级关系如下:

  • 双亲、孩子和兄弟:结点的子树的根称为该结点的孩子,相应地,该结点称为其子结点的双亲。具有相同双亲的结点互为兄弟。如A是树根,B、C、D是A的孩子结点,B、C、D互为兄弟;B是E、F的双亲,E、F是兄弟。
  • 结点的度:一个结点的子树的个数记为该结点的度。如A的度为3,B的度为2,C的度为0,D的度为1。
  • 叶子结点:也称终端结点,指度为0的结点。如E、F、C、G都是叶子结点。
  • 内部结点:度不为零的结点称为分支结点或非终端结点。除根结点之外,分支结点也称为内部结点。如B、D都是内部结点。
  • 结点的层次:根为第一层,根的孩子为第二层,以此类推。如A在第1层,B、C、D在第2层,E、F、G在第3层。
  • 树的高度:一棵树的最大层次数记为树的高度(或深度)。如上图树的高度为3。
  • 有序(无序)树:若将树中结点的各子树看成是从左到右具有次序的,即不能交换, 则称该树为有序树,否则称为无序树。
  • 森林:m(m≥0)棵互不相交的树的集合。

2)二叉树

二叉树是n(n ≥0)个结点的有限集合,它或者是空树(n =0),或者是由一个根结点及两棵不相交的、分别称为左子树和右子树的树所组成。

树和二叉树的主要区别是:

  • 二叉树中结点的子树要区分左子树和右子树,即使在结点只有一棵子树的情况下也要明确指出该子树是左子树还是右子树,树中则不区分。
  • 二叉树中结点的最大度为 2,而树中不限制结点的度数。

二叉树的重要特性:

  1. 二叉树第i层(i ≥1)上最多有 2 i − 1 2^{i-1} 2i1个结点。
  2. 深度为k的二叉树至多有 2 k − 1 2^{k-1} 2k1个结点(K≥1)。
  3. 对任何一棵二叉树,若其叶子结点数为 n 0 n_0 n0,度为2的结点数为 n 2 n_2 n2,则 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1
  4. 具有n个结点的完全二叉树的深度为 l o g 2 n + 1 log_2n + 1 log2n+1,向下取整。

满二叉树:深度为k的二叉树有 2 k − 1 2^{k-1} 2k1个结点。
完全二叉树:深度为k、有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号为1~n的结点一一对应。

二叉树的顺序存储结构,特点如下:

  • 如果i=1,则结点i无父结点,是二叉树的根,如果i>1,则父节点是i/2向下取整;
  • 如果2i>n,则结点i为叶子结点,无左子结点,否则,其左子结点是结点2i;
  • 如果2i+1>n,则结点i无右叶子结点,否则,其右子结点是节点2i+1;

二叉树的遍历,遍历是按某种策略访问树中的每个结点,且仅访问一次。

由于二叉树所具有的递归性质,一棵非空的二叉树可以看作是由根结点、左子树和右子树三部分构成,因此若能依次遍历这三个部分的信息,也就遍历了整棵二叉树。按照遍历左子树要在遍历右子树之前进行的约定,依据访问根结点位置的不同,可得到二叉树的前序、中序和后序三种遍历方法,另外自上而下、自左至右逐层访问树中各层结点的过程就是层次遍历。

  • 前序遍历:根结点->左子树->右子树。
  • 中序遍历:左子树->根结点->右子树。
  • 后序遍历:左子树->右子树->根结点。
  • 层次遍历:自上而下、自左至右逐层访问树中各层结点。

最优二叉树又称为哈夫曼树,是一类带权路径长度最短的树。从树中一个结点到另一个结点之间的通路称为结点间的路径,该通路上分支数目称为路径长度。树的路径长度是从树根到每一个叶子之间的路径长度之和。结点的带权路径长度为从该结点到树根之间的路径长度与该结点权的乘积。

二叉查找树又称为二叉排序树,它或者是一棵空树,或者是具有如下性质的二叉树。

  1. 若它的左子树非空,则左子树上所有结点的关键码值均小于根结点的关键码值。
  2. 若它的右子树非空,则右子树上所有结点的关键码值均大于根结点的关键码值;
  3. 左、右子树本身就是两棵二叉查找树。

对二叉查找树进行中序遍历,可得到一个关键码递增有序的结点序列。

1.4.2 图

1)图的定义和术语

图是比树结构更复杂的一种数据结构,任意两个结点之间都可能有直接的关系,所以图中一个结点的前驱和后继的数目是没有限制的。图结构被用于描述各种复杂的数据对象,在自然科学、社会科学和人文科学等许多领域有非常广泛的应用。

图的相关术语如下:

  • 有向图:每条边都是有方向的。
  • 无向图:每条边都是无方向的。
  • 完全图:一个无向图具有n个顶点,而每一个项点与其他n-1个项点之间都有边。显然,含有n个顶点的无向完全图共有n(n-1)/2条边。类似地,有n个项点的有向完全图中弧的数目为n(n-1),即任意两个不同顶点之间都存在方向相反的两条弧。
  • 度、出度和入度:度是指关联于该顶点的边的数目;顶点的入度是以该项点为终点的有向边的数目,而项点的出度指以该顶点为起点的有向边的数目。若为有向图,顶点的度表示该顶点的入度和出度之和。
  • 路径:路径长度是路径上边或弧的数目。第一个顶点和最后一个项点相同的路径称为回路或环。若一条路径上除了开始顶点和结束顶点可以相同外,其余顶点均不相同,这种路径称为一条简单路径。
  • 子图:两个图,若一个图被包含于另一个图。
  • 连通图:无向图中任意两个顶点都是连通的。
  • 强连通图:有向图中,两个顶点互通都有路径。
  • 网:边(或弧)具有权值的图称为网。

2)图的存储结构

常用的存储结构有邻接矩阵和邻接表。

邻接矩阵表示法是用一个n阶方阵R来存放图中的各结点的关联信息,其矩阵元素 R i j R_{ij} Rij定义为:
R i j = { 1 若顶点 i 到顶点 j 有邻接边 0 若顶点 i 到顶点 j 无邻接边 R_{ij}=\begin{cases} 1 若顶点i到顶点j有邻接边\\ 0 若顶点i到顶点j无邻接边 \end{cases} Rij={1若顶点i到顶点j有邻接边0若顶点i到顶点j无邻接边

示例

缺点是有许多为0的,造成浪费。

邻接链表表示法是首先把每个顶点的邻接顶点用链表示出来,然后用一个一维数组来顺序存储上面每个链表的头指针。

邻接链表

优点是容易找度和关系,存放空间减少了。

3)图的遍历

遍历

深度优先遍历(DFS),类似于树的前序遍历。根据上图遍历示例有V1,V2,V4,V8,V5,V3,V6,V7

  1. 首先访问出发顶点V;
  2. 依次从V出发搜索V的任意一个邻接点W;
  3. 若W未访问过,则从该点出发继续深度优先遍历。

广度优先,类似于树的层序遍历。根据上图遍历示例有V1,V2,V3,V4,V5,V6,V7,V8

  1. 首先访问出发顶点V
  2. 然后访问与顶点V邻接的全部未访问顶点W、X、Y…;
  3. 然后再一次访问W、X、Y…邻接的未访问的顶点。

2. 算法基础知识

2.1 基础概念

算法是问题求解过程的精确描述,它为解决某一特定类型的问题规定了一个运算过程,并且具有下列特性:

  1. 有穷行。一个算法必须在执行有穷步骤之后结束,且每一步都可在有穷时间内完成。
  2. 确定性。算法中每一条指令都必须有确切的含义,不能含糊不清。
  3. 输入。(>=0) 一个算法有零个或多个输入。
  4. 输出。(>=1) 一个算法有一个或多个输出,与输入有特定关系的量。
  5. 有效性(可行性)。算法的每个步骤都能有效执行并能在执行有限次后得到确定的结果。例如a=0,b/a就无效。

常用的算法描述方法有流程图、N/S盒图、伪代码和决策表等。

1)流程图

流程图(flow chart)即程序框图,是历史最久、流行最广的一种算法的图形表示方法。每个算法都可由若干张流程图描述。流程图给出了算法中所进行的操作以及这些操作执行的逻辑顺序。

程序流程图包括三种基本成分:加工步骤,用方框表示;逻辑条件,用菱形表示:控制流,用箭头表示。常用的几种符号如下:

流程图符号

示例. 求正整数m和n的最大公约数,流程图如下:

算法流程图

2)N/S 盒图

盒图是结构化程序设计出现之后,为支持这种设计方法而产生的一种描述工具。N/S 盒图的基本元素与控制结构如下:

N/S 盒图的基本元素与控制结构

在N/S图中,每个处理步骤用一个盒子表示,盒子可以嵌套。对于每个盒子,只能从上面进入,从下面走出,除此之外别无其他出入口,所以盒图限制了随意的控制转移,保证了程序的良好结构。

示例. 求正整数m和n的最大公约数,盒图如下:

盒图求最大公约数

3)伪代码

伪代码是一种算法描述语言,介于自然语言与编程语言之间,不用拘泥于具体的实现。

示例. 输入3个数,打印输出其中最大的数。

begin (算法开始)
      输入 a,b,c
      if a>b 则 a->max
        否则 b->max
      if c>max 则c->max
        print max
end (算法结束)

4)决策表

决策表是一种图形工具,它将比较复杂的决策问题简洁、明确、一目了然地描述出来 。

示例. 如果订购金额超过500元,以前没有欠账,则发出批准单和提货单;如果订购金额超过500元,但以前的欠账尚未还清,则发不予批准的通知;如果订购金额低于500元,则不论以前的欠账是否还清都发批准单和提货单,在欠账未还清的情况下还要发出“催款单”。处理该问题的决策表如下:

决策表

2.2 排序

1)内部排序

常见的内部排序有如下:

  • 直接插入排序:具体做法是在插入第i个记录时, R 1 , R 2 , ⋯   , R i − 1 R_1,R_2,\cdots,R_{i-1} R1,R2,,Ri1已经排好序,这时将记录 R i R_i Ri的关键字 k i k_i ki依次与关键字 k i − 1 , k i − 2 , ⋯   , k 1 k_{i-1},k_{i-2},\cdots,k_1 ki1,ki2,,k1进行比较,从而找到 R i R_i Ri应该插入的位置,插入位置及其后的记录依次向后移动。直接插入排序是一种稳定的排序方法,其时间自由度为 O ( n 2 ) O(n^2) O(n2)
void insertSort(int datall, int n )
/*将数组data[0]~data[n-1]中的n个整数按非递减有序的方式进行排列*/ 
{int i, j;
 int tmp; 
 for (i=1;i<n;i++){
    if (data[i]<data[i-1]){
        tmp = data[i];data[i] = data[i-1];
        for (j = i-1; j >= 0 && data[j] > tmp; j--) data[j+1] = data[j]; 
        data[j+1] = tmp;
    }/*if*/ 
  }/*for*/
}/*insertSort*/
  • 冒泡排序:n个记录,首先将第一个记录的关键字和第二个记录的关键字进行比较,若为逆序,则交换两个记录的值,然后比较第二个记录和第三个记录的关键字,以此类推,直至第n-1个记录和第n个记录的关键字比较完为止。冒泡排序是一种稳定的排序方法,其时间自由度为 O ( n 2 ) O(n^2) O(n2)
void bubblesort(int datall, int n)
/*将数组data[0]~data[n-1]中的n个整数按非递减有序的方式进行排列*/
{int i,j,tag; /*用tag表示排序过程中是否交换过元素值*/ 
 int tmp;
 for(i= 1,tag= 1;tag == 1 && i < n;i++){ 
     tag = 0;
     for(j=0;j < n-1;j++)
         if (data[j]>data[j+1]){
             tmp = data[j]; data[j] = data[j+1]; data[j+1] = tmp;
             tag = 1; 
         }/*if*1
  }/*for*/
}/*bubblesort*/
  • 简单选择排序:n个记录,通过n-i次关键字之间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1≤i≤n)个记录进行交换,当i等于n时所有记录有序排列。简单选择排序是一种不稳定的排序方法,其时间自由度为 O ( n 2 ) O(n^2) O(n2)
void selectsort(int datall, int n)
/*将数组data[0]~data[n-1]中的n个整数按非递减有序的方式进行排列*/ 
{int i,j,k;
 int tmp;
 for(i=1;i < n;i++){
     k= i;
     for(j = i+1;j <= n;j++) /*找出最小元素的下标,用k表示*/ 
         if (data[j]< data[k]) k=j;
     if (k != i ) {
         tmp = data[i]; data[i] = data[k]; data[k] = tmp; 
     }/*if*/
  }/*for*/
}/*selectSort*/
  • 希尔排序:又称“缩小增量排序”,是对直接插入排序方法的改进。先将整个待排记录序列分割成若干子序列,然后分别进行直接插入排序,待整个序列中的记录基本有序时,再对全体记录进行一次直接插入排序。
  • 快速排序:通过一趟排序将待排的记录划分为独立的两部分,称为前半区和后半区,其中,前半区中记录的关键字均不大于后半区记录的关键字,然后再分别对这两部分记录继续进行快速排序,从而使整个序列有序。不稳定排序,时间复杂度为 O ( n 2 ) O(n^2) O(n2)
  • 堆排序:对一组待排序记录的关键字,首先把它们按堆的定义排成一个序列(即建立初始堆),从而输出堆顶的最小关键字(对于小顶堆而言)。然后将剩余的关键字再调整成新堆,便得到次小的关键字,如此反复,直到全部关键字排成有序序列为止。不稳定排序,时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
  • 归并排序:将两个或两个以上的有序文件合并成为一个新的有序文件。

image

2)外部排序

外部排序就是对大型文件的排序,待排序的记录存放在外存。在排序的过程中,内存只存储文件的一部分记录,整个排序过程需要进行多次内外存间的数据交换。常用的外部排序方法是归并排序,一般分为两个阶段:在第一阶段,把文件中的记录分段读入内存,利用某种内部排序方法对这段记录进行排序并输出到外存的另一个文件中,在新文件中形成许多有序的记录段,称为归并段;在第二阶段,对第一阶段形成的归并段用某种归并方法进行一趟趟地归并,使文件的有序段逐渐加长,直到将整个文件归并为一个有序段时为止。

2.3 查找

查找是非数值数据处理中一种常用的基本运算,查找运算的效率与查找表所采用的数据结构和查找方法密切相关。

五大查找如下:

  • 顺序表查找:顺序查找、二分查找、索引顺序查找
  • 树表查找:二叉查找树(排序)
  • 散列表查找:哈希查找

1)顺序查找

从表中的一端开始,逐个进行记录的关键字和给定值的比较,若找到一个记录的关键字与给定值相等,则查找成功;若整个表中的记录均比较过,仍未找到关键字等于给定值的记录,则查找失败。适用于顺序存储和链式存储方式的查找表。

等概率情况下,顺序查找成功的平均查找长度为(n+1)/2。即成功查找的平均比较次数约为表长的一半,若所查记录不在表中,则至少进行n次比较才能确定失败。

缺点查找效率低,优点是算法简单且适应面广,对查找表的结构没有要求,无论记录是否按关键字有序排序均可应用

2)二分查找

基本思想是:先令查找表中间位置记录的关键字和给定值比较,若相等,则查找成功;若不等,则缩小范围,直至新的查找区间中间位置记录的关键字等于给定值或者查找区间没有元素时 (表明查找不成功)为止。中间位置为mid=[(low+high)/2]向下取整;

折半查找比顺序查找的效率要高,但它要求查找表进行顺序存储并且按关键字有序排列。因此,折半查找适用于表不易变动,且又经常进行查找的情况。

3)索引顺序查找

索引顺序查找又称分块查找,是对顺序查找方法的一种改进。

在分块查找过程中,首先将表分成若干块,每一块中关键字不一定有序,但块之间是有序的,即后一块中所有记录的关键字均大于前一个块中最大的关键字。此外,还建立了一个“索引表”,索引表按关键字有序。

查找过程分为两步:第一步在索引表中确定待查记录所在的块;第二步在块内顺序查找。

查找效率较顺序查找要好得多,但远不及二分查找。

4)二叉查找树(排序)

基本思想是先对待查找的数据进行生成树,确保树的左分支的值小于右分支的值,然后再去和每个结点比较大小,查找最合适的范围。这个算法的查找效率很高,但是如果使用这种查找方法要首先创建树。

二叉排序树的定义:二叉排序树或者是一颗空树;或者是具有如下特性的二叉树:

  • 若它的左子树不空,则左子树上所有结点的值均小于根结点的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于根结点的值;
  • 它的左、右子树也都分别是二叉排序树。

二叉查找树性质:对二叉查找树进行中序遍历,即可得到有序的序列

二叉平衡树,又称AVL树,或者是一颗空树,或者是具有下列性质的二叉树,它的左子树或右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1。

平衡因子,二叉树上任一结点的左子树深度减去右子树深度的差值,称为此结点的平衡因子。

哈夫曼树是给定N个权值作为N个叶子结点,构造一颗二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称哈夫曼树。哈弗慢树是带权路径长度最短的树,权值较大的结点离根较近。

一般可以按下面步骤构造:

  1. 将所有左、右子树都为空的作为根结点。
  2. 在森林中选取两颗根结点的权值最小的树作为一颗新树的左、右子树,且置新树的附加根结点的权值为其左、右子树上根结点的权值之和。
  3. 从森林中删除这两颗树,同时把新树加入到森林中。
  4. 重复2,3步骤,直到森林中只有一颗树为止,此树就是哈夫曼树。

应用场景是对字符集中的字符进行编码和译码

B树,可以写B-树或B_树,其中-或者_只是连字符,一颗m阶的B树是一颗平衡的多路搜索树,它或者是空树,或者是满足下列性质的树:

  • 树中每个结点最多有m课子树;
  • 若根结点不是叶子结点,则最少有两颗子树;
  • 除根之外的所有非终端结点至少有[m/2]向上取整颗子树;
  • 所有非终端结点中包含下列信息
    ( n , A 0 , k 1 , A 1 , K 1 , , A 2 , ⋯   , K n , A n ) (n,A_0,k_1,A_1,K_1,,A_2,\cdots,K_n,A_n) (n,A0,k1,A1,K1,,A2,,Kn,An)
    其中, K i ( i = 1 , 2 , ⋯   , n ) K_i(i=1,2,\cdots,n) Ki(i=1,2,,n)为关键字,且 K i < K i + 1 ( i = 1 , ⋯   , n − 1 ) K_i < K_{i+1}(i=1,\cdots,n-1) Ki<Ki+1(i=1,,n1) A i ( i = 0 , ⋯   , n ) A_i(i=0,\cdots,n) Ai(i=0,,n), A n A_n An所指子树中所有结点的关键字均大于 K n K_n Kn,n为结点中关键字的个数且满足[m/2]-1 ≤ \leq n ≤ \leq m-1。
  • 所有的叶子结点都出现在同一层次上,并且不带信息(可以看作是外部结点或查找失败的结点,实际上这些结点不存在,指向这些结点的指针为空)。

B+树是B-树的一种变形,一个m阶的B+树具有如下特性:

  1. 每个叶子结点中含有n个关键字和n个指向记录的指针;并且,所有叶子结点彼此相链接构成一个有序链表,其头指针指向含最小关键字的结点;
  2. 每个非叶子结点中的关键字 K i K_i Ki即为其相应指针$A_i$所指子树中关键字的最大值
  3. 所有叶子结点都处于同一层次上,每个叶子结点中关键字的个数均介于m/2向上取整和m之间。

5)哈希查找

散列表构造的基本思想是:已知关键字集合U,最大关键字为m,设计一个函数Hash,它以关键字为自变量,关键字的存储地址为因变量,将关键字映射到一个有限的、地址连续的区间 T 0... n − 1 ( n < m ) T_{0...n-1}(n<m) T0...n1(n<m)中,这个区间就称为散列表,散列查找中使用的转换函数称为散列函数。

对于某个哈希函数Hash和两个关键字 K 1 K_1 K1 K 2 K_2 K2,如果 K 1 ≠ K 2 K_1\neq K_2 K1=K2,而Hash( K 1 K_1 K1)=Hash( K 2 K_2 K2),则称为出现了冲突,对该哈希函数来说 K 1 K_1 K1 K 2 K_2 K2,则称为同义词。

一般情况下,只能尽可能地减少冲突而不能完全避免,所以在建造哈希表时不仅要设定一个“好”的哈希函数,而且要设定一种处理冲突的方法。

处理冲突就就是为出现冲突的关键字找到另一个“空”的哈希地址。常见的处理冲突的方法有开放定址法、链地址法(拉链法)、再哈希法、建立公共溢出区法等。

开发定址法:
H i = ( H a s h ( k e y ) + d i ) % m i = 1 , 2 , ⋯   , k ( k ≤ m − 1 ) H_i = (Hash(key)+d_i) \% m \quad i=1,2,\cdots,k (k \leq m-1) Hi=(Hash(key)+di)%mi=1,2,,k(km1)
其中,Hash(key)为哈希函数;m为哈希表的表长; d i d_i di为增量序列。
常见的增量序列有:

  • d i = 1 , 2 , 3 , ⋯   , m − 1 d_i=1,2,3,\cdots,m-1 di=1,2,3,,m1,称为线性探测再散列。
  • d i = 1 2 , − 1 2 , 2 2 , − 2 2 , 3 2 , ⋯   , ± k 2 ( k ≤ m − 1 ) d_i=1^2,-1^2,2^2,-2^2,3^2,\cdots,\pm k^2(k\leq m-1) di=12,12,22,22,32,,±k2(km1),称为二次探测再散列。
  • d i d_i di=伪随机序列,称为随机探测再散列。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

有请小发菜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值