数据结构Chap 8/排序

一、排序分类

排序的稳定性:关键字相同的元素经过排序后,相对顺序是否会改变。

1.插入排序

省流版:找出最值并往前放。

(1)代码

/*1.不带哨兵的插入排序*/
void InsertSort(int A[], int n)
{
	int i, j, temp;
	for (i = 1; i < n; i++)
	{
		if (A[i] < A[i - 1])//现在看下第i位是否需要放,若比最大的还小,则需要插入
			temp = A[i];	//待放值先放到一个temp里
		for (j = i - 1; j >= 0 && A[j] > temp; --j)//i之前的数,若比我大,都往后稍稍
			A[j + 1] = A[j];//第j位把后面的位置占了,直到第二项不再满足,j+1项还空着
		A[j + 1] = temp;//temp占j+1位。
	}
}
/*2.把自己当哨兵的插入排序*/
void Insertsort(int A[], int n)
{
	int i, j;
	for (i = 2; i <= n; i++)//第0位是待覆盖的哨兵位,第1位肯定不需要排序,从第2开始排
		A[0] = A[i];//覆盖哨兵位
	for (j = i - 1; A[j] > A[0]; j--)
		A[j + 1] = A[j];
	A[j + 1] = A[0];
}

注意:插入排序带有稳定性。带哨兵的方式优点:不需要每轮都判断j是否大于等于0【防止越界】

不带哨兵,i从1开始,带哨兵的话i从2开始

2.折半插入排序:

省流版:找出最值并往前放。

void InsertSort(int A[],int n)/*不带哨兵*/
{
	int temp;
	int low, mid, high;
	for (int i = 1; i < n; i++)
	{
		low = 0;
		high = i-1;
		temp = A[i];
		while (high >= low)
		{
			mid = (low + high) / 2;
			if (A[mid] > temp)
				high = mid - 1;
			else
				low = mid + 1;
		}
		for (int j = i-1; j > high; j--)
			//从有序的里面排序,所以从i-1开始,所以不是n
			A[j + 1] = A[j];
		A[low] = temp;
	}
}
int main()
{
	int i;
	int a[8] = { 8,4,6,2,9,3,7,3 };
	cout << "Before:"<<endl;
	for (i = 0; i < 8; i++)
		cout << a[i] << endl;
	InsertSort(a, 8);
	cout << "After:"<<endl;
	for (i = 0; i < 8; i++)
		cout << a[i] << endl;
	return 0;
}

折半插入排序其实是在直接插入排序的基础上,结合了二分查找法的思想,顺序的二分查找替代了直接插入排序中遍历查找的过程,从而更快的能够确定待插入元素的位置,但是由于移动次数并没有发生改变,所以两者的时间复杂度相同(均为最小n、最大n方)。折半插入排序是稳定的,其时间复杂度为O(N^{2})

 3.希尔排序

(1)原理:利用增量预处理,实现直接插入前:数列是基本有序的。

(2)代码:

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

时间复杂度为O(n^1.3)

希尔排序是不稳定的排序算法(因为跳着排序,不是依次那种)

 二.交换排序

 1.冒泡排序:

(1)意义

单向冒泡:每趟可以确定前一位最小数字

双向冒泡:

奇数趟时,从前往后比较,遇到逆序就交换,直到将最大元素移动到尾部。

偶数趟时,从后往前比较,遇到逆序就交换,直到将最小元素移动到首位。

(2)代码

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++)//每一趟会确定前面一位数字
	{
		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 == false)
			return;
	}
}
void D_BubbleSort(int A[], int n)//双向起泡排序,交替两个方向起泡
{
	int low = 0, high = n - 1;
	bool flag = true;//是否发生交换的标志,若没有,则可以退出循环
	while (low < high && flag)
	{
		flag = false;//每次置flag为false,进入循环
		for (int i = low; i < high; i++)//找一个最大的放右边
			if (A[i] > A[i + 1])
			{
				swap(A[i], A[i + 1]);
				flag = true;//发生交换,下次冒泡还需要进行。
			}
		high--;
		for (int i =high; i >low; i--)
			if (A[i] < A[i - 1])
			{
				swap(A[i], A[i - 1]);
				flag = true;//发生交换,下次冒泡还需要进行。
			}
		low++;
	}
}

(3)性能分析

空间复杂度:O(1)

时间复杂度:平均为O(n^2)

最好情况=比较n-1  交换0  所以最好情况:O(n)

最坏情况=比较n(n-1)/2  交换是比较的3倍。 所以最坏情况O(n^2)

冒泡排序是稳定的排序。

2.快速排序

(1)分层

(2)代码

int Partition(int A[], int low, int high)
{
	int pivot = A[low];
	while (low < high)
	{
		while (low < high && A[high] >= pivot)	--high;//如果high所指的更大,则让high一直左移,直到不满足上述条件
		A[low] = A[high];//出现high小的,换到pivot位置上
		while (low < high && A[low] <= pivot)   ++low;//LOW右移
		A[high] = A[low]; 
	}
	A[low] = pivot;
	return low;
}
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);
	}
}

(3)算法效率

快排算法优化思路: 尽量选择可以把数据中分的枢轴元素。eg:选头、中、尾三个位置的元素,取中间值作为枢轴元素/随机选一个元素作为枢轴元素。

(4)稳定性:快排不稳定

三、选择排序

1.简单选择排序

(1)代码

void SelectSort(int A[], int n)
{
	for (int i; 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]);
	}
}

(2)效率及稳定性

2.堆排序

(1)代码

/*1.大根堆第k个元素下坠过程*/
void HeadAdjust(int A[], int k, int len)
{
	A[0] = A[k];
	for (int i = 2 * k; i <= len; i *= 2)
	{
		if (i < len && A[i] < A[i + 1])
			i++;
		if (A[0] >= A[i]) break;
		else
		{
			A[k] = A[i];
			k = i;
		}
	}
}
/*2.建立大根堆*/
void BuildMaxHeap(int A[], int len){
	for (int i = len / 2; i > 0; i--)
		HeadAdjust(A, i, len);
}
/*3.利用已有的大顶堆进行堆排序*/
void HeapSort(int A[], int len)
{
	BuildMaxHeap(A, len);
	for (int i = len; i > 1; i--){
		swap(A[i], A[1]);
		HeadAdjust(A, 1, i - 1);
	}
}

(2)效率及稳定性 

 (3)插入和删除


插入:放在队尾,再利用HeadAdjust进行调整。

删除:用队尾元素补齐,再利用HeadAdjust进行调整。

四、归并排序和基数排序

1.归并排序

(1)代码

/*内部排序利用二路归并*/
//int* B = (int*)malloc(n * sizeof(int));
int* B = new(n * sizeof(int));
//左右数组各自有序后,进行合并
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表明即将带入的位置
			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);
	}
}

 (2)效率及稳定性

2.基数排序

(1)含义:

特殊的排序算法,因为不是先比整体大小,而是先把关键字拆分成d组,再整合。

 (2)应用:

 五、外部排序

1.难点:

数据元素太多,无法一次全部读入内存进行排序

2.思想:

使用“归并排序”的方法,最少只需在内存中分配3块大小的缓冲区(关键)即可对任意一个大文件进行排序【多路归并】

3.时间开销优化办法:尽量减少归并趟数(让开辟内存的缓冲区空间r更大) 

 关于归并段的数量:一共16块数据,进行生成初始归并段时,已经采用内部排序的办法让其成为8个有序的归并段,每个归并段已经是有序的了,所以只剩8个初始归并段(且每个段占两块

关于读写磁盘次数:32+32*3=128 第一个32是指生成初始归并段,16段数据读入、写出各一次,一共32次,后面是因为将整片数据归并一趟,就需要花一个32,这里归并了3趟,所以需要3个32.

 直观地说:读写外存时间与文件大小以及归并趟数有关,文件大小固定,所以为了减少读写次数就需要减少归并趟数,就必须加大每次读写的量,(每次抄的量大,看书的次数就变少)即不只采用2个输入缓冲区,而是采用4个缓冲区。

重要结论:采用多路归并可以减少归并趟数(K叉树)或者是减少初始归并段数量(初始归并段时采用多路归并使总段数变成8而不是16,r减少),从而减少磁盘I/O(读写)次数。

缺点:K叉树会导致内存开销增大且内部对比时间开销也会增大。

【解决办法:利用败者树减少关键字对比次数、利用置换-选择排序减少初始归并段数量】

3.败者树

若1024个元素对比,普通需要对比1023次,但是败者树只需要10次,即\left \lceil log_{2}K\right \rceil

 4.置换-选择排序

(1)目的:仅仅依赖内部工作区大小,获得的归并段个数较多,且长度一致!为了探索新方法,获得更长的初始归并段,引入了置换-选择排序。

(2)含义:就是将最小的选出来,然后置换出去【但是置换的不能比之前的还小,否则冻结该窗口,所以要记录MINIMAX】。

利用空的内存工作区MA(Memory Area)以及初始归并段输出文件FO(Output File)来实现。

注意:在WA中选择MINIMAX记录的过程需要通过败者树来实现。

   例:如果WA的容纳空间为6:

 如果构成K叉树时缺少归并段怎么办,下面先给一个错误的例子

 正解如下:

注意:对于k叉归并,若初始归并段的数量无法构成严格的 k 叉归并树,则需要补充几个长度为 0 的“虚段”,再进行 k叉哈夫曼树的构造。

那么:在构造时是否需要补充虚段呢?补充虚段的个数又如何确定?

解:通过最佳归并树的规律来确定,主要是由于严格K叉树只包含度为K和0的结点(从这点看就知道第一个例子不正确)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值