前言:继续肝数据结构与算法课程笔记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暂且更新完成,后续有进一步学习,继续分享。
庆幸活在当下,感恩开源生态背后的各位大佬的无私奉献
#########################
不积硅步,无以至千里
好记性不如烂笔头
感谢小甲鱼老师
截图权利归原作者所有