数据结构与算法碎片积累(十)

前言:继续肝数据结构与算法课程笔记blog

1、排序算法

1)排序:
假设含有n个记录的序列为{r1,r2,…,rn},其相应的关键字分别为{k1,k2,…,kn},需确定1,2,…,n的一种排序p1,p2,…,pn,使其相应的关键字满足,kp1<=kp2<=…<=kpn非递减(或非递增)关系,即使序列成为一个按关键字有序的序列{rp1,rp2,…,rpn},这样的操作就被称为排序。

2)排序的稳定性
假设ki=kj(1<=i<=n,1<=j<=n,i!=j),且在排序前的序列中ri领先于rj(i<j)
----如果排序后ri仍领先于rj,则称所用的排序方法是稳定的;
----反之,若可能使得排序后的序列中rj领先ri,则称所用的排序算法是不稳定的
在这里插入图片描述
【不稳定原因,两者都是731情况下,小甲鱼编号为1,怡静为4,但是排在小甲鱼前面】

3)影响排序算法性能的几个要素:
----时间性能
----辅助空间
----算法的复杂性

2、冒泡排序

1)冒泡排序的基本思想:两两相邻记录的关键字,如果反序则交换,直到没有反序的记录为止

2)冒牌排序的要点:
----两两表示的是相邻的两个元素的意思
----如果有n个元素需要比较n-1次,每一轮减少1次比较
----冒泡排序,因为其从下往上两两比较,所以看上去的就跟泡泡往上冒一样。

3)代码实现:

//210205
#include<stdio.h>

//初始版本
void BubbleSort_first(int k[],int n) {
	int i, j, temp,count1=0,count2=0;
	for (i = 0; i < n - 1; i++) {
		for (j = i + 1; j < n; j++) {
			count1++;
			if (k[i] > k[j]) {
				temp = k[j];
				k[j] = k[i];
				k[i] = temp;
				count2++;
			}
		}
	}
	printf("类似冒泡排序,%d次比较,%d次移动\n", count1, count2);
}

//冒泡排序
void BubbleSort(int k[], int n) {
	int i, j, temp,count1=0,count2=0,flag;

	flag = 1;
	for (i = 0; i < n - 1&&flag; i++) {
		for (j = n-1;j >i; j--) {
			count1++;
			flag = 0;
			if (k[j-1] > k[j]) {
				temp = k[j-1];
				k[j-1] = k[j];
				k[j] = temp;
				count2++;
				flag = 1;
			}
		}
	}
	printf("纯正冒泡排序,%d次比较,%d次移动\n", count1, count2);
}

int main() {
	int i, a[10] = { 5,2,6,0,3,9,1,7,4,8 };
	//BubbleSort_first(a, 10);
	BubbleSort(a, 10);

	printf("排序后的结果是:");
	for (i = 0; i < 10; i++) {
		printf("%d", a[i]);
	}
	printf("\n\n");
	return 0;
}

3、选择排序

1)选择排序算法就是通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1<=i<=n)个记录交换。

2)代码实现

//210205
#include<stdio.h>

void SelectSort(int k[], int n) {
	int i, j, min, temp,count1=0,count2=0;
	for (i = 0; i < n - 1; i++) {
		min = i;
		for (j = i + 1; j < n; j++) {
			if (k[j] < k[min]) {
				min = j;
			}
			count1++;
		}//找最小的下标

		if (min != i) {
			temp = k[min];
			k[min] = k[i];
			k[i] = temp;
			count2++;
		}//如果属于最小,则交换
	}
	printf("选择排序,%d次比较,%d次移动\n", count1, count2);
}

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

	printf("排序后的结果是:");
	for (i = 0; i < 10; i++) {
		printf("%d", a[i]);
	}
	printf("\n\n");
	return 0;
}

4、直接插入排序

1)直接插入排序算法(straight insertion sort)的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增加1的有序表。

2)代码实现

//210205
#include<stdio.h>

void InsertSort(int k[], int n) {
	int i, j, temp;

	for (i = 1; i < n; i++) {
		if (k[i] < k[i - 1]) {
			temp = k[i];
			for (j = i - 1; k[j] > temp; j--) {
				k[j + 1] = k[j];
			}
			k[j + 1] = temp;
		}
	}
}

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

	printf("排序后的结果是:");
	for (i = 0; i < 10; i++) {
		printf("%d", a[i]);
	}
	printf("\n\n");
	return 0;
}

5、希尔排序

1)希尔排序是直接插入排序的改进。

2)适用于基本有序、记录数少情况。

3)基本操作,先分为大组交换排序,第二次较小组比较交换排序,循环往复直到两个一组的比较交换情形。

4)代码实现

//210205
#include<stdio.h>

//希尔排序
void InsertSort(int k[], int n) {
	int i, j, temp;
	int gap = n;

	do
	{
		gap = gap / 3 + 1;
		for (i = gap; i < n; i++) {
			if (k[i] < k[i - gap]) {
				temp = k[i];
				for (j = i - gap; k[j] > temp; j-=gap) {
					k[j + gap] = k[j];
				}
				k[j + gap] = temp;
			}
		}
	} while (gap>1);
}

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

	printf("排序后的结果是:");
	for (i = 0; i < 10; i++) {
		printf("%d", a[i]);
	}
	printf("\n\n");
	return 0;
}

6、堆排序

1)堆排序,第一次找第一小,第二次找第二次小,依次类推【现在看这句话有点迷惑211010,是不是大顶堆情况?还是小顶堆情况?还是通用堆排序?】

2)
在这里插入图片描述
结点的值都是大于或等于其左右孩子的值,称为大顶堆。小顶堆概念类似,结点的值小于或等于其左右孩子的值,称为小顶堆。

3)要点:
根结点一定是堆中所有结点最大或者最小者,如果按照层序的方式给结点从1开始编号,则结点之间满足如下关系:
在这里插入图片描述
3-1)左侧公式,结点比左右孩子都大,表示大顶堆;右侧公式,结点比左右孩子都小,表示小顶堆。

3-2)下标i与2i和2i+1是双亲和子女关系

3-3)把大顶堆和小顶堆用层序遍历存入数组,则一定满足上面的表达式
在这里插入图片描述
在这里插入图片描述
4)堆排序(heap sort)就是利用堆进行排序的算法,其基本思想是:
----将待排序的序列构造一个大顶堆(或小顶堆)
----此时,整个序列的最大值(最小值)就是堆顶的根结点。将该根结点移走(就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值(最小值))
----然后,将剩余的n-1个序列重新构造一个堆,就会得到n个元素中的最大值(最小值)
----如此反复执行,便能得到一个有序序列

这里的堆遍历,采用的是层序遍历方式。【结合上图中二叉树以及数组来看】

5)代码实现

从下到上采用层序遍历方式,实现堆调整

//210205
#include<stdio.h>
int count=0;

//交换元素
void swap(int k[], int i, int j) {
	int temp;
	temp = k[i];
	k[i] = k[j];
	k[j] = temp;
}

//调整顶堆,从下往上调整
void HeapAdjust(int k[], int s, int n) {
	int i,temp;

	temp = k[s];//保存的是当前待调整双亲结点
	
	for (i = 2 * s; i <= n; i *= 2) {//i =2*s表示的是左孩子;i *= 2表示下一个结点
		count++;
		if (i<n&&k[i] < k[i + 1]) {//i<n用于确定不是最后一个结点;k[i]表示左孩子;k[i + 1]表示右孩子
			i++;
		}

		if (temp >= k[i]) {//如果待调整双亲结点大于或等于孩子,则跳出循环
			break;
		}

		k[s] = k[i];//如果待调整双亲结点不大于,则把较大的孩子结点赋值给双亲结点
		s = i;	//双亲待存放的位置保存在s处
		
	}

	k[s] = temp;
}

//堆排序
void HeapSort(int k[], int n) {
	int i;

	//构造大顶堆
	for (i = n / 2; i > 0; i--) {
		HeapAdjust(k, i, n);
	}

	for (i = n; i > 1; i--) {
		swap(k, 1, i);
		HeapAdjust(k, 1, i-1);
	}
}

int main() {
	int i, a[10] = { -1,5,2,6,0,3,9,1,7,4 };
	HeapSort(a, 9);
	printf("%d次比较\n",count);
	printf("排序后的结果是:");
	for (i = 0; i < 10; i++) {
		printf("%d", a[i]);
	}
	printf("\n\n");
	return 0;
}
//调试有问题,老师调试没有-1的,而这里调试打印有-1

7、归并排序

1)归并排序(Merge Sort)就是利用归并的思想实现的排序方法。原理是,假设初始排序有n个记录,则可看成是n个有序的子序列,每个子序列的长度为1,然后两两合并,得到n/2(上取整)个长度为2或1的有序子序列;再两两归并,…,如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序。
在这里插入图片描述
2)原理图貌似能够理解,但代码实现没理解,需要跟着上面图进行理解。

3)递归实现,不断分为两半,直到分到离散结点为止,然后对离散结点进行排序并合并,返回上一级,直到合并完整。
在这里插入图片描述
4)代码实现1(递归方式)

//210205
#include<stdio.h>
#define MAXSIZE 10

//实现归并,并把最后的结果存放到list1里
void merging(int *list1,int list1_size,int *list2,int list2_size) {
	int i, j, k,m;
	int temp[MAXSIZE];

	i = j = k = 0;

	while (i<list1_size&&j<list2_size)
	{
		if (list1[i] < list2[j]) {
			temp[k++] = list1[i++];
		}
		else
		{
			temp[k++] = list2[j++];
		}
	}
	while (i<list1_size)
	{
		temp[k++] = list1[i++];
	}
	while (j<list2_size)
	{
		temp[k++] = list2[j++];
	}

	for (m = 0; m < (list1_size + list2_size); m++) {
		list1[m] = temp[m];
	}
}

//递归方式实现归并排序
void MergeSort(int k[], int n) {
	if (n > 1) {
		int* list1 = k;
		int list1_size = n / 2;
		int* list2 = k + n / 2;
		int list2_size = n - list1_size;

		MergeSort(list1, list1_size);
		MergeSort(list2, list2_size);

		merging(list1, list1_size, list2, list2_size);//合并
	}
}

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

	printf("排序后的结果是:");
	for (i = 0; i < 10; i++) {
		printf("%d", a[i]);
	}
	printf("\n\n");
	return 0;
}
//没懂,应该对照word里面的那个图进行理解这里的迭代方式实现

5)代码实现2(迭代方式)

//210205
#include<stdio.h>
#include<stdlib.h>
#define MAXSIZE 10

//迭代实现
void MergeSort(int k[], int n) {
	int i, left_min, left_max, right_min, right_max;
	int* temp = (int*)malloc(n * sizeof(int));

	for (i = 1; i < n; i *= 2) {
		for (left_min = 0; left_min < n - i; left_min = right_max) {
			right_min = left_max = left_min + i;
			right_max = left_max + i;

			if (right_max > n) {
				right_max = n;
			}

			int next = 0;

			while (left_min<left_max&&right_min<right_max)
			{
				if (k[left_min] < k[right_min]) {
					temp[next++] = k[left_min];
				}
				else
				{
					temp[next++] = k[right_min];
				}
			}

			while (left_min<left_max)
			{
				k[--right_min] = k[--left_min];
			}

			while (next>0)
			{
				k[--right_min] = temp[--next];
			}
		}
	}

}

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

	printf("排序后的结果是:");
	for (i = 0; i < 10; i++) {
		printf("%d", a[i]);
	}
	printf("\n\n");
	return 0;
}
//有bug,写入访问权限出错
//原理以及代码都没有理解

8、快速排序

在这里插入图片描述
1)快速排序是冒泡排序的升级版本

2)冒泡排序是两两比较决定是否交换位置进行排序的;而快速排序是选着一个作为基准,大于基准就放在基准一侧,小于则放在另一侧。

3)代码实现

//210205
#include<stdio.h>
#include<stdlib.h>

void swap(int k[],int low,int high) {
	int temp;

	temp = k[low];
	k[low] = k[high];
	k[high] = temp;
}

int Partition(int k[],int low,int high) {
	int point;		//基准点

	point = k[low];

	while (low<high)
	{
		while (low<high&&k[high]>=point)//这里的=号不能缺
		{
			high--;
		}//从主函数数组右边开始过滤掉比基准点大的元素
		swap(k, low, high);	

		while (low<high&&k[low]<=point)//这里的=号不能少
		{
			low++;
		}
		swap(k, low, high);
	}
	return low;
}

void QSort(int k[], int low, int high) {
	int point;
	if (low < high) {
		point = Partition(k,low,high);//Partition()将大于基准点的点放在基准点右边,小于基准点放在基准点左边
		
		QSort(k, low, point - 1);//递归调用基准点的左边

		QSort(k, point + 1, high);//递归调用基准点的右边
	}
}

void QuickSort(int k[], int n) {
	QSort(k, 0, n - 1);//0表示初始位置,n-1表示最尾位置,排序后也属于最大值位置
}

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

	printf("排序后的结果是:");
	for (i = 0; i < 10; i++) {
		printf("%d", a[i]);
	}
	printf("\n\n");
	return 0;
}

//代码理解有待继续琢磨

9、快速排序的优化

1)优化方向:
----优化选取基准点

----优化不必要的交换

----优化小数组时的排序方案

----优化递归操作

2)什么是尾递归?如果一个函数中递归形式的调用出现在函数的末尾,我们称这个递归函数属于尾递归。

3)当编译器检测到一个函数调用的是尾递归时,它就覆盖当前的活跃记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率变得更高。

因此,只要有可能就需要将递归函数写曾尾递归的形式。

4)自我评价,代码没懂,这四种优化方法。语义上面还能理解,但是到了代码层就是一脸懵逼了。

5)代码实现(优化方向前三者):

//210205
#include<stdio.h>

//优化小数组时的排序方案
#define MAX_LENGTH_INSERT_SORT 7

//方法三:优化小数组时的排序方案
void ISort(int k[], int n) {
	int i, j, temp;
	for (i = 1; i < n; i++) {
		if (k[i] < k[i - 1]) {
			temp = k[i];

			for (j = i - 1; k[j] > temp; j--) {
				k[j + 1] = k[j];
			}
			k[j + 1] = temp;
		}
	}
}//方法三:优化小数组时的排序方案

void InsertSort(int k[], int low, int high) {
	ISort(k + low, high - low + 1);
}

void swap(int k[], int low, int high) {
	int temp;

	temp = k[low];
	k[low] = k[high];
	k[high] = temp;
}

int Partition(int k[], int low, int high) {
	int point;		//基准点

	方法一:优化基准点操作
	//int m = low + (high - low) / 2;
	//if (k[low] > k[high]) {
	//	swap(k, low, high);
	//}
	//if (k[m] > k[high]) {
	//	swap(k, m, high);
	//}
	//if (k[m] > k[low]) {
	//	swap(k, m, low);
	//}//优化基准点操作
	
	point = k[low];

	while (low < high)
	{
		while (low < high && k[high] >= point)//这里的=号不能缺
		{
			high--;
		}//从主函数数组右边开始过滤掉比基准点大的元素
		
		//方法二:优化不必要交换
		//swap(k, low, high);//原始的
		k[low] = k[high];

		while (low < high && k[low] <= point)//这里的=号不能少
		{
			low++;
		}

		//方法二:优化不必要交换
		//swap(k, low, high);//原始的
		k[high] = k[low];
	}

	//方法二:优化不必要交换
	k[low] = point;

	return low;
}

void QSort(int k[], int low, int high) {
	int point;
	//if (low < high) {
	//	point = Partition(k, low, high);//Partition()将大于基准点的点放在基准点右边,小于基准点放在基准点左边

	//	QSort(k, low, point - 1);//递归调用基准点的左边

	//	QSort(k, point + 1, high);//递归调用基准点的右边
	//}

	//方法三:优化小数组时的排序方案
	if (high-low>MAX_LENGTH_INSERT_SORT) {
		point = Partition(k, low, high);//Partition()将大于基准点的点放在基准点右边,小于基准点放在基准点左边

		QSort(k, low, point - 1);//递归调用基准点的左边

		QSort(k, point + 1, high);//递归调用基准点的右边
	}
	else
	{
		InsertSort(k, low, high);
	}	//方法三:优化小数组时的排序方案
}

void QuickSort(int k[], int n) {
	QSort(k, 0, n - 1);//0表示初始位置,n-1表示最尾位置,排序后也属于最大值位置
}

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

	printf("排序后的结果是:");
	for (i = 0; i < 10; i++) {
		printf("%d", a[i]);
	}
	printf("\n\n");
	return 0;
}

6)代码实现(优化方向最后一者,递归优化):

//210205
#include<stdio.h>

//优化小数组时的排序方案
#define MAX_LENGTH_INSERT_SORT 7

//方法三:优化小数组时的排序方案
void ISort(int k[], int n) {
	int i, j, temp;
	for (i = 1; i < n; i++) {
		if (k[i] < k[i - 1]) {
			temp = k[i];

			for (j = i - 1; k[j] > temp; j--) {
				k[j + 1] = k[j];
			}
			k[j + 1] = temp;
		}
	}
}//方法三:优化小数组时的排序方案

void InsertSort(int k[], int low, int high) {
	ISort(k + low, high - low + 1);
}

void swap(int k[], int low, int high) {
	int temp;

	temp = k[low];
	k[low] = k[high];
	k[high] = temp;
}

int Partition(int k[], int low, int high) {
	int point;		//基准点

	方法一:优化基准点操作
	//int m = low + (high - low) / 2;
	//if (k[low] > k[high]) {
	//	swap(k, low, high);
	//}
	//if (k[m] > k[high]) {
	//	swap(k, m, high);
	//}
	//if (k[m] > k[low]) {
	//	swap(k, m, low);
	//}//优化基准点操作

	point = k[low];

	while (low < high)
	{
		while (low < high && k[high] >= point)//这里的=号不能缺
		{
			high--;
		}//从主函数数组右边开始过滤掉比基准点大的元素

		//方法二:优化不必要交换
		//swap(k, low, high);//原始的
		k[low] = k[high];

		while (low < high && k[low] <= point)//这里的=号不能少
		{
			low++;
		}

		//方法二:优化不必要交换
		//swap(k, low, high);//原始的
		k[high] = k[low];
	}

	//方法二:优化不必要交换
	k[low] = point;

	return low;
}

void QSort(int k[], int low, int high) {
	int point;
	//if (low < high) {
	//	point = Partition(k, low, high);//Partition()将大于基准点的点放在基准点右边,小于基准点放在基准点左边

	//	QSort(k, low, point - 1);//递归调用基准点的左边

	//	QSort(k, point + 1, high);//递归调用基准点的右边
	//}

	//方法三:优化小数组时的排序方案
	if (high - low > MAX_LENGTH_INSERT_SORT) {
		
		//方法四:优化递归操作
		while (low<high)
		{
			point = Partition(k, low, high);

			if (point - low < high - point) {
				QSort(k, low, point - 1);

				low = point + 1;
			}
			else
			{
				QSort(k, point + 1, high);
				high = point - 1;
			}
		}//这里循环替换了一个递归调用
		//方法四:优化递归操作

		//point = Partition(k, low, high);//Partition()将大于基准点的点放在基准点右边,小于基准点放在基准点左边

		//QSort(k, low, point - 1);//递归调用基准点的左边

		//QSort(k, point + 1, high);//递归调用基准点的右边
	}
	else
	{
		InsertSort(k, low, high);
	}	//方法三:优化小数组时的排序方案
}

void QuickSort(int k[], int n) {
	QSort(k, 0, n - 1);//0表示初始位置,n-1表示最尾位置,排序后也属于最大值位置
}

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

	printf("排序后的结果是:");
	for (i = 0; i < 10; i++) {
		printf("%d", a[i]);
	}
	printf("\n\n");
	return 0;
}

10、总结回顾(排序算法)

在这里插入图片描述
1)根据排序是否全部放置在内存中,划分为内排序和外排序。

2)外排序,需要考虑内外层多次交换数据产生的消耗
在这里插入图片描述在这里插入图片描述
这一节对前面几个排序算法进行总结。实际上,没有一个适用任何场景的完美算法,只是根据实际应用情景情况而选择相应的算法。

自我评价,太菜了我,还得多多敲代码,多多查看资料学习!!!

至此,数据结构与算法系列笔记blog暂且更新完成,后续有进一步学习,继续分享。

庆幸活在当下,感恩开源生态背后的各位大佬的无私奉献

#########################
不积硅步,无以至千里
好记性不如烂笔头
感谢小甲鱼老师
截图权利归原作者所有

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值