【数据结构】排序

Posted by ShawnD on December 3, 2019

一些概念

  • 排序算法的稳定性:经过排序后,能使关键字相同的元素保持原顺序中的相对位置不变。

插入排序

直接插入排序

口语描述:前面一段排序好的序列,后面一段乱序的序列,每次从乱序的第一个往前移,移到在顺序序列中比它小的元素之后。

Note:适用情况,元素数目少,或者元素的初始序列基本有序

准确描述:n个待排序的元素由一个有序表和一个无序表组成,开始时有序表中只包含一个元素。排序过程中,每次从无序表中抽出第一个元素,将其插入到有序表中的适当位置,使有序表的长度不断加长,完成排序过程。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define MAXSIZE 20
typedef int Keytype;	//定义关键字为整型

typedef struct{
	KeyType key;	//关键字项
	InfoType otherinfo;	//其他数据项
}RedType	//记录类型

typedef struct{
	RedType r[MAXSIZE+1];	//r[0]闲置或用作哨兵
	int length;				//顺序表长度
}SqList;					//顺序表类型

void InsertSort(SqList &L){
	for(int i=2;i<=L.length;++i){
		//如果没有这个if条件会怎么样? 直接
		if(L.r[i].key<=L.r[i-1].key){	
			L.r[0] = L.r[i]; L.r[i] = L.r[i-1];
			for(j=i-2; L.r[0].key<L.r[j].key;j--)
				L.r[j+1] = L.r[j];
			L.r[j+1] = L.r[0];
		}
	}
}

折半插入排序

口语描述:在直接插入排序的基础上,寻找插入位置时使用二分查找,称为折半插入排序。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void InsertSort(SqList &L){
	for(int i=2;i<=L.length;++i){
		L.r[0] = L.r[i];
		L.r[i] = L.r[i-1];
		low = 1; high = i-1;
		while(low<=high){
			mid = (low + high) / 2;
			if(L.r[mid].key<L.r[0].key)
				low = mid + 1;
			else
				high = mid - 1;
		}
		for(int j=i-1; j>=high+1;--j){
			L.r[j+1] = L.r[j];
		}
		L.r[high+1] = L.r[0];
		
	}
}

三个需要注意的点:

1、退出条件是 low <= high 不是 low < high
2、mid的取值 在数很大的时候担心溢出应采用low+(high-low)/2这种写法
3、low与high的更新:应当是low = mid+1 high = mid-1 避免死循环

希尔排序

口语描述:选一个增量,相当于把一个序列分成了分成了增量个数个子序列,举个例子,比如132456,增量为2的话,就分成了,125, 346,然后再对子序列排序。当增量足够小时,再对全体元素进行一次插入排序。

Note:希尔排序是一种不稳定的排序算法

代码(以dlta[k]为增量的希尔排序):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 一趟希尔排序,和直接插入的排序就是每次往前走一个,变成了每次往前走dk个
// 最后元素后移的结束条件多了一个j>0,因为现在的L.r[0]仅起到暂存作用
// L.r[0]并不是此时并不是哨兵
void ShellInsert(SqList &L, int dk){ 
	for(i=dk+1;i<=L.length;i+=dk){
		if(L.r[i].key<L.r[i-dk].key){
			L.r[0] = L.r[i];
			for(j=i-dk;j>0&&L.r[0].key<L.r[j].key;j-=dk)
				L.r[j+dk] = L.r[j];
			L.r[j+dk] = L.r[0];
		}
	}
}

//总共进行t趟希尔排序,每次增量为dlta[]中的元素
//这里我觉得dlta[t-1]应该是等于1,因为最后要进行一次直接插入排序嘛
void ShellSort(SqList &L, int dlta[], int t){
	for(k=0;k<t;k++){
		ShellInsrt(L, dlta[k]);
	}
}

王道代码(每次增量为dk/2):

1
2
3
4
5
6
7
8
9
10
11
12
13
void ShellInsert(SqList &L, int n){
	for(int dk=n/2;dk>=1;dk=dk/2){
		for(int i=dk+1;i<=L.length;i+=dk){
			if(L.r[i].key<L.r[i-dk].key){
				L.r[0] = L.r[i];
				for(int j=i-dk;j>0&&L.r[j].key>L.r[0].key;j-=dk){
					L.r[j+dk] = L.r[j];
				}
				L.r[j+dk] = L.r[0];
			}
		}
	}
}

交换排序

冒泡排序

口语描述:相邻两个比较,若为逆序则交换。两趟for循环。第一趟用于改变第二趟比较的长短,注意,我总是分不清冒泡排序和选择排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void BubbleSort(SqList &L){
	// 注意,这里的flag用的很灵性,如果比较一趟,发现序列有序
	// 就不需要再进行比较了,这样省去了很多趟的比较
	for(int i=1, change=True;i<=L.length&&change;++i){
		change = False;
		for(int j=1;j<=L.length-i+1;++j){
			if(L.r[j].key>L.r[j+1].key){
				L.r[0] = L.r[j];
				L.r[j] = L.r[j+1];
				L.r[j+1] =L.r[0];
				chage = True;
			}
		}
	}
}

快速排序

口语描述:

  1. 序列中取一个元素,以这个元素为基准,把序列分成两个子序列(一半比基准元素小,一半比基准元素大)
  2. 对两个子序列重复1
  3. 重复1,2,直到整个序列有序。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void QuickSort(SqList &L, int low, int high){
	if(low < high){
		int pivotpos = Partition(L, low, high);
		QuickSort(L, low, pivotpos-1);
		QuickSort(L, pivotpos+1, high);
	}
}

int Partition(SqList &L, int low, int high){
	L.r[0] = L.r[low];
	while(low<high){
		while(low<high&&L.r[high].key>=L.r[0].key) --high;
		L.r[low] = L.r[high];
		while(low<high&&L.r[low].key<=L.r[0].key) ++low;
		L.r[high] = L.r[low];
	}
	L.r[low] = L.r[0];
	return low;
}

选择排序

简单选择排序

口语描述:每次选一个最小的数和和前面的数交换。第i趟在n-i+1个记录中选取最小的记录作为有序序列中的第i个记录。

代码:

1
2
3
4
5
6
7
void SelectSort(SqList &L){
	for(int i=1; i<L.length; ++i){
		for(k=i+1,j=i;k<=L.length;k++)
			if(L.r[k].key<L.r[j].key) j = k;
		if(j!=i)  swap(L.r[i],L.r[j]);
	}
}

树形选择排序

口语描述:每一轮都将序列两两进行比较,小的进入下一轮,直到选出最小的

堆排序

口语描述: 堆的定义: 大顶堆: \(\begin{cases} K_i \geq K_{2i} \\ K_i \geq K_{2i+1} \end{cases}\)

小顶堆: \(\begin{cases} K_i \leq K_{2i} \\ K_i \leq K_{2i+1} \end{cases}\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void HeapSort(HeapType &H){
	for(i=H.length/2;i>0;--i)
		HeapAdjust(H, i, H.length);
	for(i=H.length;i>1;--i){
		swap(H.r[1],H.r[i]);
		HeapAdjust(H, 1, i-1);
	}
}

void HeapAdjust(HeapType &H, int s, int m){
	rc = H.r[s];
	//为什么是j<=m?
	//j<m时,j没到叶子节点
	//j=m时,j在叶子节点上,要看叶子节点和目标节点是否满足堆定义
	for(j=2*s;j<=m;j*=2){
		//为什么j<m?
		//说明j还没到叶子节点
		if(j<m&&H.r[j].key>H.r[j+1].key) ++j;	
		if(rc.key<H.r[j].key) break;
		//s永远指向上一层的结点
		//j永远指向下一层的兄弟节点中较小的那一个
		//当j没到叶子节点时,且不满足堆定义
		//将两个兄弟之间较小的直接覆盖掉s
		H.r[s] = H.[j];
		//更新指针s
		s = j;
	}
	H.r[s] = rc;
}

归并排序

将n个无需序列看成n个由长度为1的有序子序列组成的序列,然后两两归并,得到n/2个长度为2或1的有序子序列,再两两归并,知道序列有序。

归并两个顺序表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void SqList_Merge(SqList &LA, SqList &LB, SqList &LC){
	int i=0, j=0, k = 0;
	ElemType *pA = LA.r, *pB = LB.r, *pC;
	LC.length = LA.length + LB.length;
	pC = LC.r = (ElemType*)realloc(r, LC.length*sizeof(ElemType*));
	while(pA<pA+LA.length&&pB<pB + LB.length){
		if(*pA < *PB){
			*pC++ = *pA++;
		}
		else{
			*pC++ = *pB++;
		}
	}
	while(pA<pA+LA.length) *pC++ = *pA++;
	while(pB<pB+LB.length) *pC++ = *pB++;
}

归并两个单链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void LinkList_Merge(LinkList &LA, LinkList &LB){
	LNode *pA =  LA->next, *pB = LB->next;
	LNode *pC = LA;
	while(pA!=NULL&&pB!=NULL){
		if(pA->Val < pB->Val){
			pC->next = pA;
			pA = pA->next;
		}else{
			pC->next = pB
			pB = pB->next;
		}
	} 
	if(pA!=NULL) pC->next = PA;
	if(pB!=NULL) pC->next = PB;
}

基数排序

以比较三位数为例 先比较个位,按顺序排列,再比较十位,按顺序排列,最后比较百位,按顺序排列。

  时间复杂度 空间复杂度 稳定性 适用情况
直接插入排序 $O(n^2)$ O(1) 稳定 n较小,初始序列基本有序
希尔排序 $O(n^{1.3})$ O(1) 不稳定  
冒泡排序 $O(n^{2})$ O(1) 稳定 n较小,初始序列基本有序
快速排序 $O(n\log_2{n})$ $O(\log_2{n})$ 不稳定 初始序列无序
简单选择排序 $O(n^{2})$ O(1) 不稳定 n较小
堆排序 $O(n\log_2{n})$ O(1) 不稳定 n较大,或只排前几位
2-路归并排序 $O(n\log_2{n})$ O(n) 稳定 n很大
链式基数排序 $O(d(n+rd))$ O(rd) 稳定 n大,关键字值小

Note:快速排序在最坏的情况下退化成冒泡排序,时间复杂度为$O(n^2)$, 快速排序每次递归都会返回一个中间值的位置,必须使用栈。所以空间复杂度就是栈用的空间。

这个图有问题 希尔排序平均时间复杂度为$O(n^{1.3})$ 快速排序空间复杂度为$O(logn)$