2015-08-17
常用的时间复杂度所耗费的时间从小到大依次是:
O(1)
3.2 线性表的定义
2015-08-17
线性表(List):零个或多个数据元素的有限序列。
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数组值推导
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-18
1.前序遍历
规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。
2015-09-18
2.中序遍历
规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。
2015-09-18
3.后序遍历
规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。
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称为弧尾(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),也有称为深度优先搜索,简称为DFS
7.5.2 广度优先遍历
2015-09-22
广度优先遍历(Breadth_First_Search),又称为广度优先搜索,简称BFS
7.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
/* 对顺序表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
/* 对顺序表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
/* 对顺序表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
/* 对顺序表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
/* 对顺序表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
/* 对顺序表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路归并排序。
/* 对顺序表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 非递归实现归并排序
/* 对顺序表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;
}
}
/* 将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 );
}
}
/* 交换顺序表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
/* 快速排序优化算法 */
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
define 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
/* 对顺序表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 );
}