数据结构总结
数据结构 知识点总结
1第一单元 基础知识
1.数据与数据结构
数据:计算机加工处理的对象,分为数值数据和非数值数据
数据元素(结点、顶点):组成数据的基本单位
数据项(字段、域):组成数据的最小单位
数据结构的概念:
(1)逻辑结构:数据元素间的逻辑关系
(a)集合结构
(b)线性结构
(c)树形结构
(d)图状结构
3类基本的逻辑结构:线性结构,树形结构,图状结构
2类基本的逻辑结构:线性结构,非线性结构
(2)存储结构:数据在计算机中的表示形式
(a)顺序存储结构
(b)链接存储结构
(c)索引存储结构
(d)散列存储结构
(3)运算:在数据上执行的操作
创建、清除、插入、删除等
·数据的逻辑结构和数据的运算定义组成了数据结构的规范。
·数据的存储表示和运算算法的描述构成数据结构的实现。
数据结构的分类:
(1)静态数据结构:一旦创建,其结构不再改变的数据结构。
(2)动态数据结构:允许进行插入删除等操作,其结构是动态变化的数据结构。
2.数据抽象和抽象数据类型
(1)抽象(降低了问题求解的难度)
数据抽象:只关注数据元素间的逻辑关系,忽略数据在计算机中的具体表示。
过程抽象:只关注数据运算的定义,忽略运算的具体实现方法。
(2)封装与信息隐蔽
(错误局部化,降低问题求解的复杂性,提高程序的可靠性)
封装:是指把数据和操纵数据的运算组合在一起的机制。使用者只能通过一组允许的运算访问其中的数据。
信息隐蔽:对使用者隐藏了数据结构或程序的实现细节。
(3)数据类型和抽象数据类型
数据类型:它是数据抽象的一种方式。一个数据类型定义了一个值的集合以 及作用于该值集的运算集合。
抽象数据类型(ADT):该类型的对象及其运算的规范,与该类型对象的表示 和运算的实现分离,实行封装和信息隐蔽,即所谓使用和实现分离,数据结 构是一种抽象数据类型。
3.算法分析的基本方法
计算机算法:一个有穷的指令序列,它规定了解决某一特定问题的一系列运算。
计算机算法的特征:输入、输出、确定性、能行性、有穷性
“好算法”的特征:正确、简明、健壮、效率
(1)时间复杂度
O(1) < O (log2n) < O (n) < O (nlog2n) < O (n2) < O (n3) < O (2n)
考点:最好、最坏和平均时间复杂度
(2)空间复杂度
算法执行过程中对存储空间的需求量。
通常是分析最坏的情况。
2第二单元 线性表
线性表(LinearList)是动态数据结构,它的表长可以改变。
1.顺序表:顺序存储表示的线性表称为顺序表
地址计算公式:loc(ai)=loc(a0)+i*k
只要给定loc(a0)和k,就可以确定线性表中任意一个元素的存储地址。
顺序表是一种随机存取结构。
相关运算:
Find(i,x):查找下标为i的元素a[i]。在x中返回表中下标为i的元素a[i](即表中第i+1个元素)。如果不存在,则返回false,否则返回true。
Insert(i,x):在表中下标为i的元素ai后插入x。若i=-1,则将新元素x插在最前面。若插入成功,返回true。
Delete(i): 删除元素a[i]。
优点:随机存取;存储空间利用率高。
缺点:插入、删除效率低;必须按事先估计的最大元素个数分配连续的存储空间,难以临时扩大。
2.单向链表
·表头指针first是指向链表的头结点的指针
相关运算:
Find(i,x):必须从表头指针开始沿链逐个计数查找,称为顺序查找。搜索运算的平均、最坏的渐近时间复杂度都是O(n)。
Insert(i,x):生成数据域为x的新结点,q指向新结点;从first开始找第i+1个结点,p指向该结点;将q插入p之后,表长加1。
·注意区分插在头结点和一般节点的情况
Delete(i):从first开始找第i+1个结点,p指向该结点,q指向p之前驱结点;从单链表中删除p;放p之空间(delete p);表长减1。
优点:单链表插入和删除只需修改一两个指针,无需移动元素。可以动态分配结点空间,线性表的长度只受内存大小限制。
缺点:查找运算费时,只能顺序查找,不能随机查找。
3.带表头结点的单向链表
注意区分“表头结点”和“头结点”。
头结点:线性表中下标为0的元素、第1个元素;
表头结点:为简化算法而附加的结点,不是线性表中的元素。
·在算法中无需将头结点和一般节点的情况分开讨论
4.单向循环链表
5.双向循环链表
插入、删除运算算法
3第三单元 栈和队列
1.栈(后进先出)
栈是限定插入和删除运算只能在线性表同一端进行的动态数据结构。
允许插入和删除元素的一端称为栈顶,另一端称为栈底。
相关运算:
IsEmpty():若栈空,则返回true; 否则返回 false。
IsFull(): 若栈满,则返回true; 否则返回 false。
Top(x):返回栈顶元素。若操作成功,则返回true;否则返回false。
Push(x):在栈顶插入元素x。
Pop():从栈中删除栈顶元素。
Clear():清除堆栈中全部元素。
(1)顺序栈
一维数组s存储栈中元素, maxTop+1为最大允许容量,top指示栈顶,top==-1表示空栈,top==maxTop表示栈满。
(2)链接栈
2.队列(先进先出)
队列是限定只能在线性表的一端插入元素,而在另一端删除元素的动态数据结构。
允许插入元素的一端称队尾,允许删除元素的另一端称队头。
(1)顺序队列
注意:使用两个指针front和rear,front指向队头元素的前一单元,rear指向队尾元素。
队头指针进一:front=(front+1) % maxSize;
队尾指针进一:rear=(rear+1) % maxSize;
空队列:当front==rear时
满队列:当(rear+1) % maxSize==front时
相关运算:
Front(x):在x中返回队头元素。操作成功返回true;否则返回false。
EnQueue(x):在队尾插入元素x。操作成功返回true;否则返回false。
Dequeue():从队列中删除队头元素。操作成功返回true;否则返回false。
(2)链接队列
4第四单元 树和二叉树
1.树
树(有根树)是包括n个结点的有限非空集合。
特性:递归数据结构,层次结构
定义一:… 定义二:…
相关术语:
双亲:若一个结点有子树,那么该结点称为子树根的双亲。
孩子:子树的根是该结点的孩子。
兄弟:有相同双亲的结点。
祖先:从根结点到某个结点路径上的所有结点都是该结点的祖先。
后裔:一个结点的所有子树上的任何结点都是该结点的后裔。
结点的度:一个结点拥有的子树数。
树的度:树中最大的结点的度。
树叶:度为零的结点。
分支结点:度不为零的结点。
结点的层次:从根开始定义起,根为第1层,其余结点的层次等于其双亲结点的层次加1。
树的高度:树中结点的最大层次。
森林:树的集合。
2.二叉树
二叉树是结点的有限集合,可以为空集,区分左右子树。
(1)性质:
二叉树的第i(i>1)层上至多有2i-1 个结点。
高度为h的二叉树上至多有2^h–1个结点。
叶结点的个数总是比度为2的结点的个数多一个。
(2)几个概念:
满二叉树:高度为h的二叉树恰好有2^h–1个结点时称为满二叉树。
完全二叉树:一棵二叉树中,只有最下面两层结点的度可以小于2,并且最下一层的叶结点集中在靠左的若干位置上,这样的二叉树称为完全二叉树。(性质见书P71)
扩充二叉树:除叶子结点外,其余结点都必须有两个孩子。
(3)相关运算:
Root(x): 若二叉树非空,则x有根的值,并返回true,否则返回false。
MakeTree(x, left, right): 构造一棵二叉树:根的值为x,以left和right为左右子树。
BreakTree(x, left, right):拆分二叉树为三部分:x为根的值,left和right分别为原二叉树的左、右子树。
(4)二叉树的遍历
前序遍历、中序遍历、后序遍历、层次遍历(规则见书P75)
递归算法见P77
(5)二叉树的存储表示
顺序表示:完全二叉树中的结点可以按层次顺序,存储在一片连续的存储单元中。
链接表示:lChild和rChild为分别指向左、右孩子的指针,element是元素域。共有n-1个指针域非空,n+1个空指针域。
3.树和森林
(1)树和森林的存储表示(见书P82)
多重链表表示法
孩子兄弟链表示法
双亲表示法
三重链表表示法
(*)注意和二叉树的存储表示区分!
(2)树的遍历
前序遍历:访问根结点;从左往右前序遍历根的每一棵子树。
后序遍历:从左往右后序遍历根的每一棵子树;访问根结点。
层次遍历:从上往下,同一层从左往右,访问树中的每个结点。
PreOrder: GDAFEMHZ 先上再左
InOrder: ADEFGHMZ(中序) 先左再上
PostOrder: AEFDHZMG 先左再右
(3)森林的遍历
加入一个虚结点,作为各棵树的根;遍历这棵树;删去虚结点。
先序遍历:访问第一棵树的根;按先序遍历第一棵树的根节点的子树组成 的森林;按先序遍历除第一棵树外其余树组成的森林。
中序遍历、后序遍历以此类推。
(4)森林与二叉树的转换(见书P81)
4.哈夫曼树和哈夫曼编码
(1)路径长度:在二叉树中,从根到任意一个后裔结点的路径长度是指从根结点到该后裔结点的路径上所包括的边的数目。
二叉树的内路径长度:从根到其它所有分支结点的路径长度之和。
二叉树的外路径长度:从根到其它所有叶子结点的路径长度之和。
二叉树的加权路径长度:二叉树中所有叶子结点的加权路径长度之和。
(2)哈夫曼树和哈夫曼算法
最优二叉树:具有最小加权路径长度的二叉树。
哈夫曼算法:由哈夫曼给出、用于构造最优二叉树的算法。
a.用给定的一组权值{w1,w2 ,…,wn},生成一个有n棵二叉树组成的集合 F={T1,T2,…,Tn},其中,每棵二叉树Ti只有一个结点,即权值为wi的 根结点。
b.从F中选择两棵根结点权值最小的二叉树,作为新二叉树根的左、右子树, 新二叉树根的权值是左、右子树根结点的权值之和。
c.从F中删除这两棵二叉树,另将新二叉树加入F中。
d.重复(2)和(3),直到F中只包含一棵二叉树为止。
哈夫曼树:用哈夫曼算法构造的最优二叉树。
(3)哈夫曼编码
哈夫曼树的每个叶结点对应一个字符。在从哈夫曼树的每个结点到其左孩子、右孩子的边上分别标上0、1。将从根到每个叶子的路径上的数码连接起来,就是该叶子所代表的字符的编码。
5第五单元 图
1.图
图是一种非线性结构。在图中,每个结点可以有任意个前驱、任意个后继。
相关术语:
顶点:图中的结点常称为顶点。
边:结点的偶对。
有向图:若代表一条边的偶对是有序的,则称其为有向图。用〈u,v〉表示有向边。
无向图:若代表一条边的偶对是无序的,则称其为无向图。用(u,v)表示无向边。
完全图:一个图有最多的边数,无向完全图有n(n-1)/2条边,有向完全图有n(n-1)条边。
简单路径:一条路径上的所有顶点,除起始顶点和终止顶点可以相同外,其余顶点各不相同。
回路:是一条简单路径,其起始顶点和终止顶点相同。
连通图:无向图中,若两个顶点u和v之间存在一条从u到v的路径,则称u和v是连通的。若图中任意一对顶点都是连通的。
强连通图:有向图中,若任意一对顶点u和v间存在一条从u到v的路径和一条从v到u的路径。
连通分量:无向图的极大连通子图。
强连通分量:有向图的极大强连通子图。
度:在无向图中,与某个顶点相关联的边的数目。
入度:在有向图中,以某个顶点为头(始点)的边的数目。
出度:在有向图中,以某个顶点为尾(终点)的边的数目。
有向图的根:恰有一个顶点入度为0,其余顶点入度为1,该顶点称为有向图的根。
网:带权值的图。
相关运算:
Exist(u,v):如果图中存在边<u,v>,则函数返回true,否则返回false。
Insert(u,v,w):向图中添加权为w的边<u,v>,若插入成功,则函数返回Success;若图中已存在边<u,v>,则函数返回Duplicate;其它情况函数返回Failure。
Remove(u,v):从图中删除边<u,v>,若图中不存在边<u,v>,则函数返回NotPresent;若图中存在边<u,v>,则从图中删除此边,函数返回Success;其它情况函数返回Failure。
Vertices():函数返回图中顶点数目。
2.图的存储结构
(1)邻接矩阵
a[u][u]=0: 主对角线元素都是0;
a[u][v]=w:
若<u,v>ÎE,则w=1,或 w=w(i,j)(带权图);
若<u,v>ÏE,则w=noEdge,其中,noEdge=0(不带权图);noEdge=INF(带权图)。
(2)邻接表
在邻接表中,为图的每个顶点u建立了一个单链表,链表中的每个结点代表一条边<u,v>,称为边结点。这样,顶点u的单链表记录了邻接自u的全部顶点。每个单链表相当于邻接矩阵的一行。
3.图的遍历
(1)深度优先遍历
a.访问顶点v,并对v做已访问标记;
b.依次从v的未访问的邻接点出发,对图作深度优先搜索。
图中所有顶点,以及在遍历时经过的边(即从已访问的顶点到达未访问顶点的边)构成的子图,称为图的深度优先搜索生成树(或生成森林)。
(2)广度优先遍历
a.访问顶点v,并对v做已访问标记;
b.依次访问v的各个未访问过的邻接点;
c.再依次访问分别于这些邻接点相邻接且未访问过的顶点。
图中所有顶点以及在遍历时经过的边(即从已访问的顶点到达未访问顶点的边)构成的子图称为图的广度优先搜索生成树(或生成森林)。
算法见书P166
4.拓扑排序
拓扑排序:将有向图中的顶点排成一个拓扑序列的过程。
拓扑序列:有向图中的一个顶点序列,对图中任意两个顶点i和j,若i是j的前驱结点,则在线性序列中i先于j。
AOV网:以顶点表示活动,有向边表示活动之间的领先关系的有向图。
注意:拓扑序列不是唯一的!
可以用拓扑排序的方法来测试有向图是否存在回路,若经过拓扑排序后所有顶点都已列出,则不存在回路。
排序步骤:
a.任选一个入度为零的顶点,并输出之;
b.从图中删除该顶点及其所有出边;
c.重复步骤1、2,直到所有顶点都已输出,或者直到剩下的图中再也没有入度为零的顶点为止,后者表示图中包含有向回路。
5.最小代价生成树
无向连通图的生成树是一个极小连通子图,它包括图中全部顶点,并且有尽可能少的边。
无向连通网络的最小代价生成树是所有生成树中边的权值之和最小的。
(1)普里姆算法:
首先,从n个顶点中任选一个顶点v加入到原来为空的生成树中;然后,重复执行下列操作:从一个顶点在生成树中,而另一个顶点不在生成树中的那些边中,选取一条权值最小的边,并将这条边以及它所关联的目前还不在生成树中的那个顶点加入到生成树中。当生成树中的顶点数达到n时,整个构造过程结束。
(2)克鲁斯卡尔算法
6.单源最短路径
边的权值之和最小的路径称为最短路径,并称v(x)为这条最短路径的源点,v(i)为终点。
迪杰斯特拉算法:
按最短路径长度值由小到大的次序,逐步求得每一条最短路径。
算法描述
1)算法思想:设G=(V,E)是一个带权有向图,把图中顶点集合V分成两组,第一组为已求出最短路径的顶点集合(用S表示,初始时S中只有一个源点,以后每求得一条最短路径 , 就将加入到集合S中,直到全部顶点都加入到S中,算法就结束了),第二组为其余未确定最短路径的顶点集合(用U表示),按最短路径长度的递增次序依次把第二组的顶点加入S中。在加入的过程中,总保持从源点v到S中各顶点的最短路径长度不大于从源点v到U中任何顶点的最短路径长度。此外,每个顶点对应一个距离,S中的顶点的距离就是从v到此顶点的最短路径长度,U中的顶点的距离,是从v到此顶点只包括S中的顶点为中间顶点的当前最短路径长度。
2)算法步骤:
a.初始时,S只包含源点,即S={v},v的距离为0。U包含除v外的其他顶点,即:U={其余顶点},若v与U中顶点u有边,则<u,v>正常有权值,若u不是v的出边邻接点,则<u,v>权值为∞。
b.从U中选取一个距离v最小的顶点k,把k,加入S中(该选定的距离就是v到k的最短路径长度)。
c.以k为新考虑的中间点,修改U中各顶点的距离;若从源点v到顶点u的距离(经过顶点k)比原来距离(不经过顶点k)短,则修改顶点u的距离值,修改后的距离值的顶点k的距离加上边上的权。
d.重复步骤b和c直到所有顶点都包含在S中。
3)算法实例
先给出一个无向图
用Dijkstra算法找出以A为起点的单源最短路径步骤如下
7.关键路径
关键路径:在无回路的有向网络中,假设只有一个入度为0的顶点(称为源点)和一个出度为0的顶点(称为汇点),则从源点到汇点之间的最长的路径称为关键路径。
AOE网:以顶点代表事件,有向边表示活动,有向边上的权表示一向活动所需的时间。 注意AOV网和AOE网的区别
关键活动:对整个工程的最短完成时间有影响的活动。
求关键路径的算法:(见书P174)
a.计算每个事件可能的最早发生时间
b.计算每个事件允许的最迟发生时间
c.输出关键活动
算法具体内容参考
6第六单元 搜索
关键字:用来标识一个数据元素的某个数据项。
主关键字:可以唯一标识一个数据元素的关键字。
次关键字:不能唯一标识一个数据元素的关键字。
搜索:在数据结构中寻找关键字等于给定值的元素。
平均搜索长度:搜索过程中关键字值之间的平均比较次数。
1.二分搜索
将中间位置上的元素的关键字与待搜索元素的关键字相比较
若相等:搜索成功。
若较小:在中间元素的左边的表中进行二分搜索,若较大则在右边进行。
实现:用low和high分别指示表的两端,m为中间元素的位置
初始时有m = (low + high)/ 2
若表长为偶数,根据小数与int型数的转换可知,m应该向下取整。
每次比较之后,根据比较的结果调整low或high的值来调整需要继续进行二分搜索操作的表的范围。
static int binarySerach(int[] array, int key) { int left = 0; int right = array.length - 1; // 这里必须是 <= while (left <= right) { int mid = (left + right) / 2; if (array[mid] == key) { return mid; } else if (array[mid] < key) { left = mid + 1; } else { right = mid - 1; } } return -1; }
每次移动left和right指针的时候,需要在mid的基础上+1或者-1, 防止出现死循环, 程序也就能够正确的运行
2.二叉搜索树
每个结点的关键字都大于其左子树上所有结点的关键字,小于右子树上所有结点的关键字(中序遍历该树,所得序列的关键字的值递增)。
搜索过程中,若元素值小于根结点的关键字值,则进入左子树,反之进入右子树。
1) 二叉搜索树的删除:
a.若被删结点没有孩子:用空子树NULL代替即可。
b.若被删结点有一个孩子:用孩子代替即可。
c.若被删结点有两个孩子:找到其中序遍历下的直接后继,将其值复制,并删除该后继。
2) 二叉搜索树的高度:
对于n个结点的二叉搜索树:
最大高度为n,此时,按此顺序插入的元素的关键字值递增或者递减。
最小高度为log2n,将结点按关键字从小到大排列,二分搜索的过程中,按比较次数由小到大的顺序,插入结点可得最小高度。
3.平衡二叉树
任何一个结点的左右子树的高度相差不过1。
1)平衡因子:结点的左子树的高度减去右子树的高度。
平衡过程:插入、平衡、重组
a.插入:将新结点q按照平衡二叉搜索树的排序性质作为树叶插入,令其平衡因子为0,记下离q最近且平衡因子不为0的祖先结点s,若所有祖先结点的平衡因子均为0,令s指向根结点。
b.平衡:修改从s到q的路径上的结点的平衡因子,不修改s和q,对于其中的结点p,若q插在p的左子树,则平衡因子+1,否则-1。
c.重组:当s的平衡因子为0时,s为根结点,此时将s的平衡因子+1或者-1即可。
若s的平衡因子为-1,q插在s的左子树,或+1,插在右子树,只需改平衡因子为0。
若s的平衡因子为+1,q插在左子树,此时左重组,反之右重组。
2)重组方式:
a.若插入前,从根结点到新结点的插入位置的路径上,所有结点的平衡因子值均为0,则插入即可。
b.若插在结点较矮的子树上,则插入即可。
c. if(r->bf == 1)
{s->lchild = r->rchild; r->rchild = s;}//LL
d. u = r->rchild;
r->rchild = u->lchild;
u->lchild = r;
s->lchild = u-> rchild;
u->rchild = s;
4.散列表:
哈希表
散列搜索的搜索过程是按照关键字来计算元素可能的存储地址
确定一个函数,每个元素的关键字经由这一函数映射出一个函数值
这样的函数称作散列函数,得到的值为散列地址,函数的值域为散列地址空间
好的散列函数应该使得函数值尽可能均匀分布在散列地址空间
1)处理冲突:
a.开地址法(线性探测)
地址为i的单元发生冲突时,依次探测(i+1)%m,(i+2)%m…将元素插入第一个空单元。
即每次往后移动一个存储单元查找是否有空闲地址,允许循环,直到再次到达地址i或在此之前找到空闲地址。
缺点:n个被占用的单元连成一片时,后面的空单元被占用的可能性增加。
b.开地址法(双散列函数探测)
当i = h1(k)被占用时,
C = h2(k) 然后依次探测(i + c)%m (i + 2c)%m…
线性探测每次只移动一个存储单元,双散列函数探测法中,每次移动的大小由第二个散列函数决定。
缺点:由于寻找是跳跃性的,部分空单元可能总被跳过去,无法利用。
c.拉链法:将散列地址相同的元素链接起来,构成一个线性链表,将各链表的表头指针存入散列表。
2)装载密度=存入散列表的节点个数\散列地址空间大小。
3) 开地址法中,删除一个结点时,只能在删除的结点上做标记,不能真正删除,不然会影响其他的结点查找。
5.不同搜索方式的时间复杂度:
顺序搜索:查找和插入的时间复杂度:O(n)
二分查找:查找时间复杂度O(logn) 插入时间复杂度O(n)?存疑
二叉搜索树:查找时间复杂度O(logn)插入时间复杂度O(logn)
散列表法:O(1)
7第七单元 排序
1.内排序:元素个数不多,排序过程只用到内存。
2.排序码:作为排序依据的关键字(通常是次关键字)。
3.排序的稳定性:在线性表中可能存在多个排序码相同的元素,如果排序前后这些元素的相对位置有可能被改变,则这种排序方法为不稳定的排序方法。
4.简单排序:从所有元素中选出排序码最小的元素与第一个元素交换位置,从第一个元素之后的元素中选出排序码最小的元素与第二个元素交换位置,以此类推。
比较次数与初始排列顺序无关,为(n-1)加到1。
移动次数与初始排列顺序有关,最多移动3(n-1)次(赋值操作有3次,包括temp)。
算法执行时间为O(n2)。
算法所需空间大小为O(1)。
为不稳定排序。
5.直接插入排序:将第一个元素看成一个有序表,将第二个元素插入该表,再插入第三个,每次插入需要通过比较来决定元素间的相对位置。
比较次数,最少为n-1次,最多为n加到2。
移动次数最少为2(n-1)次,最多为n+1加到3。
算法执行平均时间为O(n2)。
算法执行时间最长为O(n2)。
算法所需空间大小为O(1)。
为稳定排序。
6.冒泡排序:先在n个元素间逐个比较,将较大的元素移到右边,一趟过后,最大的元素置于最右端,再在n-1个元素之间比较,以此类推,当某趟没有出现交换情况或i<2,结束排序。(设置swap记录是否有过交换)
比较次数,最少为n-1次(从小到大排列好时最少),最多为n-1加到1。
移动次数,最少不移动,最多为3(n-1加到1)(从大到小排列时最多)。
算法执行平均时间为O(n2)。
算法执行时间最坏为O(n2)。
算法所需空间大小为O(1)。
为稳定排序。
一直把直接插入排序当做冒泡排序了:
for(int i=0;i<n;++i) for(int j=i+1;j<n;++j)
上面是直接插入排序,比较的是a[i]与a[j],相当于每次寻找最小值插入数组前端
for(int i=0;i<n;++i) for(int j=n-1;j>i;--j) if(a[j]<a[j-1])
这才是冒泡排序,比较的是a[j]和a[j+1],相当于每次寻找最大值插入数组后端。因此可以记录swap,当某趟比较中swap为0,则可以提前结束排序。
7.快速排序:从n个元素中任取一个元素,将其余元素根据排序码大小分别置于该元素两边,再对两边以这种方式继续排序。
算法平均执行时间为O(nlog2n)。
最坏情况为O(n2)。
算法所需空间大小为O(log2n)。
为不稳定排序。
实现:数据最开始存放在数组的1~n空间内
以第一个元素为标准,将它置于0处,设置i=1,j=n
将j与标准元素相比较,如果j较大,则j-1(较大的数留在原处)
如果j较小,则将j与i交换(较小的数置于另一边)
交换之后,将i与标准元素比较,如果i较小,则i+1(较小的数留在原处)
如果i较大,则将i与j交换(较大的数置于另一边)
交换之后,继续将j与标准元素比较(从两边逐步比较,逼近中间)
i和j相遇之后,结束第一趟排序,将标准元素置于i和j所在的位置
C++代码实现:
//Asimple #include <stdio.h> //打印数组 void show(int *a, int n) { int i; for(i=0; i<n; i++) printf("%d ",a[i]); printf("\n"); } //找合适的位置 int Find(int *a, int low, int high) { int temp = a[low]; while(low < high) { while(low<high && a[high] >= temp) high -- ; a[low] = a[high] ; while(low<high && a[low] <= temp) low ++ ; a[high] = a[low] ; } a[low] = temp ; return low; } //快速排序 void Quick_sort(int *a, int low, int high) { int pos; if(low < high) { pos = Find(a,low,high); Quick_sort(a,low,pos-1); Quick_sort(a,pos+1,high); } } int main() { int a[1001], n, i; while(scanf("%d",&n)!=EOF) { for(i=0; i<n; i++) scanf("%d",&a[i]); Quick_sort(a,0,n-1); show(a,n); } return 0; }
8.归并排序:将n个元素视作n个有序表,将相邻的有序表归并,得到n/2个长度为2的有序表,以此类推,每次归并会进行内部的比较移动。
归并次数为log2n。
每次归并的比较次数不超过n-1。
移动次数都为n。
算法执行时间为O(nlog2n)。
算法所需空间大小为O(n)。
为稳定排序。
实现:设置i,j,k,i和j指向有序表的两端,k指向辅助空间,每次归并时,将i和j中较小的一个移入k中,并将移动的i或j移向中间,实现有序表的内部排序。
-
//将有二个有序数列a[first...mid]和a[mid...last]合并。 void mergearray(int a[], int first, int mid, int last, int temp[]) { int i = first, j = mid + 1; int m = mid, n = last; int k = 0; while (i <= m && j <= n) { if (a[i] <= a[j]) temp[k++] = a[i++]; else temp[k++] = a[j++]; } while (i <= m) temp[k++] = a[i++]; while (j <= n) temp[k++] = a[j++]; for (i = 0; i < k; i++) a[first + i] = temp[i]; } void mergesort(int a[], int first, int last, int temp[]) { if (first < last) { int mid = (first + last) / 2; mergesort(a, first, mid, temp); //左边有序 mergesort(a, mid + 1, last, temp); //右边有序 mergearray(a, first, mid, last, temp); //再将二个有序数列合并 } } bool MergeSort(int a[], int n) { int *p = new int[n]; if (p == NULL) return false; mergesort(a, 0, n - 1, p); delete[] p; return true; }
注:有的书上是在mergearray()合并有序数列时分配临时数组,但是过多的new操作会非常费时。因此作了下小小的变化。只在MergeSort()中new一个临时数组。后面的操作都共用这一个临时数组
快速排序与归并排序中判断条件都是left<right,二分查找中判断条件都是left<=right
9.堆排序:按堆的定义,将元素调整为堆,交换第一个元素和最后一个元素,再将n-1个元素调整为堆,再交换第一个元素和最后一个元素,以此类推。
算法执行时间为O(nlog2n)。
算法所需空间为O(1)。
为不稳定排序。
首先,按堆的定义将数组R[0..n]调整为堆(这个过程称为创建初始堆),交换R[0]和R[n];
然后,将R[0..n-1]调整为堆,交换R[0]和R[n-1];
如此反复,直到交换了R[0]和R[1]为止。
以上思想可归纳为两个操作:
(1)根据初始数组去构造初始堆(构建一个完全二叉树,保证所有的父结点都比它的孩子结点数值大)。
(2)每次交换第一个和最后一个元素,输出最后一个元素(最大值),然后把剩下元素重新调整为大根堆。
当输出完最后一个元素后,这个数组已经是按照从小到大的顺序排列了。
先通过详细的实例图来看一下,如何构建初始堆。
设有一个无序序列 { 1, 3, 4, 5, 2, 6, 9, 7, 8, 0 }。
构造了初始堆后,我们来看一下完整的堆排序处理:
还是针对前面提到的无序序列 { 1, 3, 4, 5, 2, 6, 9, 7, 8, 0 } 来加以说明。
相信,通过以上两幅图,应该能很直观的演示堆排序的操作处理。
核心代码
public void HeapAdjust(int[] array, int parent, int length) { int temp = array[parent]; // temp保存当前父节点 int child = 2 * parent + 1; // 先获得左孩子 while (child < length) { // 如果有右孩子结点,并且右孩子结点的值大于左孩子结点,则选取右孩子结点 if (child + 1 < length && array[child] < array[child + 1]) { child++; } // 如果父结点的值已经大于孩子结点的值,则直接结束 if (temp >= array[child]) break; // 把孩子结点的值赋给父结点 array[parent] = array[child]; // 选取孩子结点的左孩子结点,继续向下筛选 parent = child; child = 2 * child + 1; } array[parent] = temp; } public void heapSort(int[] list) { // 循环建立初始堆 for (int i = list.length / 2; i >= 0; i--) { HeapAdjust(list, i, list.length - 1); } // 进行n-1次循环,完成排序 for (int i = list.length - 1; i > 0; i--) { // 最后一个元素和第一元素进行交换 int temp = list[i]; list[i] = list[0]; list[0] = temp; // 筛选 R[0] 结点,得到i-1个结点的堆 HeapAdjust(list, 0, i); System.out.format("第 %d 趟: \t", list.length - i); printPart(list, 0, list.length - 1); } }

浙公网安备 33010602011771号