一。排序

1.排序定义:

将一组杂乱无章的数据按一定规律顺次排列起来。即,将无序序列排成一个有序序列(由小到大或由大到小)的运算。

2.排序的分类

(1)按数据存储介质;内部排序和外部排序

内部排序:数据量不大、数据在内存,无需内外存交换数据

外部排序:数据量较大、数据在外存(文件排序)

外部排序时,要将数据分批调入内存来排序,中间结果还要及时放入外存,显然外部排序要复杂得多。


(2)按比较器个数:串行排序和并行排序

串行排序: (单处理机) (同一时刻比较一对元素)

并行排序:(多处理机)(同一时刻比较多对元素)


(3)按主要操作:比较排序和基数排序

比较排序:用比较的方法

插入排序、交换排序、选择排序、归并排序

基数排序:不比较元素的大小,仅仅根据元素本身的取值确定其有序位置。


(4)按辅助空间:原地排序和非原地排序

原地排序:辅助空间用量为O(1)的排序方法。(所占的辅助存储空间与参加排序的数据量大小无关)

非原地排序:辅助空间用量超过O(1)的排序方法。


(5)按稳定性:稳定排序和非稳定排序

稳定排序:能够使任何数值相等的元素,排序以后相对次序不变。

排序的稳定性只对结构类型数据排序有意义。

非稳定性排序:不是稳定排序的方法。

排序方法是否稳定,并不能衡量一一个排序算法的优劣。


(6)按自然性:自然排序和非自然排序

自然排序:输入数据越有序,排序的速度越快的排序方法。

非自然排序:不是自然排序的方法。


二。插入排序

1.基本思想:

每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止。

即边插入边排序,保证子序列中随时都是排好序的

2.直接插入排序​--采用顺序查找法查找插入位置

void print(int a[], int n, int i){
printf("%d:", i);
for (int j = 0; j<n; j++){
printf("%d", a[j]);
}
printf("\n");
}

//直接插入排序函数
void InsertSort(int a[], int n)
{
for (int i = 1; i<n; i++){
if (a[i] < a[i - 1]){//若第 i 个元素大于 i-1 元素则直接插入;反之,需要找到适当的插入位置后在插入。
int j = i - 1;
int x = a[i];
while (j>-1 && x < a[j]){ //采用顺序查找方式找到插入的位置,在查找的同时,将数组中的元素进行后移操作,给插入元素腾出空间
a[j + 1] = a[j];
j--;
}
a[j + 1] = x; //插入到正确位置
}
print(a, n, i);//打印每次排序后的结果
}
}

int main(){
int a[8] = { 3, 1, 7, 5, 2, 4, 9, 6 };
InsertSort(a, 8);
return 0;
}



时间复杂度结论

原始数据越接近有序,排序速度越快

最坏情况下(输入数据是逆有序的)Tw(n)=O(n2)

■平均情况下,耗时差不多是最坏情况的一半Te(n)=O(n2”要提高查找速度

  • 减少元素的比较次数
  • 减少元素的移动次数

3.折半插入排序

//折半插入算法
void BInsertSort(int a[], int size){
int i, j, low = 0, high = 0, mid;
int temp = 0;
for (i = 1; i<size; i++) {
low = 0;
high = i - 1;
temp = a[i];
//采用折半查找法判断插入位置,最终变量 low 表示插入位置
while (low <= high) {
mid = (low + high) / 2;
if (a[mid]>temp) {
high = mid - 1;
}
else{
low = mid + 1;
}
}
//有序表中插入位置后的元素统一后移
for (j = i; j>low; j--) {
a[j] = a[j - 1];
}
a[low] = temp;//插入元素
print(a, 8, i);
}

}




​折半插入排序算法相比较于直接插入排序算法,只是减少了关键字间的比较次数,而记录的移动次数没有进行优化,所以该算法的​时间复杂度为0(n2)空间复杂度为0(1)​是一种稳定的排序方法

4.希尔排序

基本思想:

先将整个待排记录序列分割成若干子序列,分别进行直接插入排序,待整个序列中的记录"基本有序”时,再对全体记录进行一次直接插入排序。数据结构 - 排序_排序方法

希尔排序特点:

■一次移动,移动位置较大,跳跃式地接近排序后的最终位置

■最后一次只需要少量移动

■增量序列必须是递减的,最后一个必须是1

增量序列应该是互为质数的(以减少重复比较)


算法分析:

希尔排序算法效率与增量序列的取值有关

●时间复杂度是n和d的函数:

O(n1.25) ~O (1.6n1.25) 一一 经验公式

●空间复杂度为O(1)

●是一种不稳定的排序方法

  • 如何选择最佳d序列,目前尚未解决
  • 最后一个增量值必须为1,无除了1之外的公因子
  • 不宜在链式存储结构上实现

三。交换排序

1.基本思想

两两比较,如果发生逆序则交换,直到所有记录都排好序为止。

2.冒泡排序

每趟不断将记录两两比较,并按“前小后大”规则交换

数据结构 - 排序_结点_02

#include <stdio.h>
//交换 a 和 b 的位置的函数
void swap(int *a, int *b);
int main()
{
int array[8] = {49,38,65,97,76,13,27,49};
int i, j;
int key;
//有多少记录,就需要多少次冒泡,当比较过程,所有记录都按照升序排列时,排序结束
for (i = 1; i < 8; i++){
key=0;//每次开始冒泡前,初始化 key 值为 0
//每次起泡从下标为 0 开始,到 8-i 结束
for (j = 0; j<8-i; j++){
if (array[j] > array[j+1]){
key=1;
swap(&array[j], &array[j+1]);
}
}
//如果 key 值为 0,表明表中记录排序完成
if (key==0) {
break;
}
}
for (i = 0; i < 8; i++){
printf("%d ", array[i]);
}
return 0;
}
void swap(int *a, int *b){
int temp;
temp = *a;
*a = *b;
*b = temp;
}


优点:每趟结束时,不仅能挤出一个最大值到最后面位置,还能同时部分地理顺其他元素;

冒泡排序的算法评价

■冒泡排序最好时间复杂度是O(n)

冒泡排序最坏时间复杂度为O(n2)

冒泡排序平均时间复杂度为O(n2)

冒泡排序算法中增加一个辅助空间temp,辅助空间为S(n)=O(1)

■冒泡排序是稳定的

3.快速排序

基本思想:

●任取一个元素(如:第一个)为中心

●所有比它小的元素一律前放,比它大的元素一律后放,形成左右两个子表;

●对各子表重新选择中心元素并依此规则调整,直到每个子表的元素只剩一个

①每一趟的子表的形成是采用从两头向中间交替式逼近法;

②由于每趟中对各子表的操作都相似,可采用递归算法。

#define MAX 9
//单个记录的结构体
typedef struct {
int key;

}SqNote;
//记录表的结构体
typedef struct {
SqNote r[MAX];
int length;

}SqList;
//此方法中,存储记录的数组中,下标为 0 的位置时空着的,不放任何记录,记录从下标为 1 处开始依次存放
int Partition(SqList *L, int low, int high){
L->r[0] = L->r[low];
int pivotkey = L->r[low].key;
//直到两指针相遇,程序结束
while (low<high) {
//high指针左移,直至遇到比pivotkey值小的记录,指针停止移动
while (low<high && L->r[high].key >= pivotkey) {
high--;

}
//直接将high指向的小于支点的记录移动到low指针的位置。
L->r[low] = L->r[high];
//low 指针右移,直至遇到比pivotkey值大的记录,指针停止移动
while (low<high && L->r[low].key <= pivotkey) {
low++;

}
//直接将low指向的大于支点的记录移动到high指针的位置
L->r[high] = L->r[low];

}
//将支点添加到准确的位置
L->r[low] = L->r[0];
return low;

}
void QSort(SqList *L, int low, int high){
if (low<high) {
//找到支点的位置
int pivotloc = Partition(L, low, high);
//对支点左侧的子表进行排序
QSort(L, low, pivotloc - 1);
//对支点右侧的子表进行排序
QSort(L, pivotloc + 1, high);

}

}
void QuickSort(SqList *L){
QSort(L, 1, L->length);

}
int main() {
SqList * L = (SqList*)malloc(sizeof(SqList));
L->length = 8;
L->r[1].key = 49;
L->r[2].key = 38;
L->r[3].key = 65;
L->r[4].key = 97;
L->r[5].key = 76;
L->r[6].key = 13;
L->r[7].key = 27;
L->r[8].key = 49;
QuickSort(L);
for (int i = 1; i <= L->length; i++) {
printf("%d ", L->r[i].key);

}
return 0;

}

快速排序算法的时间复杂度为O(nlog2n) 空间复杂度最坏为O(n)

稳定性:

由于每次枢轴记录的关键字都是大于其它所有记录的关键字,致使一次划分之后得到的子序列(1)的长度为0,这时已经退化成为没有改进措施的冒泡排序。

快速排序不适于对原本有序或基本有序的记录序列进行排序。


快速排序算法分析

  • 划分元素的选取是影响时间性能的关键
  • 输入数据次序越乱,所选划分元素值的随机性越好,排序速度越快,快速排序不是自然排序方法。
  • 改变划分元素的选取方法,至多只能改变算法平均情况的下的世界性能,无法改变最坏情况下的时间性能。即最坏情况下,快速排序的时间复杂性总是O(n2)

四。选择排序

1.简单选择排序

基本思想:在待排序的数据中选出最大(小)的元素放在其最终的位置。

基本操作:

1.首先通过n-1次关键字比较,从n个记录中找出关键字最小的记录,将它与第一个记录交换

2.再通过n-2次比较,从剩余的n-1个记录中找出关键字次小的记录,将它与第二个记录交换

3.重复上述操作,共进行n-1趟排序后,排序结束


#define MAX 9
//单个记录的结构体
typedef struct {
int key;
}SqNote;
//记录表的结构体
typedef struct {
SqNote r[MAX];
int length;
}SqList;
//交换两个记录的位置
void swap(SqNote *a, SqNote *b){
int key = a->key;
a->key = b->key;
b->key = key;
}
//查找表中关键字的最小值
int SelectMinKey(SqList *L, int i){
int min = i;
//从下标为 i+1 开始,一直遍历至最后一个关键字,找到最小值所在的位置
while (i + 1<L->length) {
if (L->r[min].key>L->r[i + 1].key) {
min = i + 1;
}
i++;
}
return min;
}
//简单选择排序算法实现函数
void SelectSort(SqList * L){
for (int i = 0; i<L->length; i++) {
//查找第 i 的位置所要放置的最小值的位置
int j = SelectMinKey(L, i);
//如果 j 和 i 不相等,说明最小值不在下标为 i 的位置,需要交换
if (i != j) {
swap(&(L->r[i]), &(L->r[j]));
}
}
}

int main() {
SqList * L = (SqList*)malloc(sizeof(SqList));
L->length = 8;
L->r[0].key = 49;
L->r[1].key = 38;
L->r[2].key = 65;
L->r[3].key = 97;
L->r[4].key = 76;
L->r[5].key = 13;
L->r[6].key = 27;
L->r[7].key = 49;
SelectSort(L);
for (int i = 0; i<L->length; i++) {
printf("%d ", L->r[i].key);
}
return 0;
}


2.堆排序

数据结构 - 排序_结点_03

从上面堆的定义可以看出,堆实质是满足如下性质的完全二叉树:

二叉树中任一非叶子结点均小于(大于)它的孩子结点。

数据结构 - 排序_排序方法_04

基本操作:

若在输出堆顶的最小值(最大值)后,使得剩余n-1个元素的序列重又建成一个堆,则得到n个元素的次小值(次大值) ....如此反复, 便能得到一个有序序列,这个过程称之为堆排序。

堆的调整

小根堆的调整:

1.输出堆顶元素之后,以堆中最后一个元素替代之;

2.然后将根结点值与左、右子树的根结点值进行比较,并与其中小者进行交换;

3.重复上述操作,直至叶子结点,将得到新的堆,称这个从堆顶至叶子的调整过程为”筛选“

堆的建立

由于堆实质上是一个线形表,那么我们可以顺序存储一个堆。

数据结构 - 排序_结点_05

#include <stdio.h>
#include <stdlib.h>
#define MAX 9
//单个记录的结构体
typedef struct {
int key;
}SqNote;
//记录表的结构体
typedef struct {
SqNote r[MAX];
int length;
}SqList;
//将以 r[s]为根结点的子树构成堆,堆中每个根结点的值都比其孩子结点的值大
void HeapAdjust(SqList * H, int s, int m){
SqNote rc = H->r[s];//先对操作位置上的结点数据进行保存,放置后序移动元素丢失。
//对于第 s 个结点,筛选一直到叶子结点结束
for (int j = 2 * s; j <= m; j *= 2) {
//找到值最大的孩子结点
if (j + 1<m && (H->r[j].key<H->r[j + 1].key)) {
j++;
}
//如果当前结点比最大的孩子结点的值还大,则不需要对此结点进行筛选,直接略过
if (!(rc.key<H->r[j].key)) {
break;
}
//如果当前结点的值比孩子结点中最大的值小,则将最大的值移至该结点,由于 rc 记录着该结点的值,所以该结点的值不会丢失
H->r[s] = H->r[j];
s = j;//s相当于指针的作用,指向其孩子结点,继续进行筛选
}
H->r[s] = rc;//最终需将rc的值添加到正确的位置
}
//交换两个记录的位置
void swap(SqNote *a, SqNote *b){
int key = a->key;
a->key = b->key;
b->key = key;
}
void HeapSort(SqList *H){
//构建堆的过程
for (int i = H->length / 2; i>0; i--) {
//对于有孩子结点的根结点进行筛选
HeapAdjust(H, i, H->length);
}
//通过不断地筛选出最大值,同时不断地进行筛选剩余元素
for (int i = H->length; i>1; i--) {
//交换过程,即为将选出的最大值进行保存大表的最后,同时用最后位置上的元素进行替换,为下一次筛选做准备
swap(&(H->r[1]), &(H->r[i]));
//进行筛选次最大值的工作
HeapAdjust(H, 1, i - 1);
}
}

int main() {
SqList * L = (SqList*)malloc(sizeof(SqList));
L->length = 8;
L->r[1].key = 49;
L->r[2].key = 38;
L->r[3].key = 65;
L->r[4].key = 97;
L->r[5].key = 76;
L->r[6].key = 13;
L->r[7].key = 27;
L->r[8].key = 49;
HeapSort(L);
for (int i = 1; i <= L->length; i++) {
printf("%d ", L->r[i].key);
}
return 0;
}

算法性能分析

■堆排序的时间主要耗费在建初始堆和调整建新堆时进行的反复筛选上。堆排序在最坏情况下,其时间复杂度也为O(nlog2n),这是 堆排序的最大优点。无论待排序列中的记录是正序还是逆序排列,都不会使堆排序处于“最好”或"最坏"的状态。

另外,堆排序仅需一个记录大小供交换用的辅助存储空间。

然而堆排序是一种不稳定的排序方法, 它不适用于待排序记录个数n较少的情况,但对于n较大的文件还是很有效的。

五。归并排序

基本思想:将两个或两个以上的有序子序列“归并”为一个有序序列。在内部排序中,通常采用的是2-路归并排序。


#include <stdio.h>
#include <stdlib.h>
#define MAX 8
typedef struct{
int key;

}SqNode;
typedef struct{
SqNode r[MAX];
int length;

}SqList;
//SR中的记录分成两部分:下标从 i 至 m 有序,从 m+1 至 n 也有序,此函数的功能是合二为一至TR数组中,使整个记录表有序
void Merge(SqNode SR[], SqNode TR[], int i, int m, int n){
int j, k;
//将SR数组中的两部分记录按照从小到大的顺序添加至TR数组中
for (j = m + 1, k = i; i <= m && j <= n; k++) {
if (SR[i].key<SR[j].key) {
TR[k] = SR[i++];

}
else{
TR[k] = SR[j++];

}

}
//将剩余的比目前TR数组中都大的记录复制到TR数组的最后位置
while (i <= m) {
TR[k++] = SR[i++];

}
while (j <= n) {
TR[k++] = SR[j++];

}

}

void MSort(SqNode SR[], SqNode TR1[], int s, int t){
SqNode TR2[MAX];
//递归的出口
if (s == t) {
TR1[s] = SR[s];

}
else{
int m = (s + t) / 2;//每次递归将记录表中记录平分,直至每个记录各成一张表
MSort(SR, TR2, s, m);//将分开的前半部分表中的记录进行排序
MSort(SR, TR2, m + 1, t);//将后半部分表中的记录进行归并排序
Merge(TR2, TR1, s, m, t);//最后将前半部分和后半部分中的记录统一进行排序

}

}
//归并排序
void MergeSort(SqList *L){
MSort(L->r, L->r, 1, L->length);

}

int main() {
SqList * L = (SqList*)malloc(sizeof(SqList));
L->length = 7;
L->r[1].key = 49;
L->r[2].key = 38;
L->r[3].key = 65;
L->r[4].key = 97;
L->r[5].key = 76;
L->r[6].key = 13;
L->r[7].key = 27;
MergeSort(L);
for (int i = 1; i <= L->length; i++)
{
printf("%d ", L->r[i].key);

}
return 0;

}

算法性能分析

归并排序算法的时间复杂度为O(nlogn).该算法相比于堆排序和快速排序,主要的优点是:当记录表中含有值相同的记录时,排序前和排序后在表中的相对位置不会改变。


六。基数排序

基本思想:分配+收集也叫桶排序或箱排序:设置若干个箱子,将关键字为的记录放入第k个箱子,然后在按序号将非空的连接。

基数排序: (数字是有范围的,均由0-9这十个数字组成,则只需设置十个箱子,相继按个、十、..进行排序.


算法分析

时间效率: O(k*(n+m))

n:数据量

k:关键字个数

m:关键字取值范围为m个值(桶数)

空间效率: O(n+m)

稳定性:稳定


数据结构 - 排序_sql_06

七。各种排序方法的综合比较

一、时间性能

1.按平均的时间性能来分,有三类排序方法:

”时间复杂度为O(nlogn)的方法有:

快速排序、堆排序和归并排序,其中以快速排序为最好;

时间复杂度为O(n2)的有:

直接插入排序、冒泡排序和简单选择排序,其中以直接插入为最好,特别是对那些对关键字近似有序的记录序列尤为如此;

时间复杂度为O(n)的排序方法只有:基数排序。

2.当待排记录序列按关键字顺序有序时,直接插入排序和冒泡排序能达到

O(n)的时间复杂度;而对于快速排序而言,这是最不好的情况,此时的时间

性能退化为O(n2),因此是应该尽量避免的情况。

3.简单选择排序、堆排序和归并排序的时间性能不随记录序列中关键字的

分布而改变。

二、空间性能

指的是排序过程中所需的辅助空间大小

1.所有的简单排序方法(包括:直接插入、冒泡和简单选择)和堆排序的空间复杂度为O(1)

2.快速排序为O(logn),为栈所需的辅助空间

3.归并排序所需辅助空间最多,其空间复杂度为O(n)

4.链式基数排序需附设队列首尾指针,则空间复杂度为O(rd)

三、排序方法的稳定性能

  • 稳定的排序方法指的是,对于两个关键字相等的记录,它们在序列中的相对位置,在排序之前和经过排序之后,没有改变。
  • 当对多关键字的记录序列进行LSD方法排序时,必须采用稳定的排序方法。
  • ”对于不稳定的排序方法,只要能举出一一个实例说明即可。
  • 简单选择排序(可优化),快速排序和堆排序是不稳定的排序方法。

四、关于“排序方法的时间复杂度的下限”

”本章讨论的各种排序方法,除基数排序外,其它方法都是基于“比较关键字”进行排序的排序方法,可以证明,这类排序法可能达到的最快的时间复杂度为O(nlogn)。

(基数排序不是基于“比较关键字”的排序方法,所以它不受这个限制)。

■可以用一棵判定树来描述这类基于“比较关键字”进行排序的排序方法。



外部排序(略)