【背诵笔记】内部排序

类别

在这里插入图片描述

1,插入类排序
在已有的有序序列中,通过插入新的关键词进行排序。有直接插入排序、折半插入排序和希尔排序。

2,交换类排序
以交换为核心,每一趟都通过一系列的交换动作,让一个关键词排到它的位置。有冒泡排序和快速排序

3,选择类排序
以选择为核心,每一趟都选出一个最大或最小的关键词,把它和有序序列中第一个或最后一个关键词交换,使选出的关键词在合适的位置。有简单选择排序和堆排序。

4,归并类排序
将两个或两个以上的有序序列合并为一个新的有序序列。有K路归并排序。

5,基数排序
把一个逻辑关键字拆分为多个关键字,然后对多各关键词进行排序。

插入类排序

在这里插入图片描述

直接插入排序

思路

在排序中,序列为以下状态:

有序待排无序
L[1,…,i-1]L[i]L[i+1,…,n]

要将L[i]插入有序序列L[1,…,i-1]中
1,在L[1,…,i-1]中找到L[i]的插入位置K(从后向前遍历,K>=L[i])
2,将L[k,…,i-1]全部后移一位,空出L[k]
3,将L[i]插入L[K]

初始时,我们将L[1]看做有序,依次对L[2]-L[n]执行n-1次插入排序

代码

//直接插入排序
void InsertSort(int A[], int n) {
	int i, j, temp;
	for (i = 1; i < n; ++i)//从第二个元素开始遍历
	{
		if (A[i] < A[i - 1]) {//若当前遍历的元素>=前一元素,已经有序故不操作;若<前一元素,则执行
			temp = A[i];//暂存元素值
			for (j = i - 1; j >= 0&&A[j]>temp; --j)//从前一元素,向前遍历有序序列,直到遇到大于等于该元素的,或者遍历到头
				A[j + 1] = A[j];
			A[j + 1] = temp;//插在A[J]后面(若A[j]==temp,保证了稳定性)
		}
	}
}

算法效率

1,空间复杂度:O(1)
只占用几个常量,空间复杂度不计算输入元素大小。

2,时间复杂度:O( n 2 n^{2} n2)
从第二个元素开始要循环n-1次。
每次循环最主要的开销是对比元素大小和移动元素。最坏的情况,逆序序列,每次要都要和已排序的所有元素对比,且有序序列元素都要后移。第i次循环,有序序列有i个元素,要对比i次,移动i+1次(包括待排序元素本身一次)。
则总体时间复杂度为O( n 2 n^{2} n2)

3,稳定性:稳定
从第二个元素开始遍历
if (A[i] < A[i - 1]) //若当前遍历的元素>=前一元素,已经有序,不操作;等于号保证了稳定性。若<前一元素,则执行。

链表

思路

对数组插入排序时,是两重循环,第一重是从第二个元素开始遍历整个数组,第二个是反向循环,从当前元素开始向前对已排序列遍历,第一个小于待排元素的元素,并插到它后面。

但单链表无法反向,所以第二重循环就可以对已排序列进行正向遍历,找到第一个大于当前结点值的结点,并插到它前面。
首先将L断开,L只保留一个节点。其余节点由p指向为待排序列。

遍历L找到第一个大于等于待排元素的节点,将待排元素插在前面,或者遍历完已排序列,则插在已排序列末尾。

带头结点代码
void select_sort(Listnode*& L)
{
	Listnode *p,*pLate,*m,*prem;
	if(L->next!=NULL)
	{
		p=L->next-next;//待排序列
		L->next->next=NULL;//已排元素
		while(p!=NULL){
			prem=L;
			m=prem->next;//遍历已排元素
			while(m!=NULL&&m->data<p->data){//在已排序列找到第一个大于等于待排元素的元素。
				prem=m;
				m=m->next;
			}
			//将待排元素插入找到的元素之前
			pLate=p->next;//从待排序列取出元素
			p->next=prem->next;//插在找到的元素前面
			prem->next=p;
			p=pLate;//指针重新指向待排序列第一个元素
		}
	}
}
时间复杂度

对于每次循环来说
1,移动元素的次数减少,只需改动指针,无需把每个元素后移来插入。
2,关键字对比仍然是O( n 2 n^{2} n2)级别。因为链表只能从头到尾遍历,无法随机访问。这样每次都要从头到尾遍历已排序的序列,找到>=待排元素或者遍历完已排序列。

折半插入排序

思路

在直接插入排序的基础上做改进,以减少比较次数。

有序待排无序
L[1,…,i-1]L[i]L[i+1,…,n]

1,对已排序列L[1,…,i-1]使用折半查找法找到比待排元素L[i]大的元素L[k]
2,将L[k,…,i-1]全部后移一位,空出L[k]
3,将L[i]插入L[K]

折半查找:将待排序元素与有序序列中间元素((low+high)/2)对比,若小于就查中间元素左边序列(high=mid-1),大于等于就查找右边序列(low=mid+1)。
最后low指向的元素就是大于待排元素的最小元素,也就是说[low,i-1]均大于待排元素;而[0,high]小于等于待排元素,这时只需将待排元素插入low前即可。

代码

//折半插入排序
void InsertSort(int A[], int n) {
	int i, j, low,high,mid,temp;
	for (i = 1; i < n; ++i)//从第二个元素开始遍历
	{
		temp = A[i];//暂存数据
		low = 0; high = i - 1;//已排序序列首尾元素
		while (low <= high) {//折半查找
			mid = (low + high) / 2;//取中间元素
			if (A[mid] > temp)//不断缩小查找访问
				high = mid - 1;
			else
				low = mid + 1;//low会不断指向值更大的方向
		}
		若j<low,待排元素比前面所有元素都大,无需后移,直接插在原处。
		for (j = i - 1; j >= low; --j)//值大于temp的元素,依次后移
			A[j + 1] = A[j];
		A[low] = temp;//插入low位置
	}
}

算法效率

1,空间复杂度:O(1)
只占用几个常量,空间复杂度不计算输入元素大小。

2,时间复杂度:O( n 2 n^{2} n2)
与直接插入排序算法相比,折半插入排序
在关键字对比方面,由于采取了折半查找法,时间大大减少。且每次比较的次数都固定,和初试序列无关,都是在low>high时结束。

在关键词移动方面,则和直接插入一样。
因此时间复杂度是O( n 2 n^{2} n2)

3,稳定性:稳定
将待排序元素与有序序列中间元素((low+high)/2)对比,若小于就查中间元素左边序列(high=mid-1),大于等于就查找右边序列(low=mid+1)。等于保证左边的序列始终大于等于待排元素,保证了稳定性。

希尔排序

在这里插入图片描述

思路

插入排序适合基本有序的序列,若序列逆序时间复杂度达到 O ( n 2 ) O(n^{2}) O(n2),若为正序则可提升到O(n),希尔排序(缩小增量排序)就是在此基础上进行优化。
首先确定一个小于n的增量d,将序列分为形如L[i,i+d,…,i+kd]的d个子表。即间隔d的元素组成子表,然后对每个子表进行插入排序。不断缩小d的值,进行排序。当整个表已经基本有序(具有较好的局部有序性)的时候(d=1),最后进行一次排序即可。
在这里插入图片描述

代码

void ShellSort(int A[], int n) {
	int d, i, j, k, temp;
	for (d = n / 2; d >= 1; d = d / 2)//取增量,以增量为间隔构建子表
	{
		for (i = 0; i < d; ++i)//遍历子表的头结点
		{
			for (j = i + d; j < n; j = j + d)//遍历每个子表的元素,默认第一个元素有序,按直接排序处理。
			{
				if (A[j] < A[j - d]) {
					temp = A[j];//暂存数据
					for (k = j - d; k >= 0 && temp < A[k]; k -= d)
						A[k + d] = A[k];
					A[k + d] = temp;
				}
			}
		}//每次都使整个序列更有序,从而直接排序速度越来越快。
	}
}

算法效率

1,空间复杂度:O(1)
只占用几个常量,空间复杂度不计算输入元素大小。

2,时间复杂度:O( n 2 n^{2} n2)
根据增量的不同,会有不同的时间复杂度
d=d/2时是O( n 2 n^{2} n2)
d= 2 k + 1 2^{k}+1 2k+1是O( n 1.5 n^{1.5} n1.5)

3,稳定性:不稳定
当值相同的值,划分到不同子表后,可能导致相对顺序变化

交换类排序

在这里插入图片描述

冒泡排序

思路

从后往前两两比较相邻元素的值,若为逆序(A[i-1]>A[i]),则交换两元素,直到序列比较完毕。每趟排序都将未排序列中最小元素放在已排序列的最终位置。
最多执行n-1次,若某一趟排序中没有进行任何交换,说明序列已经有序,则停止排序。
排序过程中,若两元素相同,则不交换,保证稳定性。

代码

void swap(int &a,int &b)
{
	int temp = a;
	a = b;
	b = temp;
}
//冒泡排序
void BubbleSort(int A[], int n) {
	for (int i = 0; i < n-1; ++i) {
		int flag = 1;
		for (int j = n - 1; j > i; --j) {
			if (A[j - 1] > A[j])
			{
				swap(A[j - 1], A[j]);
				flag = 0;
			}
		}
		if (flag == 1)
		{
			return;
		}
	}
}

链表实现

思路

数组是从后往前冒泡,而链表特别是单链表不方便从后向前查找,因此可以从前向后把较大的值冒到后面去。

链表排序最好还是要交换节点,而不只是交换值。

交换节点后p指针等于后移了一位
1,交换节点后
指针p与q(p的后一元素指针)交换了位置,实际相当于p已经后移一位,
2,不交换节点
当前元素小于下一元素,p需要手动后移,指向下一个元素(更大的)
3,pre位置不变
无论是否交换,pre指针位置不变,因此通过pre执行后移。

代码

//定义链表结构体
typedef struct Linklist
{
	int data;
	struct Linklist* next;
}Linklist;

//交换链表中两个节点
void swap(Linklist* pre,Linklist* p, Linklist* q)
{
	pre->next = q;
	p->next = q->next;
	q->next = p;

};

//输出链表节点
void print(Linklist* L)
{
	Linklist* p = L;
	for (int i = 0; i < L->data; ++i)//L为头结点不能动 否则每次遍历就会改变条件
	{
		printf("%d ", p->next->data);
		p = p->next;
	}
	printf("\n");
}
//简单选择排序(升序)
void Bubblesort(Linklist* head)
{
	for (int i = head->data - 1; i > 0; --i)
	{
		int flag = 1;
		Linklist* pre = head;
		Linklist* p = pre->next;
		for (int j = 0; j < i; ++j)//从第一个元素开始遍历到n-1个元素
		{
			if (p->data > p->next->data) {//将大的元素冒泡上去
				swap(pre, p, p->next);//元素交换后,p指针就相当于前移了一次(数组中只交换值,不改变下标)
				flag = 0;
			}
			pre = pre->next;//不管交换与否,pre位置不变,因此通过pre指针指行后移
			p = pre->next;
		}
		if (flag == 1)
			return;
	}
}

算法效率

1,空间复杂度:O(1)
只占用几个常量,空间复杂度不计算输入元素大小。

2,时间复杂度:O( n 2 n^{2} n2)
最好情况:时间复杂度是O( n n n),序列有序。只需只需一趟,对比n-1次,flag==false即结束算法。

最坏情况:时间复杂度是O( n 2 n^{2} n2),序列逆序。需要执行n-1趟,第i趟排序要执行n-i次对比,每次交换则需要执行3次。

3,稳定性:稳定
if (A[j - 1] > A[j])保证了,相同元素不会发生交换,从而保证了稳定性。

4,全局有序性
冒泡排序产生的有序序列是全局有序的,也就是说有序序列的元素都是在其最终位置上。

快速排序

在这里插入图片描述

思路

快速排序是基于分治法进行的。在待排序列L[1…n]中建立一个元素pivot作为枢轴(常为首元素)。通过一趟排序将序列分为独立的两个部分L[1…K-1]和L[K+1…n]。使L[1…K-1]中所有元素小于pivot,L[K+1…n]中所有元素大于等于pivot,pivot放在其最终位置L[K]上。然后递归地进行多次快速排序,直到所有子表只有一个元素或者为空,则所有元素都放在最终位置。

具体的一趟排序:
low和high指针分别指向首尾元素,pivot取首元素。
从high向左,找到第一个小于pivot的元素,将其替换到low处。
从low向右,找到第一个大于等于pivot的元素,将其替换到high处。
high和low交替向中间搜索元素,直到low==high,此处就是pivot的最终位置

若序列只有一个元素,显然已经有序,该元素就在最终位置处。
具体的递归机制可以看我的这篇帖子:【上机代码】函数调用栈,快速排序与归并排序.

代码

//partition实现从low和high向中间扫描序列,以low指向的元素为基准,将序列分为两个部分,基准在中间,左边所有元素小于基准,右边所有元素大于等于基准。
//返回基准值,和排序好的序列
int partition(int A[], int low, int high)
{
	int pivot = A[low];
	while (low < high) {
		while (low < high && A[high] >= pivot)
			--high;
		A[low] = A[high];
		while (low < high && A[low] <= pivot)
			++low;
		A[high] = A[low];
	}
	A[low]= pivot;
	return low;
}
//基于递归实现,先按基准排序,然后分别递归处理基准左右的子表,子表长度>1时就继续划分,直到所有子表被处理完毕。
void QuickSort(int A[], int low, int high) {
	if (low < high) {
		int pivotpos = partition(A, low, high);
		QuickSort(A, low, pivotpos - 1);
		QuickSort(A, pivotpos + 1, high);
	}
}

算法效率

1,空间复杂度
由于快速排序是递归的,占用的空间与递归的最大调用深度一致。为 O ( l o g n ) O(log^{n}) O(logn)

2,时间复杂度
快速排序的运行速度与序列划分是否对称有关。
最坏情况,长度为n的序列,被分为n-1长度的子表和0长度子表。若每层递归都是这种情况(序列有序或逆序),对应的时间复杂度为 O ( n 2 ) O(n^{2}) O(n2)
平均来说时间复杂度为 O ( n l o g n ) O(nlog^{n}) O(nlogn),是所有排序算法中平均表现最好的。

提高时间效率:选取一个可以将序列平均划分的枢轴,可以取头尾和中间元素的中间值为枢纽,或者取随机值为枢轴。

3,稳定性,不稳定
若在右子表有两个相同的元素,在交换到左子表后,相对次序会变化。
3 2 2‘
2’ 3 2
2‘ 2 3

4,排序后的位置
每次排序后都会将枢轴放在其最终位置上。

简单选择排序

在这里插入图片描述

1,思路

选择排序算法通过选择和交换来实现排序,其排序流程如下:
(1)序列表为L[1…n],每趟(第i趟)在L[i…n]中的n-i+1个(第一趟n个、第二趟n-1个…)待排元素中选取最小的元素,然后与L[i]交换。每趟确定一个元素的最终位置。
(2)然后不断重复,直到n-1趟,待排序列只剩一个元素就不需要再选择了。便完成了对原始序列的排序。

2,代码

void swap(int& a, int& b)
{
	int temp = a;
	a = b;
	b = temp;
}
//简单选择排序
void SelectSort(int A[], int n) {
	for (int i = 0; i < n - 1; ++i) {
		int min = i;
		for (int j = i + 1; j < n; ++j) {
			if (A[j] < A[min])
				min = j;
		}
		if (min != i)
			swap(A[i], A[min]);
	}
}

链表实现

思路

关于链表的实现有两点疑问:
1,带头结点的单链表如何交换两个元素
A,B两个元素要设4个指针分别指向A、B的前后节点。交换时分相邻和不相邻两种情况:
(1)相邻只需要更改3个节点指向
A的前置指向B,B指向A,A指向B的后置

(2)不相邻要更改4个节点指向
A前置 A A后置 。。。。B前置 B B后置
A的前置指向B,B指向A的后置
B的前置指向A,A指向B的后置

2,简单选择排序中,链表如何区分两个元素
在数组的排序中,值是用下标来标识不同的元素,即使值相同也可区分。
在链表的排序中,在之前的遍历中已经找到的是<min的元素,找到的元素一定和min的值不相同,(相同的元素就不动了)故可以用p->data != min->data作为条件。或者结构体中加一个id来区分不同的元素。

代码

//简单选择排序(不带头结点,升序)
void select_sort(Listnode*& L)
{
	//h为待排序列
	Listnode* h = L, * p, * q, * prem, * m;
	L = NULL;//L为已排序列
	while (h!=NULL)
	{
		p = m= h;//m为最大值,p为待排元素
		q = prem= NULL;//最大值前驱,q为p前驱
		while (p!= NULL)//遍历序列找到最大值
		{
			if (p->data > m->data)
			{
				m= p;
				prem= q;
			}
			q = p;
			p = p->next;
		}
		if (m == h)//最大值在待排序列第一个,此时pmax为null
			h = h->next;//易错
		else
			prem->next = m->next;//取出max
		m->next = L;
		L = m;
	}
}

3,算法效率

⑴空间复杂度

仅使用常数个辅助单元,空间复杂度为O(1)

⑵时间复杂度

元素的移动次数较少,每趟最多移动3次,也就是说最多移动3(n-1)次
元素的比较次数固定,与初始序列无关,始终为n(n-1)/2次。
则时间复杂度为O( n 2 n^{2} n2)

⑶稳定性

不稳定,在第i趟找到最小元素后,和第i个元素交换,可能会导致第i个元素与其他含有相同关键词的元素相对位置发生改变。
2 2‘ 1
1 2’ 2(破坏稳定性)

堆排序

在这里插入图片描述

1,堆

⑴概念

大根堆,L(i)>=L(2i)且L(i)>=L(2i+1)
小根堆,L(i)<=L(2i)且L(i)<=L(2i+1)

⑵堆与完全二叉树

可以将堆视为一棵完全二叉树,大根堆就是根节点为最大元素,且任何一个子树中根节点值大于等于左右孩子节点的值。小根堆则想反,根节点最小,任意子树中根节点值小于等于左右孩子节点的值。

⑶性质

根据完全二叉树的性质,可以推广得堆的性质。若节点按从上到下,从左到右依次编号。对于节点i。
①左孩子:2i
②右孩子:2i+1
③父节点:⌊i/2⌋
证明:
(1)证:完全二叉树中任何一层最左的节点编号n,则其左子树为2n,右子树为2n+1.
①显然,对于第L层的最左节点,等于L-1层最后一个节点编号+1;也就是从第1层到L-1层所有节点数+1。
②则:前L-1层节点数= 2 0 + 2 1 + . . . + 2 ( L − 2 ) = 2 ( L − 1 ) − 1 2^0+2^1+...+2^{(L-2)} = 2^{(L-1)}-1 20+21+...+2(L2)=2(L1)1个(第i层有 2 ( i − 1 ) 2^{(i-1)} 2(i1)个节点)。
③则第L层最左节点编号为 2 ( L − 1 ) 2^{(L-1)} 2(L1),其左子树为第L+1层的最左节点,编号为 2 L 2^L 2L,右子树节点编号为 2 L + 1 2^L+1 2L+1。得证。

(2)证:完全二叉树中任一节点编号n,则其左子树为2n,右子树为2n+1.
①取第L层的任意节点N,编号为n。设L层最左节点为M,编号为m。
②显然,L层中,N左边有n-m个节点。由于是完全二叉树,这n-m个节点都有左右孩子。
③则在L+1层中,N的左孩子NL左边有2(n-m)个节点。
④由(1)知第L+1层的最左节点编号为2m,则NL的编号为2m+2(n-m)=2n.得证

④i节点所在的层⌈ l o g i + 1 log^{i+1} logi+1⌉、⌊ l o g i log^{i} logi⌋+1

理解:
(每层最后一个元素编号= 2 x − 1 2^{x}-1 2x1,x为层号。则x=⌈ l o g i + 1 log^{i+1} logi+1⌉=⌊ l o g i log^{i} logi⌋+1,{2,对前者的理解,最大的也就是i取该层最后一个元素时, l o g i + 1 log^{i+1} logi+1=层号,再取该层其他元素值只会小于层号,故向上取整。2,对后者的理解, l o g i log^{i} logi取最小的也就是i取该层第一个元素时, l o g i log^{i} logi=层号-1,再取该层其他元素值只会大于层号-1,故向下取整。})

⑤若i为叶子结点:i>⌊n/2⌋(n为节点总数)
⑥若i为非叶子结点:i<=⌊n/2⌋

理解:
⌊n/2⌋就是最后一层最后一个元素的父节点,自然可以以它为界划分叶子和非叶子结点。
在这里插99入图片描述

2,构建初始堆

⑴思路

n个元素的序列对应着n个节点的完全二叉树。其中最后一个节点的父节点为⌊n/2⌋,对⌊n/2⌋节点进行调整(若根节点小于左右孩子节点的较大者,则与较大者进行交换),使其构成大根堆。
循环对所有非叶子节点(⌊n/2⌋-1)进行上述调整,交换后可能破坏下一级,需要继续下沉对下一级子树进行调整。直到底或者下级子树满足堆条件。
循环到根节点,堆构建完毕。

⑵代码

//从序列k位置元素向下,调整为堆
void HeadAdjust(int A[], int k, int n) {
	A[0] = A[k];//暂存根节点
	for (int i = 2 * k; i <= n; i = i * 2)//下沉节点,沿左右孩子较大者
	{
		if (i < n && A[i] < A[i + 1])//找出左右孩子最大者
			++i;
		if (A[0] >= A[i])//大于等于孩子节点,调整完毕
			break;
		else {//小于孩子节点,交换
			A[k] = A[i];
			k = i;//根节点下沉到较大的孩子节点位置,继续循环对比,值仍然存在A[0]
		}
	}
	A[k] = A[0];//根节点的值不断下沉,最后存于最终位置
}
//构建堆
void BuildMaxHeap(int A[], int n)
{
	for (int i = n / 2; i > 0; i--)//遍历所有非叶子节点
		HeadAdjust(A, i, n);
}

3,堆排序

⑴思路

①将L[1…n]中的n个元素构成初始堆(大根堆),则堆顶元素为最大值,输出堆顶元素后
②将堆底元素送入堆顶,此时根节点不满足堆的性质。将堆顶元素向下调整,直至满足堆的性质。然后输出堆顶元素
③重复,直到堆中只剩下一个元素。

⑵代码

void swap(int& a, int& b) {
	int temp = a;
	a = b;
	b = temp;
}
//堆排序
void HeapSort(int A[], int n)//n为待排序列长度,非数组长度
{
	BuildMaxHeap(A, n);//建立堆
	for (int i = n; i > 1; i--) {//从最后一个节点开始向前遍历
		swap(A[i], A[1]);//堆顶与堆底元素交换,存入有序序列最终位置
		HeadAdjust(A, 1, i-1);//有序序列不再参与调整,继续调整堆。
	}
}

⑶小根堆排序

只需改动堆调整部分,改为调整为小根堆即可。

void HeadAdjust(int A[], int k, int n) {
	A[0] = A[k];
	for (int i = 2 * k; i <= n; i = i * 2)
	{
		if (i < n && A[i] > A[i + 1])//找左右子树中较小的
			++i;
		if (A[0] <= A[i])//根节点小于等于左右子树就结束调整
			break;
		else {//根节点比左右子树小就交换
			A[k] = A[i];
			k = i;
		}
	}
	A[k] = A[0];
}

4,插入删除

在这里插入图片描述

插入

思路,待插入元素先插入堆底。然后与父节点对比,若比父节点小就交换。然后再与新的父节点对比。循环直到比父节点大或者到根节点。(逆向调整)

注意: n为堆的元素数量,插入后要增加,因此用引用型

void Insert(int A[], int& n, int a) {
	int i = ++n;
	A[i] = a;
	while (i / 2 > 0) {
		if (A[i] < A[i / 2])
			swap(A[i], A[i / 2]);
		else//调整完毕 无需继续对比
			break;
		i = i / 2;
	}
}

删除

思路,将堆底元素替换到待删元素处,序列长度减1。然后对该元素进行一次堆调整。
注意: n为堆的元素数量,删除后要减少,因此用引用型

void Delete(int A[], int& n, int i) {
	A[i] = A[n];
	--n;
	HeadAdjust(A, i, n);
}

5,算法效率

⑴空间复杂度

常数级O(1)

⑵时间复杂度

堆排序主要分为建堆和调整堆两个步骤,下面分别进行分析:

void BuildMaxHeap( ) //建堆的时间复杂度为0(n)
①第i层最多有 2 i − 1 2^{i-1} 2i1个节点
②建立一个大根堆,最多需要从第1层对比到第h-1层, ∑ i = h − 1 1 2 i − 1 ∗ ( h − i ) ∗ 2 < = 4 n \sum_{i=h-1}^{1}{2^{i-1}*(h-i)*2}<=4n i=h112i1(hi)2<=4n
则建堆时间复杂度为0(n)

void HeadAdjust( ) //调整的时间复杂度为O( n l o g 2 n nlog_{2}^{n} nlog2n
①一个节点在一次下沉中最多对比两次(左右孩子之间对比、孩子较大/小者与根对比)
②完全二叉树高为h,对第i层节点,下面最多有h-i层,则最多对比2(h-i),时间复杂度为O(h);又 h = ⌊ l o g 2 n ⌋ + 1 h=⌊log^{n}_{2}⌋+1 h=log2n+1(每层最每层节点带入log+1都是>= h i h_{i} hi,且< h i + 1 h_{i+1} hi+1,所以向下取整。)
O(h)=O( l o g 2 n log_{2}^{n} log2n)
③排序时最多对n-1个节点(从n到2)进行调整,则时间复杂度为O( n l o g 2 n nlog_{2}^{n} nlog2n

则最终的时间复杂度为0(n+ n l o g 2 n nlog_{2}^{n} nlog2n)=O( n l o g 2 n nlog_{2}^{n} nlog2n

⑶稳定性

不稳定,可能将后面相同值的节点调整到前面;

原始:1 2 2’
建堆:2 1 2’
排序:1 2’ 2

6,一些问题

⑴堆排序输入的len为序列长度

在上机代码中应该注意:
1,数组长度不等于序列长度,A[0]专门空出来用来暂存数据,因为在下沉的时候不方便处理。
2,堆排序几个函数中输入的length都是待排序的序列长度。在设计代码要注意创建的数组长度与输入排序函数的长度;比如排序后的输出要从1开始,而不是从0开始,到length结束。插入、删除要用引用型,以便修改序列长度。

⑵只排前几个元素用什么代码

比如序列有几百万个元素,只需要对前100个做出排序。
插入、快排、归并等排序都只有在全部排序后才得到最终位置。效率不高。
冒泡、堆排序和简单选择排序,每趟都是确定一个元素的最终位置。可以应用在这里。

归并排序

在这里插入图片描述

思路

1,总体思路
将两个或两个以上的有序序列两两合并为新的有序表。
初始将序列中n个元素视为n个长度为1的有序表,执行两两合并为⌈n/2⌉个长度为2或1的有序表。然后继续归并,直到合并为一个长度为n的有序序列。

2,如何归并
Merge(int A[],int low,int mid,int high)执行两两归并
两段有序序列为A[low,…,mid]和A[mid+,…,high],将两段序列复制到辅助数组B中,对数组B的两段序列,分别依次从头到尾取元素对比,较小者放入A中(从A[low]开始放,即覆盖原序列);若一段序列元素取完,则把剩下的元素都放入A中。

3,递归调用
MergeSort()
对序列进行递归调用,不满足low < high就会结束递归,然后从长度为1的相邻序列开始排序。最后对左右有序子表归并为长度为n的序列。

代码

int* B = (int*)malloc(10 * sizeof(int));//Merge中是和原始序列下标一样调用,不能按需分配。

void Merge(int A[], int low, int mid, int high) {
	int i, j, k;
	for (k = low; k <= high; k++)
		B[k] = A[k];
	for (i = low, j = mid + 1, k = i; i <= mid && j <= high; k++) {
		if (B[i] <= B[j])
			A[k] = B[i++];
		else
			A[k] = B[j++];
	}
	while (i <= mid) A[k++] = B[i++];
	while (j <= high) A[k++] = B[j++];
}

void MergeSort(int A[], int low, int high) {
	if (low < high) {
		int mid = (low + high) / 2;
		MergeSort(A, low, mid);
		MergeSort(A, mid + 1, high);
		Merge(A, low, mid, high);
	}
}

算法效率

空间复杂度

O(n) 辅助数组的长度n

时间复杂度

O( n l o g n nlog^{n} nlogn)

稳定性

稳定
2 1 1‘ 2’
先把左右子表内部有序
1 2 1‘ 2’
左==右时,先放左边的,保证有序
1 1‘ 2 2’

与直接插入排序(链表)

都是新建立一个辅助数据结构,然后在辅助结构中进行对比,找出最小的往原结构中填充。
1,归并排序是建立一个辅助数组B,在B中先比较各有序序列元素,按顺序覆盖进原数组A中

2,直接插入排序是,建立一个辅助链表h,同时将原链表清空。然后在辅助链表中依次找出最小的,插入原链表中。

基数排序(会手算即可)

在这里插入图片描述

思路

基于关键字各位大小排序。假设线性表中每个节点 a j a_{j} aj由d元组{ k j d − 1 , k j d − 2 , . . . , k j 1 , k j 0 k_{j}^{d-1},k_{j}^{d-2},...,k_{j}^{1},k_{j}^{0} kjd1,kjd2,...,kj1,kj0}组成。 k j d − 1 k_{j}^{d-1} kjd1为最主位关键字, k j 0 k_{j}^{0} kj0为最次为关键字。通常排序方法可分为最高位优先和最低位优先。
最低位优先:
①以r为基数进行排序,r是各位取值的范围。建立队列 Q 0 . . . . Q r − 1 Q_{0}....Q_{r-1} Q0....Qr1
②分配:队列都置空,观测各个关键字的位 k j i k_{j}^{i} kji,位等于多少就放把该关键字入对应的队列中,如 k j i = k k_{j}^{i}=k kji=k,就放入 Q k Q_{k} Qk
③收集:依次把队列 Q 0 . . . . Q r − 1 Q_{0}....Q_{r-1} Q0....Qr1中关键字首尾相连,就组成了新的元素。
④依次从低位到高位执行分配和收集。排序结束。
最高位优先:
从最高位向最低位依次执行分配和收集。

算法效率

空间复杂度

需要r个队列(r个头尾指针),则空间复杂度为O®

时间复杂度

d为位数,r为基数,n为序列长度
需要d趟分配和收集,每趟分配对序列遍历O(n),收集对队列遍历O®,则时间复杂度为O(d(n+r)),为固定值,与初始序列无关

稳定性

稳定,按位遍历就是保证了稳定性,同一位中值相同则右边入队;收集时则是反方向,使左边先出队,则最终还是保持了相对位置。

1 2 2‘ 3

Q1 Q2 Q3
1 2 3
2’

1 2 2’ 3

总结

时间复杂度

1,平均情况下
直接插入、折半插入、简单选择和冒泡排序为O( n 2 n^{2} n2)
快速排序、归并排序和堆排序为O( n l o g n nlog^{n} nlogn)
基数排序为O(d(n+r)),d为位数,r为基数,n为序列长度

2,最好情况下
直接插入和冒泡排序可达O(n)

3,与初始序列无关
简单选择排序,基数排序

空间复杂度

1,快速排序,需要使用递归栈,平均复杂度为O( l o g n log^{n} logn)
2,2路归并排序,需要使用辅助数组,平均复杂度为O(n)
3,基数排序需要r个队列(r个头尾指针),则空间复杂度为O®
4,其他的都是O(1)

ps:注意简单选择排序的链表实现,虽然也新建了链表,但是只建立了指针,所以还是常数级。

稳定性

1,希尔排序,当值相同的值,划分到不同子表后,可能导致相对顺序变化。
2,快速排序,若在右子表有两个相同的元素,在交换到左子表后,相对次序会变化。
3,简单选择排序,在第i趟找到最小元素后,和第i个元素交换,可能会导致第i个元素与其他含有相同关键词的元素相对位置发生改变。
4,堆排序,孩子和根交换,在序列中孩子和根之间隔着若干元素,可能会调到前面值相同元素之前,导致相对顺序变化。
5,其他排序是稳定的。

其他问题

1,每趟排序都能使一些元素放在最终位置:冒泡排序、快速排序、简单选择排序、堆排序
2,排序躺数与序列有关:交换类,冒泡,快排
3,关键字比较次数与序列无关,简单选择、折半插入
4,序列元素基本有序,适合冒泡、直接插入
5,序列较长,适合时间复杂度为O( n l o g n nlog^{n} nlogn)的算法,也就是快速排序、归并排序和堆排序。其中快排适合元素随机分布情况;堆排序占用辅助空间较少,且最坏情况下时间复杂度也较小。前两者都不稳定,只有归并排序则是稳定的。
6,n很大,但是关键字位数小且可以分解是,基数排序效果不错
比如,n=10000,关键词为3位数字,则r为9,排3轮
基数排序:0(d(n+r))=30027
快速排序:0(nlog^{n})=132877
7,记录的信息比较大可以利用链表存储,减少移动时间(直接插入、冒泡、简单选择)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

燕南路GISer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值