《大话数据结构》的笔记(作者: 程杰)

时间:2022-08-13 11:25:24
《大话数据结构》的笔记(作者: 程杰)

第1章 数据结构绪论2015-08-11数据结构:
是相互之间存在一种或多种特定关系的数据元素的集合。
1.3 数据结构起源2015-08-12程序设计=数据结构+算法1.4.5 数据结构2015-08-12数据结构:是相互之间存在一种或多种特定关系的数据元素的集合。1.5.1 逻辑结构2015-08-12按照视点的不同,我们把数据结构分为逻辑结构和物理结构。2015-08-12逻辑结构:是指数据对象中数据元素之间的相互关系。其实这也是我们今后最需要关注的问题。逻辑结构分为以下四种:2015-08-12集合结构:集合结构中的数据元素除了同属于一个集合外,它们之间没有其他关系。各个数据元素是“平等”的,它们的共同属性是“同属于一个集合”。数据结构中的集合关系就类似于数学中的集合2015-08-12线性结构:线性结构中的数据元素之间是一对一的关系2015-08-12树形结构:树形结构中的数据元素之间存在一种一对多的层次关系2015-08-12图形结构:图形结构的数据元素是多对多的关系1.5.2 物理结构2015-08-12物理结构:是指数据的逻辑结构在计算机中的存储形式。2015-08-12物理结构:是指数据的逻辑结构在计算机中的存储形式。2015-08-12顺序存储结构:是把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的2015-08-12链式存储结构:是把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。数据元素的存储关系并不能反映其逻辑关系,因此需要用一个指针存放数据元素的地址,这样通过地址就可以找到相关联数据元素的位置2015-08-12逻辑结构是面向问题的,而物理结构就是面向计算机的,其基本的目标就是将数据及其逻辑关系存储到计算机的内存中。1.6.1 数据类型2015-08-12数据类型:是指一组性质相同的值的集合及定义在此集合上的一些操作的总称。2015-08-12在C语言中,按照取值的不同,数据类型可以分为两类:
原子类型:是不可以再分解的基本类型,包括整型、实型、字符型等。结构类型:由若干个类型组合而成,是可以再分解的。例如,整型数组是由若干整型数据组成的。
1.6.2 抽象数据类型2015-08-12抽象数据类型体现了程序设计中问题分解、抽象和信息隐藏的特性。抽象数据类型把实际生活中的问题分解为多个规模小且容易处理的问题,然后建立一个计算机能处理的数据模型,并把每个功能模块的实现细节作为一个独立的单元,从而使具体实现过程隐藏起来。第2章 算法2015-08-12算法:
算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。
2.5.2 有穷性2015-08-12算法具有五个基本特性:输入、输出、有穷性、确定性和可行性。2015-08-12有穷性:指算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成。现实中经常会写出死循环的代码,这就是不满足有穷性2.5.4 可行性2015-08-12确定性:算法的每一步骤都具有确定的含义,不会出现二义性。算法在一定条件下,只有一条执行路径,相同的输入只能有唯一的输出结果。算法的每个步骤被精确定义而无歧义。2015-08-12可行性:算法的每一步都必须是可行的,也就是说,每一步都能够通过执行有限次数完成。可行性意味着算法可以转换为程序上机运行,并得到正确的结果2.6.1 正确性2015-08-13正确性:算法的正确性是指算法至少应该具有输入、输出和加工处理无歧义性、能正确反映问题的需求、能够得到问题的正确答案。
但是算法的“正确”通常在用法上有很大的差别,大体分为以下四个层次。 1.算法程序没有语法错误。 2.算法程序对于合法的输入数据能够产生满足要求的输出结果。 3.算法程序对于非法的输入数据能够得出满足规格说明的结果。 4.算法程序对于精心选择的,甚至刁难的测试数据都有满足要求的输出结果。
2.6.2 可读性2015-08-13、理解和交流。
可读性高有助于人们理解算法,晦涩难懂的算法往往隐含错误,不易被发现,并且难于调试和修改。
2.6.4 时间效率高和存储量低2015-08-13我们写代码的目的,一方面是为了让计算机执行,但还有一个重要的目的是为了便于他人阅读,让人理解和交流,自己将来也可能阅读,如果可读性不好,时间长了自己都不知道写了些什么。可读性是算法(也包括实现它的代码)好坏很重要的标志。2015-08-13健壮性:当输入数据不合法时,算法也能做出相关处理,而不是产生异常或莫名其妙的结果。2015-08-13时间效率指的是算法的执行时间,对于同一个问题,如果有多个算法能够解决,执行时间短的算法效率高,执行时间长的效率低。存储量需求指的是算法在执行过程中需要的最大存储空间,主要指算法程序运行时所占用的内存或外部硬盘存储空间。设计算法应该尽量满足时间效率高和存储量低的需求2.7.2 事前分析估算方法2015-08-17一个用高级程序语言编写的程序在计算机上运行时所消耗的时间取决于下列因素: 1.算法采用的策略、方法。 2.编译产生的代码质量。 3.问题的输入规模。 4.机器执行指令的速度。2.9.1 算法时间复杂度定义2015-08-17在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n)=O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。
这样用大写O( )来体现算法时间复杂度的记法,我们称之为大O记法。
2.10 常见的时间复杂度2015-08-17常用的时间复杂度所耗费的时间从小到大依次是:
O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
3.2 线性表的定义2015-08-17线性表(List):零个或多个数据元素的有限序列。2015-08-17首先它是一个序列。也就是说,元素之间是有顺序的,若元素存在多个,则第一个元素无前驱,最后一个元素无后继,其他每个元素都有且只有一个前驱和后继。2015-08-17然后,线性表强调是有限的3.5.2 插入操作2015-08-17插入算法的思路:
如果插入位置不合理,抛出异常;如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;将要插入元素填入位置i处; ?表长加1。
3.6.2 线性表链式存储结构定义2015-08-18线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。这就意味着,这些数据元素可以存在内存未被占用的任意位置第4章 栈与队列2015-08-18栈与队列:
栈是限定仅在表尾进行插入和删除操作的线性表。队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表。
4.2.1 栈的定义2015-08-18我们把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的栈称为空栈。栈又称为后进先出(LastIn First Out)的线性表,简称LIFO结构。4.8.2 递归定义2015-08-18在高级语言中,调用自己和其他函数并没有本质的不同。我们把一个直接调用自己或通过一系列的调用语句间接地调用自己的函数,称做递归函数。2015-08-18迭代和递归的区别是:迭代使用的是循环结构,递归使用的是选择结构。递归能使程序的结构更清晰、更简洁、更容易让人理解,从而减少读懂代码的时间。但是大量的递归调用会建立函数的副本,会耗费大量的时间和内存。迭代则不需要反复调用函数和占用额外的内存。因此我们应该视不同情况选择不同的代码实现方式。4.9.2 后缀表达式计算结果2015-08-20我们先来看看,对于“9+(3-1)×3+10÷2”,如果要用后缀表示法应该是什么样子:“9 3 1-3*+102/+”,这样的表达式称为后缀表达式,叫后缀的原因在于所有的符号都是在要运算数字的后面出现。显然,这里没有了括号。对于从来没有接触过后缀表达式的同学来讲,这样的表述是很难受的2015-08-20后缀表达式:9 3 1-3*+10 2/+
规则:从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果。
4.9.3 中缀表达式转后缀表达式2015-08-20中缀表达式“9+(3-1)×3+10÷2”转化为后缀表达式“9 3 1-3*+10 2/+”。
规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是右括号或优先级不高于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。
2015-08-20要想让计算机具有处理我们通常的标准(中缀)表达式的能力,最重要的就是两步: 1.将中缀表达式转化为后缀表达式(栈用来进出运算的符号)。 2.将后缀表达式进行运算得出结果(栈用来进出运算的数字)。4.10 队列的定义2015-08-25队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。
4.13.2 队列的链式存储结构——出队操作2015-08-25总的来说,在可以确定队列长度最大值的情况下,建议用循环队列,如果你无法预估队列的长度时,则用链队列。4.14 总结回顾2015-08-25对于队列来说,为了避免数组插入和删除时需要移动数据,于是就引入了循环队列,使得队头和队尾可以在数组中循环变化。解决了移动数据的时间损耗,使得本来插入和删除是O(n)的时间复杂度变成了O(1)。5.1 开场白2015-08-26枯眼望遥山隔水,往来曾见几心知?壶空怕酌一杯酒,笔下难成和韵诗。途路阻人离别久,讯音无雁寄回迟。孤灯夜守长寥寂,夫忆妻兮父忆儿。2015-08-26儿忆父兮妻忆夫,寂寥长守夜灯孤。迟回寄雁无音讯,久别离人阻路途。诗韵和成难下笔,酒杯一酌怕空壶。知心几见曾来往,水隔山遥望眼枯。2015-08-26这种诗体叫做回文诗。它是一种可以倒读或反复回旋阅读的诗体。刚才这首就是正读是丈夫思念妻子,倒读是妻子思念丈夫的古诗。5.3 串的比较2015-08-26计算机中的常用字符是使用标准的ASCII编码,更准确一点,由7位二进制数表示一个字符,总共可以表示128个字符。后来发现一些特殊符号的出现,128个不够用,于是扩展ASCII码由8位二进制数表示一个字符,总共可以表示256个字符,这已经足够满足以英语为主的语言和特殊符号进行输入、存储、输出等操作的字符需要了。可是,单我们国家就有除汉族外的满、回、藏、蒙古、*等多个少数民族文字,换作全世界估计要有成百上千种语言与文字,显然这256个字符是不够的,因此后来就有了Unicode编码,比较常用的是由16位的二进制数表示一个字符,这样总共就可以表示2
16个字符,约是6.5万多个字符,足够表示世界上所有语言的所有字符了。当然,为了和ASCII码兼容,Unicode的前256个字符与ASCII码完全相同。
5.7.5 nextval数组值推导2015-09-15KMP算法6.2 树的定义2015-09-16树(Tree)是n(n≥0)个结点的有限集。n=0时称为空树。在任意一棵非空树中:(1)有且仅有一个特定的称为根(Root)的结点;(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、……、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)6.2.2 结点间关系2015-09-16结点的子树的根称为该结点的孩子(Child),相应地,该结点称为孩子的双亲(Parent)。嗯,为什么不是父或母,叫双亲呢?呵呵,对于结点来说其父母同体,唯一的一个,所以只能把它称为双亲了。同一个双亲的孩子之间互称兄弟(Sibling)。结点的祖先是从根到该结点所经分支上的所有结点。所以对于H来说,D、B、A都是它的祖先。反之,以某结点为根的子树中的任一结点都称为该结点的子孙6.2.3 树的其他相关概念2015-09-16结点的层次(Level)从根开始定义起,根为第一层,根的孩子为第二层。若某结点在第l层,则其子树就在第l+1层。其双亲在同一层的结点互为堂兄弟。2015-09-16如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。6.8.2 二叉树遍历方法2015-09-181.前序遍历
规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。
2015-09-182.中序遍历
规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。
2015-09-183.后序遍历
规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。
2015-09-18.层序遍历
规则是若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。
6.8.3 前序遍历算法2015-09-18前序遍历算法
二叉树的定义是用递归的方式,所以,实现遍历算法也可以采用递归,而且极其简洁明了。先来看看二叉树的前序遍历算法。代码如下:
/* 二叉树的前序遍历递归算法 */ void PreOrderTraverse( BiTree T )
{
     if ( T == NULL )
          return;                 /* 显示结点数据,可以更改为其他对结点操作 */
     printf( "%c", T->data );        /* 再先序遍历左子树 */
     PreOrderTraverse( T->lchild );  /* 最后先序遍历右子树 */
     PreOrderTraverse( T->rchild );
}
6.8.4 中序遍历算法2015-09-18中序遍历算法
那么二叉树的中序遍历算法是如何呢?哈哈,别以为很复杂,它和前序遍历算法仅仅只是代码的顺序上的差异。
/* 二叉树的中序遍历递归算法 */ void InOrderTraverse( BiTree T )
{
     if ( T == NULL )
          return;                 /* 中序遍历左子树 */
     InOrderTraverse( T->lchild );   /* 显示结点数据,可以更改为其他对结点操作 */
     printf( "%c", T->data );        /* 最后中序遍历右子树 */
     InOrderTraverse( T->rchild );
}
6.8.5 后序遍历算法2015-09-18后序遍历算法
那么同样的,后序遍历也就很容易想到应该如何写代码了。
/* 二叉树的后序遍历递归算法 */ void PostOrderTraverse( BiTree T )
{
     if ( T == NULL )
          return;                 /* 先后序遍历左子树 */
     PostOrderTraverse( T->lchild ); /* 再后序遍历右子树 */
     PostOrderTraverse( T->rchild ); /* 显示结点数据,可以更改为其他对结点操作 */
     printf( "%c", T->data );
}
6.9 二叉树的建立2015-09-18我们就可以来看看如何生成一棵二叉树了。假设二叉树的结点均为一个字符,我们把刚才前序遍历序列AB#D##C##用键盘挨个输入。实现的算法如下:/* 按前序输入二叉树中结点的值(一个字符) */
/* #表示空树,构造二叉链表表示二叉树T。 */ void CreateBiTree( BiTree *T )
{
     TElemType ch;
     scanf( "%c", &ch ); if ( ch == '#' )
          *T = NULL;
     else{ *T = (BiTree) malloc( sizeof(BiTNode) );
           if ( !*T )
                exit( OVERFLOW );
/* 生成根结点 */ (*T)->data = ch;
/* 构造左子树 */ CreateBiTree( &(*T)->lchild );
/* 构造右子树 */ CreateBiTree( &(*T)->rchild ); }
}
6.11.1 树转换为二叉树2015-09-19将树转换为二叉树的步骤如下 1.加线。在所有兄弟结点之间加一条连线。 2.去线。对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线。 3.层次调整。以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子。6.11.2 森林转换为二叉树2015-09-19森林转换为二叉树
森林是由若干棵树组成的,所以完全可以理解为,森林中的每一棵树都是兄弟,可以按照兄弟的处理办法来操作。步骤如下: 1.把每个树转换为二叉树。 2.第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。当所有的二叉树连接起来后就得到了由森林转换来的二叉树
6.12.1 赫夫曼树2015-09-21我们平时所用的压缩和解压缩技术也都是基于赫夫曼的研究之上发展而来7.2 图的定义2015-09-21图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。7.2.1 各种图定义2015-09-21无向边:若顶点vi到vj之间的边没有方向,则称这条边为无向边(Edge),用无序偶对(vi,vj)来表示。如果图中任意两个顶点之间的边都是无向边,则称该图为无向图(Undirected graphs)。2015-09-21有向边:若从顶点vi到vj的边有方向,则称这条边为有向边,也称为弧(Arc)。用有序偶<vi,vj>来表示,vi称为弧尾(Tail),vj称为弧头(Head)。如果图中任意两个顶点之间的边都是有向边,则称该图为有向图(Directed graphs)。2015-09-21无向边用小括号“()”表示,而有向边则是用尖括号“<>”表示。2015-09-21在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。含有n个顶点的无向完全图有n(n-1)/2条边。2015-09-21在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。含有n个顶点的有向完全图有n×(n-1)条边2015-09-21有很少条边或弧的图称为稀疏图,反之称为稠密图。这里稀疏和稠密是模糊的概念,都是相对而言的2015-09-21有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权(Weight)。这些权可以表示从一个顶点到另一个顶点的距离或耗费。这种带权的图通常称为网(Network)。7.2.4 图的定义与术语总结2015-09-21图按照有无方向分为无向图和有向图。无向图由顶点和边构成,有向图由顶点和弧构成。弧有弧尾和弧头之分。2015-09-21图中顶点之间有邻接点、依附的概念。无向图顶点的边数叫做度,有向图顶点分为入度和出度。2015-09-21图中顶点间存在路径,两顶点存在路径则说明是连通的,如果路径最终回到起始点则称为环,当中不重复叫简单路径。若任意两顶点都是连通的,则图就是连通图,有向则称强连通图。图中有子图,若子图极大连通则就是连通分量,有向的则称强连通分量。7.4.1 邻接矩阵2015-09-21图的邻接矩阵(Adjacency Matrix)存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。7.4.5 边集数组2015-09-21边集数组是由两个一维数组构成。一个是存储顶点的信息;另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、终点下标(end)和权(weight)组成,如图7-4-14所示。显然边集数组关注的是边的集合,在边集数组中要查找一个顶点的度需要扫描整个边数组,效率并不高。因此它更适合对边依次进行处理的操作,而不适合对顶点相关的操作。7.5 图的遍历2015-09-21对于图的遍历来说,如何避免因回路陷入死循环,就需要科学地设计遍历方案,通常有两种遍历次序方案:它们是深度优先遍历和广度优先遍历。7.5.1 深度优先遍历2015-09-22深度优先遍历(Depth_First_Search),也有称为深度优先搜索,简称为DFS7.5.2 广度优先遍历2015-09-22广度优先遍历(Breadth_First_Search),又称为广度优先搜索,简称BFS7.6 最小生成树2015-09-22找连通网的最小生成树,经典的有两种算法,普里姆算法和克鲁斯卡尔算法。8.2 查找概论2015-09-22静态查找表(Static Search Table):只作查找操作的查找表。它的主要操作有:(1)查询某个“特定的”数据元素是否在查找表中。(2)检索某个“特定的”数据元素和各种属性。2015-09-22动态查找表(Dynamic Search Table):在查找过程中同时插入查找表中不存在的数据元素,或者从查找表中删除已经存在的某个数据元素。显然动态查找表的操作就是两个:(1)查找时插入数据元素。(2)查找时删除数据元素。8.3.2 顺序表查找优化2015-09-22顺序表查找优化到这里并非足够完美,因为每次循环时都需要对i是否越界,即是否小于等于n作判断。事实上,还可以有更好一点的办法,设置一个哨兵,可以解决不需要每次让i与n作比较。看下面的改进后的顺序查找算法代码。/* 有哨兵顺序查找 */
int Sequential_Search2( int *a, int n, int key )
{
     int i;          /* 设置a[0]为关键字值,我们称之为“哨兵” */
     a[0]     = key;  /* 循环从数组尾部开始 */
     i     = n; while ( a[i] != key )
     {
          i--;
     } /* 返回0则说明查找失败 */
     return(i);
}
2015-09-22这种在查找方向的尽头放置“哨兵”免去了在查找过程中每一次比较后都要判断查找位置是否越界的小技巧,看似与原先差别不大,但在总数据较多时,效率提高很大,是非常好的编码技巧。当然,“哨兵”也不一定就一定要在数组开始,也可以在末端。8.4.1 折半查找2015-09-22/* 有哨兵顺序查找 */
int Sequential_Search2( int *a, int n, int key )
{
     int i;                                          /* 设置a[0]为关键字值,我们称之为“哨兵” */
     a[0]     = key;                                  /* 循环从数组尾部开始 */
     i     = n; while ( a[i] != key )
     {
          i--;
     } /* 返回0则说明查找失败 *//* 折半查找 */
     int Binary_Search( int *a, int n, int key )
     {
          int low, high, mid;                     /* 定义最低下标为记录首位 */
          low     = 1;                            /* 定义最高下标为记录末位 */
          high     = n; while ( low <= high )
          {                                       /* 折半 */
               mid = (low + high) / 2;         /* 若查找值比中值小 */
               if ( key < a[mid] )             /* 最高下标调整到中位下标小一位 */
                    high = mid - 1;         /* 若查找值比中值大 */
               else if ( key > a[mid] )        /* 最低下标调整到中位下标大一位 */
                    low = mid + 1;
               else
/* 若相等则说明mid即为查找到的位置 */ return(mid);
          }
          return(0);
     }


     return(i);
}
8.4.2 插值查找2015-09-23折半查找代码的第8句,我们略微等式变换后得到:
也就是mid等于最低下标low加上最高下标high与low的差的一半。算法科学家们考虑的就是将这个1/2进行改进,改进为下面的计算方案:将1/2改成了(key-a[low])/(a[high]-a[low])有什么道理呢?假设a[11]={0,1,16,24,35,47,59,62,73,88,99},low=1,high=10,则a[low]=1,a[high]=99,如果我们要找的是key=16时,按原来折半的做法,我们需要四次(如图8-4-6)才可以得到结果,但如果用新办法,(key-a[low])/(a[high]-a[low])=(16-1)/(99-1)≈0.153,即mid≈1+0.153×(10-1)=2.377取整得到mid=2,我们只需要二次就查找到结果了,显然大大提高了查找的效率。
换句话说,我们只需要在折半查找算法的代码中更改一下第8行代码如下: 
mid=low+ (high-low)*(key-a[low])/(a[high]-a[low]); /* 插值 */
8.5.1 稠密索引2015-09-24索引按照结构可以分为线性索引、树形索引和多级索引。我们这里就只介绍线性索引技术。所谓线性索引就是将索引项集合组织为线性结构,也称为索引表。我们重点介绍三种线性索引:稠密索引、分块索引和倒排索引。8.6 二叉排序树2015-09-24二叉排序树(Binary Sort Tree),又称为二叉查找树。它或者是一棵空树,或者是具有下列性质的二叉树。
若它的左子树不空,则左子树上所有结点的值均小于它的根结构的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
它的左、右子树也分别为二叉排序树。从二叉排序树的定义也可以知道,它前提是二叉树,然后它采用了递归的定义方法,再者,它的结点间满足一定的次序关系,左子树结点一定比其双亲结点小,右子树结点一定比其双亲结点大。
构造一棵二叉排序树的目的,其实并不是为了排序,而是为了提高查找和插入删除关键字的速度
8.6.1 二叉排序树查找操作2015-09-24首先我们提供一个二叉树的结构。/* 二叉树的二叉链表结点结构定义 */
/* 结点结构 */ typedef struct BiTNode
{                               /* 结点数据 */
     int          data;   /* 左右孩子指针 */
     struct BiTNode     *lchild, *rchild;
} BiTNode, *BiTree;
2015-09-24然后我们来看看二叉排序树的查找是如何实现的。/* 递归查找二叉排序树T中是否存在key, */


/* 指针f指向T的双亲,其初始调用值为NULL */ /* 若查找成功,则指针p指向该数据元素结点,并
* 返回TRUE */ /* 否则指针p指向查找路径*问的最后一个结点
* 并返回FALSE */Status SearchBST( BiTree T, int key, BiTree f, BiTree *p )
{       /* 查找不成功 */
     if ( !T )
     {
          *p = f; return(FALSE);
     } /* 查找成功 */
     else if ( key == T->data )
     {
          *p = T; return(TRUE);
     } else if ( key < T->data )
/* 在左子树继续查找 */ return(SearchBST( T->lchild, key, T, p ) );
     else    /* 在右子树继续查找 */
          return(SearchBST( T->rchild, key, T, p ) );
}
8.6.2 二叉排序树插入操作2015-09-24有了二叉排序树的查找函数,那么所谓的二叉排序树的插入,其实也就是将关键字放到树中的合适位置而已,来看代码。/* 当二叉排序树T中不存在关键字等于key的数据元
* 素时, */ /* 插入key并返回TRUE,否则返回FALSE */
Status InsertBST( BiTree *T, int key )
{
     BiTree p, s;                            /* 查找不成功 */
     if ( !SearchBST( *T, key, NULL, &p ) )
     {
          s          = (BiTree) malloc( sizeof(BiTNode) ); s->data = key;
          s->lchild     = s->rchild = NULL; if ( !p )
/* 插入s为新的根结点 */ *T = s;
          else if ( key < p->data )       /* 插入s为左孩子 */
               p->lchild = s;
          else
/* 插入s为右孩子 */ p->rchild = s;
          return(TRUE);
     }else                                   /* 树中已有关键字相同的结点,不再插入 */
          return(FALSE);
}
8.9.2 散列表查找步骤2015-09-25散列技术既是一种存储方法,也是一种查找方法。然而它与线性表、树、图等结构不同的是,前面几种结构,数据元素之间都存在某种逻辑关系,可以用连线图示表示出来,而散列技术的记录之间不存在什么逻辑关系,它只与关键字有关联。因此,散列主要是面向查找的存储结构。8.10.2 数字分析法2015-09-25若我们现在要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前7位都是相同的。那么我们选择后面的四位成为散列地址就是不错的选择。如果这样的抽取工作还是容易出现冲突问题,还可以对抽取出来的数字再进行反转(如1234改成4321)、右环位移(如1234改成4123)、左环位移、甚至前两数与后两数叠加(如1234改成12+34=46)等方法。总的目的就是为了提供一个散列函数,能够合理地将关键字分配到散列表的各位置。8.10.3 平方取中法2015-09-25这个方法计算很简单,假设关键字是1234,那么它的平方就是1522756,再抽取中间的3位就是227,用做散列地址。再比如关键字是4321,那么它的平方就是18671041,抽取中间的3位就可以是671,也可以是710,用做散列地址。平方取中法比较适合于不知道关键字的分布,而位数又不是很大的情况。8.10.4 折叠法2015-09-25折叠法是将关键字从左到右分割成位数相等的几部分(注意最后一部分位数不够时可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
比如我们的关键字是9876543210,散列表表长为三位,我们将它分为四组,987|654|321|0,然后将它们叠加求和987+654+321+0=1962,再求后3位得到散列地址为962。
2015-09-25有时可能这还不能够保证分布均匀,不妨从一端向另一端来回折叠后对齐相加。比如我们将987和321反转,再与654和0相加,变成789+654+123+0=1566,此时散列地址为566。
折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况。
8.10.5 除留余数法2015-09-25此方法为最常用的构造散列函数方法。对于散列表长为m的散列函数公式为:
f(key)=key mod p(p≤m)mod是取模(求余数)的意思。事实上,这方法不仅可以对关键字直接取模,也可在折叠、平方取中后再取模。
2015-09-25因此根据前辈们的经验,若散列表表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。8.10.6 随机数法2015-09-29选择一个随机数,取关键字的随机函数值为它的散列地址。也就是f(key)=random(key)。这里random是随机函数。当关键字的长度不等时,采用这个方法构造散列函数是比较合适的。
有同学问,那如果关键字是字符串如何处理?其实无论是英文字符,还是中文字符,也包括各种各样的符号,它们都可以转化为某种数字来对待,比如ASCII码或者Unicode码等,因此也就可以使用上面的这些方法。
9.2.2 内排序与外排序2015-09-29根据在排序过程中待排序的记录是否全部被放置在内存中,排序分为:内排序和外排序。
内排序是在排序整个过程中,待排序的所有记录全部被放置在内存中。外排序是由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行。
9.3.1 最简单排序实现2015-09-30最简单排序实现2015-09-30/* 对顺序表L作交换排序(冒泡排序初级版) */
void BubbleSort0( SqList *L )
{
     int i, j; for ( i = 1; i < L->length; i++ )
     {
          for ( j = i + 1; j <= L->length; j++ )
          {
               if ( L->r[i] > L->r[j] )
               {       /* 交换L->r[i]与L->r[j]的值 */
                    swap( L, i, j );
               }
          }
     }
}
9.3.2 冒泡排序算法2015-09-30冒泡排序算法2015-09-30/* 对顺序表L作冒泡排序 */
void BubbleSort( SqList *L )
{
     int i, j; for ( i = 1; i < L->length; i++ )
     {                       /* 注意j是从后往前循环 */
          for ( j = L->length - 1; j >= i; j-- )
          {
/* 若前者大于后者(注意这里与上一算法差异) */ if ( L->r[j] > L->r[j + 1] )
               {       /* 交换L->r[j]与L->r[j+1]的值 */
                    swap( L, j, j + 1 );
               }
          }
     }
}
9.3.3 冒泡排序优化2015-09-30冒泡排序优化2015-09-30/* 对顺序表L作改进冒泡算法 */
void BubbleSort2( SqList *L )
{
     int     i, j;           /* flag用来作为标记 */
     Status     flag = TRUE;    /* 若flag为true说明有过数据交换,否则停止循环 */
     for ( i = 1; i < L->length && flag; i++ )
     {
/* 初始为false */ flag = FALSE;
          for ( j = L->length - 1; j >= i; j-- )
          {
               if ( L->r[j] > L->r[j + 1] )
               {
/* 交换L->r[j]与L->r[j+1]的值 */ swap( L, j, j + 1 );
/* 如果有数据交换,则flag为true */ flag = TRUE;
               }
          }
     }
}
9.4.1 简单选择排序算法2015-09-30简单选择排序算法2015-09-30/* 对顺序表L作简单选择排序 */
void SelectSort( SqList *L )
{
     int i, j, min; for ( i = 1; i < L->length; i++ )
     {                       /* 将当前下标定义为最小值下标 */
          min = i;        /* 循环之后的数据 */
          for ( j = i + 1; j <= L->length; j++ )
          {
/* 如果有小于当前最小值的关键字 */ if ( L->r[min] > L->r[j] )
/* 将此关键字的下标赋值给min */ min = j;
          } /* 若min不等于i,说明找到最小值,交换 */
          if ( i != min ) /* 交换L->r[i]与L->r[min]的值 */
               swap( L, i, min );
     }
}
9.5.1 直接插入排序算法2015-09-30直接插入排序算法2015-09-30/* 对顺序表L作直接插入排序 */
void InsertSort( SqList *L )
{
     int i, j; for ( i = 2; i <= L->length; i++ )
     {                                                               /* 需将L->r[i]插入有序子表 */
          if ( L->r[i] < L->r[i - 1] )
          {
/* 设置哨兵 */ L->r[0] = L->r[i];
               for ( j = i - 1; L->r[j] > L->r[0]; j-- )       /* 记录后移 */
                    L->r[j + 1] = L->r[j];                  /* 插入到正确位置 */
               L->r[j + 1] = L->r[0];
          }
     }
}
9.6.2 希尔排序算法2015-09-30希尔排序算法代码如下。

/* 对顺序表L作希尔排序 */void ShellSort( SqList *L ){int i, j; int increment = L->length;do{/* 增量序列 */ increment = increment / 3 + 1;for ( i = increment + 1; i <= L->length; i++ ){if ( L->r[i] < L->r[i - increment] ){/* 需将L->r[i]插入有序增量子表 */ /* 暂存在L->r[0] */L->r[0] = L->r[i]; for ( j = i - increment; j > 0 && L->r[0] < L->r[j]; j -= increment )    /* 记录后移,查找插入位置 */L->r[j + increment] = L->r[j];                          /* 插入 */L->r[j + increment] = L->r[0];}}}while ( increment > 1 );}
9.7.1 堆排序算法2015-09-30堆排序算法2015-09-30/* 对顺序表L进行堆排序 */
void HeapSort( SqList *L )
{
     int i; /* 把L中的r构建成一个大顶堆 */
     for ( i = L->length / 2; i > 0; i-- )
          HeapAdjust( L, i, L->length );
     for ( i = L->length; i > 1; i-- )
     {
/* 将堆顶记录和当前未经排序子序列的最后一个记录交换 */ swap( L, 1, i );
/* 将L->r[1..i-1]重新调整为大顶堆 */ HeapAdjust( L, 1, i - 1 );
     }
}
9.8.1 归并排序算法2015-09-30归并排序(Merging Sort)就是利用归并的思想实现的排序方法。它的原理是假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到|n/2|(|x|表示不小于x的最小整数)个长度为2或1的有序子序列;再两两归并,……,如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序。2015-09-30/* 对顺序表L作归并排序 */
void MergeSort( SqList *L )
{
     MSort( L->r, L->r, 1, L->length );
}


2015-09-30/* 将SR[s..t]归并排序为TR1[s..t] */
void MSort( int SR[], int TR1[], int s, int t )
{
     int m; int TR2[MAXSIZE + 1];
     if ( s == t )
          TR1[s] = SR[s];
     else {
/* 将SR[s..t]平分为SR[s..m]和SR[m+1..t] */ m = (s + t) / 2;
/* 递归将SR[s..m]归并为有序的TR2[s..m] */ MSort( SR, TR2, s, m );
/* 递归将SR[m+1..t]归并为有序TR2[m+1..t] */ MSort( SR, TR2, m + 1, t );
/* 将TR2[s..m]和TR2[m+1..t] */ /* 归并到TR1[s..t] */
          Merge( TR2, TR1, s, m, t );
     }
}
2015-09-30/* 将有序的SR[i..m]和SR[m+1..n]归并为有序的
* TR[i..n] */void Merge( int SR[], int TR[], int i, int m, int n )
{
     int j, k, l;
/* 将SR中记录由小到大归并入TR */ for ( j = m + 1, k = i; i <= m && j <= n; k++ )
     {
          if ( SR[i] < SR[j] )
               TR[k] = SR[i++];
          else
               TR[k] = SR[j++];
     }
     if ( i <= m )
     {
          for ( l = 0; l <= m - i; l++ )  /* 将剩余的SR[i..m]复制到TR */
               TR[k + l] = SR[i + l];
     }
     if ( j <= n )
     {
          for ( l = 0; l <= n - j; l++ )  /* 将剩余的SR[j..n]复制到TR */
               TR[k + l] = SR[j + l];
     }
}
9.8.3 非递归实现归并排序2015-09-30非递归实现归并排序2015-09-30/* 对顺序表L作归并非递归排序 */
void MergeSort2( SqList *L )
{
/* 申请额外空间 */ int     * TR     = (int *) malloc( L->length * sizeof(int) );
     int          k     = 1; while ( k < L->length )
     {
          MergePass( L->r, TR, k, L->length );
/*子序列长度加倍 */ k = 2 * k;
          MergePass( TR, L->r, k, L->length ); /* 子序列长度加倍 */
          k = 2 * k;
     }
}


2015-09-30/* 将SR[]中相邻长度为s的子序列两两归并到TR[] */
void MergePass( int SR[], int TR[], int s, int n )
{
     int i = 1; int j;
     while ( i <= n - 2 * s + 1 )
     {
/* 两两归并 */ Merge( SR, TR, i, i + s - 1, i + 2 * s - 1 );
          i = i + 2 * s;
     }
/* 归并最后两个序列 */ if ( i < n - s + 1 )
          Merge( SR, TR, i, i + s - 1, n );  /* 若最后只剩下单个子序列 */
     else for ( j = i; j <= n; j++ )
               TR[j] = SR[j];
}


9.9 快速排序2015-09-30希尔排序相当于直接插入排序的升级,它们同属于插入排序类,堆排序相当于简单选择排序的升级,它们同属于选择排序类。而快速排序其实就是我们前面认为最慢的冒泡排序的升级,它们都属于交换排序类。即它也是通过不断比较和移动交换来实现排序的,只不过它的实现,增大了记录的比较和移动的距离,将关键字较大的记录从前面直接移动到后面,关键字较小的记录从后面直接移动到前面,从而减少了总的比较次数和移动交换次数。9.9.1 快速排序算法2015-09-30快速排序(Quick Sort)的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。2015-09-30/* 对顺序表L作快速排序 */
void QuickSort( SqList *L )
{
     QSort( L, 1, L->length );
}
2015-09-30/* 对顺序表L中的子序列L->r[low..high]作快速排
* 序 */void QSort( SqList *L, int low, int high )
{
     int pivot;
     if ( low < high )
     {
/* 将L->r[low..high]一分为二, */ /* 算出枢轴值pivot */
          pivot = Partition( L, low, high );      /* 对低子表递归排序 */
          QSort( L, low, pivot - 1 );             /* 对高子表递归排序 */
          QSort( L, pivot + 1, high );
     }
}
2015-09-30/* 交换顺序表L中子表的记录,使枢轴记录到位,
* 并返回其所在位置 */ /* 此时在它之前(后)的记录均不大(小)于
* 它。 */int Partition( SqList *L, int low, int high )
{
     int pivotkey;
/* 用子表的第一个记录作枢轴记录 */ pivotkey = L->r[low];
/* 从表的两端交替向中间扫描 */ while ( low < high )
     {
          while ( low < high && L->r[high] >= pivotkey )
               high--;         /* 将比枢轴记录小的记录交换到低端 */
          swap( L, low, high ); while ( low < high && L->r[low] <= pivotkey )
               low++;          /* 将比枢轴记录大的记录交换到高端 */
          swap( L, low, high );
     }
/* 返回枢轴所在位置 */ return(low);
}


9.9.3 快速排序优化2015-09-30当中的交换其实是不需要的2015-09-30/* 快速排序优化算法 */
int Partition1( SqList *L, int low, int high )
{
     int pivotkey;                   /* 这里省略三数取中代码 */
/* 用子表的第一个记录作枢轴记录 */ pivotkey     = L->r[low];
/* 将枢轴关键字备份到L->r[0] */ L->r[0]     = pivotkey;
/* 从表的两端交替向中间扫描 */ while ( low < high )
     {
          while ( low < high && L->r[high] >= pivotkey )
               high--;         /* 采用替换而不是交换的方式进行操作 */
          L->r[low] = L->r[high]; while ( low < high && L->r[low] <= pivotkey )
               low++;          /* 采用替换而不是交换的方式进行操作 */
          L->r[high] = L->r[low];
     }
/* 将枢轴数值替换回L.r[low] */ L->r[low] = L->r[0];
/* 返回枢轴所在位置 */ return(low);
}
2015-09-30优化小数组时的排序方案2015-09-30define MAX_LENGTH_INSERT_SORT 7 /* 数组长度阀值 */
/* 对顺序表L中的子序列L.r[low..high]作快速排序 */ void QSort( SqList &L, int low, int high )
{
     int pivot;
     if ( (high - low) > MAX_LENGTH_INSERT_SORT )
     {
/* 当high-low大于常数时用快速排序 */ /* 将L.r[low..high]一分为二, */
/* 并算出枢轴值pivot */ pivot = Partition( L, low, high );
/* 对低子表递归排序 */ QSort( L, low, pivot - 1 );
/* 对高子表递归排序 */ QSort( L, pivot + 1, high );
     } else
/* 当high-low小于等于常数时用直接插入排序 */ InsertSort( L );
}
2015-09-30优化递归操作2015-09-30/* 对顺序表L中的子序列L.r[low..high]作快速排序 */
void QSort1( SqList *L, int low, int high )
{
     int pivot; if ( (high - low) > MAX_LENGTH_INSERT_SORT )
     {
          while ( low < high )
          {       /* L.r[low..high]一分为二, */
/* 算出枢轴值pivot */ pivot = Partition1( L, low, high );
/* 对低子表递归排序 */ QSort1( L, low, pivot - 1 );
/* 尾递归 */ low = pivot + 1;
          }
     }else InsertSort( L );
}