声明:本博客仅为本人学习途中做的笔记 采自青岛大学王卓老师的视频教学 主要内容为算法思路,具体代码实现还需修改后才能运行,望各位看官多多包涵,您的点赞与评论是对我最大的肯定!
数 据 结 构 { 数 据 的 逻 辑 结 构 { 线 性 结 构 { 线 性 表 栈 ( 特 殊 线 性 表 ) 队 列 ( 特 殊 线 性 表 ) 字 符 串 、 数 组 、 广 义 表 非 线 性 结 构 { 树 形 结 构 图 形 结 构 数 据 的 存 储 结 构 { 顺 序 存 储 链 式 结 构 数 据 的 运 算 : 插 入 、 删 除 、 修 改 、 查 找 、 排 序 等 数据结构 \begin{cases} 数据的逻辑结构\begin{cases} 线性结构\begin{cases}线性表\\栈(特殊线性表)\\队列(特殊线性表)\\字符串、数组、广义表\end{cases} \\非线性结构\begin{cases}树形结构\\图形结构\end{cases} \end{cases} \\ 数据的存储结构\begin{cases} 顺序存储\\链式结构 \end{cases} \\ 数据的运算:插入、删除、修改、查找、排序等 \end{cases} 数据结构⎩⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎧数据的逻辑结构⎩⎪⎪⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎪⎪⎧线性结构⎩⎪⎪⎪⎨⎪⎪⎪⎧线性表栈(特殊线性表)队列(特殊线性表)字符串、数组、广义表非线性结构{树形结构图形结构数据的存储结构{顺序存储链式结构数据的运算:插入、删除、修改、查找、排序等
什么是排序?
排序:将一组杂乱无章的数据按一定规律顺次排列起来。
即,将一个无序序列排成一个有序序列(从小到大或从大到小的运算)。
如果参加排序的数据结点包含多个数据域,那么往往是针对其中某个域而言。
排序方法的分类:
按数据存储介质:内部排序和外部排序
按比较器个数:串行排序和并行排序
按主要操作:比较排序和基数排序
按辅助空间:原地排序和非原地排序
按稳定性:稳定排序和非稳定排序
按自然性:自然排序和非自然排序
按存储介质可分为:
内部排序:数据量不大、数据在内存,无需内外存交换数据
外部排序:数据量较大、数据在外存(文件排序)
外部排序时,要将数据分批调入内存来排序,中间结果还要及时放入外存,显然外部排序要复杂得多。
按比较器个数可分为:
串行排序:单处理机(同一时刻比较一对元素)
并行排序:多处理机(同一时刻比较多对元素)
按主要操作可分为:
比较排序:用比较的方法
插入排序、交换排序、选择排序、归并排序
基数排序:不比较元素的大小,仅仅根据元素本身的取值确定其有序位置
按辅助空间可分为:
原地排序:辅助空间用量为O(1)的排序方法。
(所占的辅助存储空间与参加排序的数据量大小无关)
非原地排序:辅助空间用量超过O(1)的排序方法。
按稳定性可分为:
稳定排序:能够使任何数值相等的元素,排序以后相对次序不变。
非稳定排序:不是稳定排序的方法。
(排序的稳定性只对结构类型数据有意义。)
按自然性可分为:
自然排序:输入数据越有序,排序的速度越快的方法。
非自然排序:不是自然排序的方法。
重点学习内容:
按数据存储介质:内部排序和外部排序
按比较器个数:串行排序和并行排序
按主要操作:比较排序和基数排序
按排序依据原则:
插入排序:直接插入排序、折半插入排序、希尔排序
交换排序:冒泡排序、快速排序
选择排序:简单选择排序、堆排序
归并排序:2-路由并排序
基数排序
存储结构–记录序列以顺序表存储
#define MAXSIZE 20 //设记录不超过20个
typedef int KeyType; //设关键字为整型量(int型)
Typedef struct { //定义每个记录(数据元素的结构)
KeyType key; //关键字
InfoType otherinfo; //其他数据项
}RedType; //Record Type
Typedef struct { //定义顺序表的结构
RedType r[MAXSIZE+1];//存储顺序表的向量
//r[0]一般作哨兵或缓冲区
int length; //顺序表的长度
}SqList
基本思想:
每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到全部插入为止。
基本操作:有序插入
在有序序列中插入一个元素,保持序列有序,有序长度不断增加。
起初,a[0]是长度为1的子序列。然后,逐一将a[1]至a[n-1]插入到有序子序列中。
有序插入方法:
在插a[i]前,数组a的前半段(a[0]-a[i-1])是有序段,后半段(a[i]-a[n-1])是停留于输入次序的“无序段”。
插入a[i]使a[0]~a[i-1]有序,也就是要为a[i]找到有序位置j (0<=j<=i) ,将a[i]插入在a[j]的位置上。
a[j]比待插入元素大则后移,直到找到比待插入元素小的位置,后一位就是要插入的位置;或者找到尽头依然没有比待插入元素小的元素,则将待插入元素置为首位
void lnsertSort( SqList &L ) {
int i, j;
for (i=2; i<=L.length; ++i) {
if (L.r[i].key < L.r[i-1].key){
//若"<",需将L.r[i]插入有序子表
L.r[0]=L.r[i];//复制为哨兵
for(j=i-1;L.r[0].key<L.r[j].key;--j ){
L.r[j+1]=L.r[j];//记录后移
}
L.r[j+1]=L.r[O];//插入到正确位置
}
}
直接插入排序–性能分析
实现排序的基本操作有两个:
(1)"比较"序列中两个关键字的大小
(2)"移动"记录。
时间复杂度结论
原始数据越接近有序,排序速度越快
最坏情况下(输入数据是逆有序的) Tw(n)=O(n 2 ^2 2)
平均情况下,耗时差不多是最坏情况的一半 Te(n)=O(n 2 ^2 2)
要提高查找速度
减少元素的比较次数
减少元素的移动次数
void BlnsertSort ( SqList &L ) {
//依次插入第2~第n个元素
for ( i = 2; i<= L.length ; ++i ){
L.r[0]=L.r[i];//当前插入元素存到“哨兵”位置
low=1;high=i-1;//采用二分查找法查找插入位置
while(low<=high){
mid=(low+high)/2;
if(L.r[0].key<L.r[mid].key)
high=mid-1;
else low=mid+1;
}//循环结束,high+1则为插入位置
for(j=i-1;j>=high+1;--j)
L.r[j+1]=L.r[j];//移动元素
L.r[high+1] = L.r[O];//插入到正确位置
}
}// BInsertSort
折半插入排序–算法分析
折半查找比顺序查找快,所以折半插入排序就平均性能来说比直接插入排序要快;
它所需要的关键码比较次数与待排序对象序列的初始排列无关,仅依赖于对象个数。在插入第i个对象时,需要经过[ log 2 i \log_2i log2i]+1次关键码比较,才能确定它应插入的位置;
折半插入排序的对象移动次数与直接插入排序相同,依赖于对象的初始排列。
基本思想:
先将整个待排记录序列分割成若干子序列,分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接
插入排序。
希尔排序算法特点:
(1)缩小增量
(2)多遍插入排序
五间隔的三个元素先进行排序,然后其后一位的元素进行排序,重复进行。做完一次插入排序后可以使数据元素大大变得有序
希尔排序思路:
1.定义增量序列Dx : D M _M M>D M − 1 _{M-1} M−1>.…>D 1 _1 1=1
刚才的例子中:D 3 _3 3=5,D 2 _2 2=3,D 1 _1 1=1
2.对每个D k _k k进行“Dx-间隔”插入排序(k=M,M-1,…1)
希尔排序特点
一次移动,移动位置较大,跳跃式地接近排序后的最终位置
最后一次只需要少量移动
增量序列必须是递减的,最后一个必须是1
增量序列应该是互质的
希尔排序算法
主程序:
//dk值依次存放在dlta[t]中
void ShellSort (Sqlist &L, int dlta[], int t){
//按增量序列dlta[0..t-1]对顺序表L作希尔排序。
for(k=0; k<t; ++k)
Shelllnsert(L, dlta[k]);
//一趟增量为dlta[k]的插入排序
}/ /ShellSort
其中某一趟的排序操作:
void Shelllnsert(SqList &L, int dk){
//对顺序表L进行一趟增量为dk的Shell排序,dk为步长因子
for(i=dk+1;i<=L.length;++i)
if(r[i].key < r[i-dk].key){
r[0]=r[i];
for(j=i-dk;j>0&&(r[0].key<r[j].key); j=j-dk)
r[j+dk]=r[j];
r[j+dk]=r[0];
}
}
希尔排序算法的稳定性
时间复杂度是n和d的函数:
O(n 1.25 ^{1.25} 1.25) ~O (1.6n 1.25 ^{1.25} 1.25)一经验公式
空间复杂度为O(T)
是一种不稳定的排序方法
√如何选择最佳d序列,目前尚未解决
√最后一个增量值必须为1,无除了1之外的公因子
√不宜在链式存储结构上实现
基本思想:每趟不断将记录两两比较,并按"前小后大"规则交换
冒泡排序算法
void bubble_sort(SqList &L){//冒泡排序算法
int m,ij; RedType x;
//交换时临时存储
for(m=1; m<=n-1; m++){//总共需m趟
for(j=1; j<=n-m; j++)
if(L.r[].key>L.r[j+1].key){//发生逆序
x=L.r[j];L.r[j]=L.r[j+1];
L.r[j+1]=x;//交换
}//endif
}//for
}
优点:每趟结束时,不仅能挤出一个最大值到最后面位置,还能同时部分理顺其他元素;
如何提高效率?
一旦某一趟比较时不出现记录交换,说明已排好序了,就可以结束本算法。
改进的冒泡排序算法
void bubble_sort(SqList &L){//改进的冒泡排序算法
int m,ij,flag=1; RedType x;
//flag作为是否有交换的标记
for(m=1; m<=n-1&&flag==1; m++){
flag=0;
for(j=1; j<=m; j++)
if(L.r[].key>L.r[j+1].key){//发生逆序
flag=1;
//发生交换,flag置为1,
//若本趟没发生交换,flag保持为0
x=L.r[j];L.r[j]=L.r[j+1];
L.r[j+1]=x;//交换
}//endif
}//for
}
冒泡排序算法评价
冒泡排序最好时间复杂度是O(n)
冒泡排序最坏时间复杂度为O(n2)
冒泡排序平均时间复杂度为O(n2)
冒泡排序算法中增加一个辅助空间temp,辅助空间为S(n)=O(1)
冒泡排序是稳定的
-----改进的交换排序
基本思想
基本思想:通过一趟排序,将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录进行排序,以达到整个序列有序。
具体实现:选定一个中间数作为参考,所有元素与之比较,小的调到其左
边,大的调到其右边。
(枢轴)中间数:可以是第一个数、最后一个数、最中间一个数、任选一个数等。
例:
1.将49作为中心点放到数组下标为0处,high与low分别指向数组元素起始与末尾处
2.high向前移动,若元素比中心点小,则将该元素移到low的位置
3.low向后移动并进行比较,当元素大于中心点时将该元素移到high位置
4.重复high与low的操作,直到low与high指向同一区域。当low=high时,区间内没有数据元素,将中心点放在该位置。
找到中心点位置后将之前的表划分为两个子表,分别进行排序操作。
特点:
①每一趟的子表的形成是采用从两头向中间交替式逼近法;
②由于每趟中对各子表的操作都相似,可采用递归算法。
快速排序算法
void main(){
QSort(L, 1,L.length);
}
void QSort (SqList &L, int low, int high){//对顺序表L快速排序
if (low < high){//长度大于1
pivotloc=Partition(L,low,high);
//将L.r[low..high]一分为二
//pivotloc为枢轴元素排好序的位置
QSort(L,low,pivotloc-1); //对低子表递归排序
QSort(L,pivotloc+1,high);//对高子表递归排序
}//endif
}// QSort
//找中心点位置
int Partition(SqList &L,int low,int high){
L.r[0]=L.r[low]; pivotkey=L.r[low].key;
while(low<high){
while(low<high&&L.r[high].key>=pivotkey)
--high;
L.r[low] = L.r[high];
while(low<high&&L.r[low].key<=pivotkey)
++low;
L.r[high]=L.r[low];
}
L.r[low]=L.r[0];
return low;
}
快速排序不适于对原本有序或基本有序的记录序列进行排序。
基本思想:在待排序的数据中选出最大(小)的元素放在其最终的位置。
基本操作:
1.首先通过n-1次关键字比较,从n个记录中找出关键字最小的记录,将
它与第一个记录交换
2.再通过n-2次比较,从剩余的n-1个记录中找出关键字次小的记录,将
它与第二个记录交换
3.重复上述操作,共进行n-1趟排序后,排序结束
void SelectSort(SqList &K){
for (i=1; i<L.length; ++i){
k=i;
for(j=i+1j<=L.length;j++)
if(L.r[j].key<L.r[k].key)
k=j;//记录最小值位置
if(k!=i) L.r[i]<-->L.r[k];//交换
}
}
堆的定义
堆排序
若在输出堆顶的最小值(最大值)后,使得剩余n-1个元素的序列重又建成一个堆,则得到n个元素的次小值(次大值)……如此反复,便能得到一个有序序列,这个过程称之为堆排序。
堆的调整
如何在输出堆顶元素后,调整剩余元素为一个新的堆?
小根堆:
1.输出堆顶元素之后,以堆中最后一个元素替代之;
2.然后将根结点值与左、右子树的根结点值进行比较,并与其中小者进行交换;
3.重复上述操作,直至叶子结点,将得到新的堆,称这个从堆顶至叶子的调整过程为“筛选”
void HeapAdjust (elem R[ ], int s, int m) {
/*已知R[s..m]中记录的关键字除R[s]之外均满足堆的定义,本函数调整R[s]的关键字,使R[s..m]成为一个大根堆*/
rc = R[s];
for ( j=2*s; j<=m; j*=2){
//沿key较大的孩子结点向下筛选
if (j<m && R[j]< R[j+1]) ++j;
//j为key较大的记录的下标
if ( rc >= R[j] ) break;
R[s] = R[j];s = j;//rc应插入在位置s上
}//for
R[s] = rc; //插入
}// HeapAdjust
可以看出:
对一个无序序列反复“筛选”就可以得到一个堆;
即:从一个无序序列建堆的过程就是一个反复“筛选”的过程。
那么 ,如何从一个无序序列建成一个堆?
显然:
单结点的二叉树是堆;
在完全二叉树中所有以叶子结点(序号i > n/2)为根的子树是堆。
这样,我们只需依次将以序号为n/2,n/2 - 1,…1的结点为根的子树均调整为堆即可。
前一个结点49,左右孩子13较小,与13置换
继续调整右子树,将49与较小的27交换
小根堆建立完毕
将初始无序的R[1]到R[n]建成一个小根堆,可用以下语句实现:
for(i=n/2;i>=1;i--)
HeapAdjust(R,i,n);
由以上分析知:
若对一个无序序列建堆,然后输出根;重复该过程就可以由一个无序序列输出有序序列。
实质上,堆排序就是利用完全二叉树中父结点与孩子结点之间的内在关系来排序的。
堆排序算法如下:
void HeapSort(elemR []){//对R[1]到R[n]进行堆排序
int i;
for(i=n/2;i>=1;i--)
HeapAdjust(R,i,n); //建初始堆
for(i=n;i>1;i--){//进行n - 1趟排序
Swap(R[1],R[i]); //根与最后一个元素交换
HeapAdjust(R,1,i-1);//对R[1]到R[i-1]重新建堆
}
} //HeapSort
基本思想:将两个或两个以上的有序子序列"归并”为一一个有序予外。
在内部排序中,通常采用的是2-路归并排序。
即:将两个位置相邻的有序子序列R[/…m]和R[m+1…n]归并为一个有序序列R[/…n]
归并排序示例
基本思想:分配+收集
也叫桶排序或箱排序:设置若干个箱子,将关键字为k的记录放入第k个箱
子,然后在按序号将非空的连接。
基数排序:数字是有范围的,均由0-9这十个数字组成,则只需设置十个
箱子,相继按个、十、百…进行排序.
一、时间性能
1.按平均的时间性能来分,有三类排序方法:
时间复杂度为O(nlogn)的方法有:
快速排序、堆排序和归并排序,其中以快速排序为最好;
时间复杂度为O(n 2 ^2 2)的有:
直接插入排序、(冒泡排序和简单选择排序,其中以直接插入为最好,特别是对那些对关键字近似有序的记录序列尤为如此;
时间复杂度为O(n)的排序方法只有:基数排序。
2.当待排记录序列按关键字顺序有序时,直接插入排序和冒泡排序能达到O(n)的时间复杂度;而对于快速排序而言,这是最不好的情况,此时的时间
性能退化为O(n 2 ^2 2),因此是应该尽量避免的情况。
3.简单选择排序、堆排序和归并排序的时间性能不随记录序列中关键字的
分布而改变。
二、空间性能
指的是排序过程中所需的辅助空间大小
1.所有的简单排序方法(包括:直接插入、冒泡和简单选择)和堆排序的空
间复杂度为O(1)
2.快速排序为O(logn),为栈所需的辅助空间
3.归并排序所需辅助空间最多,其空间复杂度为O(n)
4.链式基数排序需附设队列首尾指针,则空间复杂度为O(rd)
三、排序方法的稳定性能
稳定的排序方法指的是,对于两个关键字相等的记录,它们在序列中的相
对位置,在排序之前和经过排序之后,没有改变。
当对多关键字的记录序列进行LSD方法排序时,必须采用稳定的排序方法。
对于不稳定的排序方法,只要能举出一个实例说明即可。
快速排序和堆排序是不稳定的排序方法。
四、关于“排序方法的时间复杂度的下限
本章讨论的各种排序方法,除基数排序外,其它方法都是基于“比较关键
字”进行排序的排序方法,可以证明,这类排序法可能达到的最快的时间
复杂度为O(nlogn)。
(基数排序不是基于“比较关键字”的排序方法,所以它不受这个限制)。
可以用一棵判定树来描述这类基于“比较关键字"进行排序的排序方法。
未完待续…