数据结构——排序总结

声明:此总结较简练,对于每种算法未给出排序例子,适合有一定基础进行复习了解具体实现性能分析使用,建议初学者选择更详细的教程

框架:

内部排序:

一、插入排序

直接插入排序

将元素L[i]插入已有序的子序列L[1...i-1]

1)找出L[i]在子序列中的插入位置k

2)将L[k...i-1]的元素后移一位

3)将L[i]复制到L[k]

void insertsort(element a[i], int n) {
	int i, j;
	for(i=2;i<=n;i++)
		if (a[i] < a[i - 1]) {
			a[0] = a[i];
			for (j = i - 1; a[0] < a[j]; j--)
				a[j + 1] = a[j];
			a[j] = a[0];
		}
}

空间复杂度:O(1)

时间复杂度:总共进行n-1趟插入,插入分为比较和移动

                         最好情况:已有序,每趟比较一次,不移动,O(n)

                        最坏情况:逆序,O(n^2)

算法稳定,适用于顺序存储的线性表

折半插入

比较元素时使用折半查找,可以减少元素比较的次数,时间变为O(nlog2(n)),但元素移动次数不变,因此总的时间复杂度仍为O(n^2),其余特性与直接插入一致

void insertsort(element a[i], int n) {
	int i, j, low, high, mid;
	for (i = 2; i <= n; i++) {
		a[0] = a[i];
		low = 1, high = i - 1;
		while (low <= high) {
			mid = low + high >> 1;
			if (a[mid] > a[0])high = mid - 1;
			else low = mid + 1;
		}
		for (j = i - 1; j >= high + 1; --j)
			a[j + 1] = a[j];
		a[j] = a[0];
	}
}

希尔排序(减少增量排序)

将原数组分割为如L[i,i+d,i+2d,...,i+kd]的子表,对每个子表进行直接插入排序,然后取d2<d,重复此过程,直至d为1

void shellsort(element a[], int n) {
	int d, i, j;
	for(d=n/2;d>0;d=d/2)
		for(i=d+1;i<=n;i++)
			if (a[i] < a[i - d]) {
				a[0] = a[i];
				for (j = i - d; a[j] > a[0] && j > 0; j -= d)
					a[j + d] = a[j];
				a[j] = a[0];
			}
}

空间复杂度依然为O(1)

时间复杂度在特定范围内普遍认为在O(n^1.3),最坏情况为O(n^2)

算法不稳定,适用于顺序存储的线性表

二、交换排序

冒泡排序

两两比较相邻元素的值,若逆序则交换

每趟排序可使最小(或最大)元素交换到第一个(或最后一个位置)

void bubblesort(element a[], int n) {
	for (int i = 0; i < n; i++) {
		bool flag = false;
		for(int j=n-1;j>i;j--)
			if (a[j - 1] > a[j]) {
				swap(a[j - 1], a[j]);
				flag = true;
			}
		if (!flag) return;
	}
}

(此伪代码为排递增序列,每次选一个最小值)

空间复杂度:O(1)

时间复杂度:最好情况:本身有序,仅比较n次,无交换,O(n)

                     平均和最坏情况:O(n^2)

                     最坏情况:逆序,n-1躺排序,第i躺比较n-i次,每次移动需要交换3个元素(swap实质就是开一个辅助变量交换一下)

算法稳定,适用于顺序存储和链式存储的线性表

快速排序

有多种实现方法,考研主体使用递归的挖坑法(分治思想)

在排序序列中任取一个元素作为枢轴,通过一趟排序将序列划分为两个独立部分L[1..k-1],L[k+1...n]

左边各元素均小于枢轴L[k],右边皆大于L[k],然后在两个子表中重复此过程

注意:每次不一定能划分为2个部分,如第一次选择枢轴若最终位置在首部或尾部,则下次也只选择一个枢轴,而若第一次选择的枢轴最终位置在中间部分,下次在两个子表中各选一个新枢轴

int partion(element a[], int low, int high) {
	element 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;
}

void quiksort(element a[], int low, int high) {
	if (low < high) {
		int pivoitpos = partion(a, low, high);
		quiksort(a, low, pivoitpos);
		quiksort(a, pivoitpos + 1, high);
	}
}

空间复杂度:因使用递归,所以取决于递归调用的最大层数,最好为O(log2(n)),最坏为O(n) 【想象一下二叉排序树,枢轴为根节点】

时间复杂度:最坏情况:基本有序(逆序),则深度可达n,为O(n^2)

算法不稳定,适用于顺序存储的线性表

三、选择排序

简单选择排序

每趟选一个剩余序列中最大(小)的元素

元素的移动次数最大不会超过3*(n-1)次

元素的比较次数于初始状态无关,必为n*(n-1)/2

void selectsort(element a[], int n) {
	for (int i = 0; i < n; 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]);
	}
}

(此伪代码为每次选最小元素)

空间复杂度:O(1)

时间复杂度:因为比较次数恒定,时间复杂度也恒定为O(n^2)

算法不稳定,适用于顺序存储和链式存储的线性表及关键字较少的情况

关于为什么不稳定大家可以试一下(2,2,1)这个序列就明白了

堆排序

可将堆视为完全二叉树

大根堆:任一非根结点的值小于其双亲结点值

小根堆:与大根堆相反

创建初始堆:
从n/2-1到1进行调整,

调整过程:看要调整结点的值是否大于左右子结点的值,若不大于,与较大者交换,交换后下面的堆可能出现问题,需要继续往下调整

堆排序(仅适用于顺序存储的线性表)
输出堆顶元素,将堆顶元素与最后一个元素互换,将堆从1开始进行调整(堆元素-1,原堆顶元素不参与调整,后序同理)

(建立大根堆)

void buildheap(element a[], int n) {
	for (int i = n / 2; i > 0; i--)
		headadjust(a, i, n);
}

void headadjust(element a[], int k, int n) {
	a[0] = a[k];
	for (int i = 2 * k; i <= n; i *= 2) {
		if (i < n && a[i] < a[i + 1])i++;
		if (a[0] >= a[i])break;
		a[k] = a[i];
		k = i;
	}
	a[k] = a[i];
}

(堆排序)

void heapsort(element a[], int n) {
	buildheap(a, n);
	for (int i = n; i > 1; i--) {
		swap(a[i], a[1]);
		headadjust(a, 1, i - 1);
	}
}

空间复杂度为O(1)

时间复杂度:建堆时关键字比较次数不超过4n,时间间为O(n),排序时间为O(n*log2(n))

                        所以总时间复杂度为O(n*log2(n))

算法不稳定,适用于顺序存储的线性表,蛇关键字多的情况

四、归并排序

以二路归并进行分析:

将两个有序表合并成一个新的有序表,n个元素,可视为n个元素为1的有序表,逐渐合并直到合并成一个长度为n的有序表为止

element* b=new element[n]
void merge(element 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; i <= mid && j <= high; k++)
		a[k] = min(a[i], a[j]);
	while (i <= mid)a[k++] = b[i++];
	while (j <= high)a[k++] = b[j++];
}

void mergesort(element a[], int low, int high) {
	if (low < high) {
		int mid = low + high >> 1;
		mergesort(a, low, mid);
		mergesort(a, mid+1, high);
		merge(a, low, mid, high);
	}
}

空间复杂度:递归调用堆栈为O(log2(n)),辅助空间为O(n) ,总体为O(n)

时间复杂度:每次归并为O(n),共cell(log2(n))次,因此为O(nlog2(n))

算法稳定,适用于顺序存储和链式存储的线性表

五、基数排序(桶排序)

不基于比较和移动进行排序

MSD(最高位优先) LSD(最低位优先)

采用r个链式队列存储和排序(r为进制即基数)

空间复杂度:O(r)

时间复杂度:需要进行d趟分配和收集,每趟分配需要遍历所有关键字O(n),每趟收集需要合并r个队列O(r),因此总时间复杂度为O(d*(n+r)),与序列初始状态无关

算法稳定,适用于顺序存储和链式存储的线性表

六、计数排序

不基于比较进行排序,空间换时间

思想:对每个待排序元素x,统计小于x的元素个数

需要两个辅助数组,b用于存放输出的排序序列,c用于存储计数值

void countsort(element a[], element b[], int n, int k) {
	int i, c[k];
	for (i = 0; i < k; i++)
		c[i] = 0;
	for (i = 0; i < n; i++)
		c[a[i]]++;
	for (i = 1; i < n; i++)
		c[i] += c[i - 1];
	for (i = n - 1; i >= 0; i--) {
		c[a[i]]--;
		b[c[a[i]]] = a[i];
	}
}

空间复杂度:O(n+k)

时间辅助度:根据for循环可得为O(n+k),k=O(n)时,复杂度为O(n),当k>O(n*logn)时,性能明显下降

算法稳定,适用于序列中元素为整数且元素范围不能太大

外部排序

通常采用归并算法

1)根据内存缓冲区大小,将外存上的文件分成若干长度为l的子文件,依次读入内存并利用内部排序算法对它们进行排序,并将排序后的有序子文件(归并段)重新写回外存

2)对归并段进行逐趟归并,使归并段逐渐由小到大

外部排序的总时间=内部排序的时间+外存读写时间+内部归并时间

每趟读次数=每趟写次数=总块数

初始归并段数量=总容量/内存分配大小

内存缓冲分配如下:

无论几路归并,至少需要一个输出缓冲区,

若想实现输入/内部归并/输出的并行处理

需在原有缓冲区数量基础上*2

未优化的归并:

(k路)在k个元素中选择最小的元素需要k-1次比较,每趟归并n个元素需要(n-1)(k-1)次比较,

共s趟归并,总比较次数为:S(n-1)(k-1)

败者树(减少比较次数):仅需比较cell(log2(k))次

置换-选择排序(减少初始归并段数)

由置换-选择排序生成的初始归并段的长度可能是不同的,此时需要最佳归并数

所谓最佳归并数其实就是哈夫曼树,但必须保证可以进行k路归并

设度为0的结点有n0个,度为k的结点有nk个,总结点n个

n=nk+n0

n=nk*k+1

对于严格的k叉树

有n0=(k-1)*nk-1,nk=(n0-1)/(k-1)

若(n0-1)%(k-1)=u!=0,则需要增加k-u-1个空归并段

归纳(重点):

1、稳定的算法有直接插入排序,折半插入,冒泡,基数,计数,归并

    不稳定的算法有希尔,简单选择,快速,堆

2、直接插入,冒泡,简单选择,折半插入的平均时间复杂度都是O(n^2),最好情况下,插入和冒泡的时间复杂度为O(n),简单选择不变,它们主要用于数据较小的情况(n<10000)

3、快速排序,堆排序,二路归并排序的平均时间复杂度都是O(nlog2(n)),最坏情况下快速排序时间复杂度变为O(n^2),空间复杂度变为O(n),堆排序,归并不变

4、比较次数与序列初始状态无关的有简单选择排序,基数排序

5、不基于比较的排序有基数排序和计数排序

手敲不易,感谢点赞

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值