当前位置: 首页 > news >正文

【异世界历险之数据结构世界(冒泡、选择、快速排序)】

前言

在这里插入图片描述

排序

排序效率测试

void TestTop() {srand(time(0));const int N1 = 100000;const int N2 = 10000000;// 动态分配内存并检查int* a1 = (int*)malloc(sizeof(int) * N1);int* a2 = (int*)malloc(sizeof(int) * N1);int* a5 = (int*)malloc(sizeof(int) * N1);int* a3 = (int*)malloc(sizeof(int) * N2);int* a4 = (int*)malloc(sizeof(int) * N2);int* a6 = (int*)malloc(sizeof(int) * N2);int* a7 = (int*)malloc(sizeof(int) * N2);if (!a1 || !a2 || !a3 ||!a4 ||!a5 ||!a6 ||!a7) {printf("Memory allocation failed!\n");free(a1);free(a2);free(a3);free(a4);free(a5);free(a6);free(a7);return;}// 填充数组for (int i = 0; i < N1; i++) {a1[i] = rand();a2[i] = a1[i];a5[i] = a1[i];}for (int i = 0; i < N2; i++){a3[i] = rand();a4[i] = a3[i];a6[i] = a3[i];a7[i] = a3[i];}// 测试 SelectSortclock_t start_time = clock();SelectSort(a1, N1);clock_t end_time = clock();double time_taken = (double)(end_time - start_time) / CLOCKS_PER_SEC;printf("SelectSort took %f seconds to sort %d elements.\n", time_taken, N1);free(a1);// 测试 InsertSortstart_time = clock();InsertSort(a2, N1);end_time = clock();time_taken = (double)(end_time - start_time) / CLOCKS_PER_SEC;printf("InsertSort took %f seconds to sort %d elements.\n", time_taken, N1);free(a2);//测试 BubbleSortstart_time = clock();BubbleSort(a5, N1);end_time = clock();time_taken = (double)(end_time - start_time) / CLOCKS_PER_SEC;printf("BubbleSort took %f seconds to sort %d elements.\n", time_taken, N1);free(a5);// 测试 ShellSortstart_time = clock();ShellSort(a3, N2);end_time = clock();time_taken = (double)(end_time - start_time) / CLOCKS_PER_SEC;printf("ShellSort took %f seconds to sort %d elements.\n", time_taken, N2);free(a3);//测试 HeapSortstart_time = clock();HeapSort(a4, N2);end_time = clock();time_taken = (double)(end_time - start_time) / CLOCKS_PER_SEC;printf("HeapSort took %f seconds to sort %d elements.\n", time_taken, N2);free(a4);//测试 QuickSortstart_time = clock();QuickSort1(a6,0,N2-1);end_time = clock();time_taken = (double)(end_time - start_time) / CLOCKS_PER_SEC;printf("QuickSort1 took %f seconds to sort %d elements.\n", time_taken, N2);free(a6);start_time = clock();QuickSort3(a7, 0, N2 - 1);end_time = clock();time_taken = (double)(end_time - start_time) / CLOCKS_PER_SEC;printf("QuickSort3 took %f seconds to sort %d elements.\n", time_taken, N2);free(a7);
}

效果展示:
在这里插入图片描述
复习+尝试之前的排序

冒泡排序

void BubbleSort(int* arr,int n)
{for (int i = 0; i < n - 1; i++){bool swapped = false;for (int j = 0; j < n - i - 1; j++){if (arr[j] > arr[j + 1]){  Swap(&arr[j], &arr[j + 1]);swapped = true;}}if (!swapped){break;}}
}

冒泡排序(Bubble Sort) 代码总结:
基本思想:
通过重复比较相邻元素,若前一个元素大于后一个元素,则交换它们的位置。经过多轮遍历,最大的元素会“冒泡”到数组的末尾,直到数组有序。
代码流程:
外层循环:控制排序的轮次,最多执行 n-1 轮。
内层循环:逐个比较相邻元素,并在需要时进行交换,逐步将较大的元素移动到数组末尾。
交换标记 swapped:用于记录本轮是否发生了交换。如果本轮没有交换,说明数组已经有序,可以提前结束排序。
优化:
如果某一轮没有发生交换,则提前退出循环,减少不必要的比较,提高效率。
时间复杂度:
最坏情况下是 O(n²),但如果数据接近有序,提前结束的机制能减少计算量。

演示
在这里插入图片描述

选择排序

方法一

void SelectSort(int* arr, int n)
{for (int i = 0; i < n-1; i++){int mini = i;for (int j = i+1; j < n; j++){if (arr[mini] > arr[j]){mini = j;}}if (mini != i){Swap(&arr[mini], &arr[i]);}}
}

选择排序(SelectSort)总结:
基本思路:每次选出当前未排序部分的最小元素,与未排序部分的第一个元素交换。
时间复杂度:
最坏和平均时间复杂度都是 O(n²),因为每一轮内层循环会遍历剩余的所有元素。
空间复杂度是 O(1),因为选择排序是就地排序,不需要额外的存储空间。

展示:
在这里插入图片描述

方法二

void SelectSort(int* arr, int n)
{  int left = 0;int right = n - 1;while (left < right){int mini = left;int maxi = right;for (int i = left + 1; i <=right; i++){if (arr[mini] > arr[i]){mini = i;}if (arr[maxi] < arr[i]){maxi = i;}}Swap(&arr[mini], &arr[left]);if (maxi == left){maxi = mini;}Swap(&arr[maxi], &arr[right]);left++;right--;}
}

易错点:

if (maxi == left){maxi = mini;}

如果没有这段代码:

在这里插入图片描述
原因:
如果maxi 与 left 相同,则arr[left]与arr[mini]交换后,值发生改变,在进行maxi与right产生错误。

总结:
基本思路:每次在未排序部分中选出最小和最大值,并将其分别放到数组的两端(left 和 right)。
时间复杂度:最坏和平均情况下,时间复杂度仍为 O(n²),因为需要通过两次遍历(内外循环)找到最小值和最大值。

快速排序

方法一(经典法)

void QuickSort(int* arr, int left,int right)
{if (left >= right) return;int pivoti = left;int begin = left;int end = right;while (begin < end){   while (begin < end && arr[end] >= arr[pivoti]) end--;while (begin < end && arr[begin] <= arr[pivoti]) begin++;Swap(&arr[begin], &arr[end]);}Swap(&arr[pivoti], &arr[begin]);QuickSort(arr, left, end - 1);QuickSort(arr, end + 1, right);
}

在这里插入图片描述

解释

1.为什么pivot为什么会找到正确排序位置?

程序扫描数组,找到比 pivot 小的元素,交换到左边;找到比 pivot 大的元素,交换到右边。
当扫描完,pivot 就处在一个位置,左边的都是小于它的,右边的都是大于它的。
这时,pivot 就被放到了正确的排序位置。

2.为什么选left,为什么先走right侧后走left侧,肯定能达到小于pivot处?
在这里插入图片描述

3.使用小于等于为什么可以?

这样做是为了防止在某些情况下,pivot 会被跳过。假设有相同的元素与 pivot 相等,使用 <= 会让相同的元素进入右侧(即 pivot 右边)。这样保证了数组中的所有元素都被处理,避免遗漏。

假设数组是 [5, 2, 8, 5, 7],pivot 为 5。
如果我们用 <=,那么当 begin 扫描到第二个 5 时,它会把它放到右边,而不会停在 pivot 之前。
如果我们用 <,可能会跳过第二个 5,使得它不正确地位于 pivot 的左边。

方案二(挖洞法)

void QuickSort2(int* arr, int left, int right)
{if (left >= right) return;int hole = left;int pivot = arr[left];int low = left;int high = right;while (low < high){while (low < high && arr[high] >= pivot) high--;arr[hole] = arr[high];hole = high;while (low < high && arr[low] <= pivot) low++;arr[hole] = arr[low];hole = low;}arr[hole] = pivot;QuickSort2(arr, left, hole - 1);QuickSort2(arr, hole + 1, right);
}

在这里插入图片描述

解释
arr[hole] = pivot;

这一步相当于经典QuickSort中的Swap,把hole左右两边分开,一边大,一边小。

方案三(前后指针)

void QuickSort3(int* arr, int left, int right)
{   if (left >= right)return;int midi = GetMidNumi(arr, left, right);if(midi != left)Swap(&arr[left], &arr[midi]);int pivoti = left;int prev = left;int cur = left + 1;while (cur <= right){if (arr[cur] < arr[pivoti] && ++prev != cur){Swap(&arr[prev], &arr[cur]);}cur++;}Swap(&arr[prev], &arr[pivoti]);QuickSort3(arr, left, prev - 1);QuickSort3(arr, prev + 1, right);
}

在这里插入图片描述

解释
if (arr[cur] < arr[pivoti] && ++prev != cur){Swap(&arr[prev], &arr[cur]);}cur++;

1.这段代码意思是如果arr[pivoti]>arr[cur],prev先++,到达下一个位置(肯定大于arr[pivoti],且prev++不是cur,防止多个小的数连续)。
2.如果arr[pivoti]>arr[cur],只有cur++。

解释三种方案的递归部分
QuickSort(arr, left, tmp - 1);  // 递归处理左边部分
QuickSort(arr, tmp + 1, right); // 递归处理右边部分

选择枢轴:首先我们选择一个枢轴 pivot,然后把数组分成两部分:
左边的部分是小于 pivot 的元素。
右边的部分是大于 pivot 的元素。
递归:对左右两部分分别再做同样的操作:
左边的部分再去选择枢轴并分成两部分,直到左边的部分只有一个元素。
右边的部分也会继续选择枢轴,直到它也只剩一个元素。
递归停止:当一个子数组只有一个元素或没有元素时,递归就停止了,因为一个元素已经是有序的了。

QuickSort总结

分治法:
分割数组:通过一个枢轴元素(pivot)将数组分成两部分,左边的部分小于枢轴,右边的部分大于枢轴。
递归排序:递归地对这两部分进行排序,直到数组完全有序。
时间复杂度:
最优情况:时间复杂度是 O(n log n),发生在每次分割都能恰好把数组分成两等部分时。
最坏情况:时间复杂度是 O(n²),发生在数组已经是有序的情况下,每次分割都只剩下一个元素。为了避免这种情况,通常会随机选择枢轴来优化。

pivot选取问题

方案一
//取中间值
int GetMidNumber(int* arr, int left, int right)
{int mid = left + right / 2;if ((arr[mid] > arr[left] && arr[mid] < arr[right])|| (arr[mid] > arr[right] && arr[mid] < arr[left])) return mid;if ((arr[right] > arr[mid] && arr[right] < arr[left])|| (arr[right] > arr[left] && arr[right] < arr[mid])) return right;return left;}
方案二
//随机pivot
int randpivot = rand()%(right-left)+left;
Swap(&arr[left], &arr[randpivot]);
pivot = left;
解释原因

如果直接取 left 作为pivot,在以下情况下会出问题:
数据已经排序或逆序:
如果数据已经有序,取 left 会导致每次只划分一个元素,会导致取值极端化,递归层次加深,性能会变差,退化成 O(n²)。
数据全相同:
如果数组元素都相同,取 left 作为枢轴无法有效划分,递归也不会减少数组的规模,浪费时间。

举例

假如数据是 [1, 2, 3, 4, 5] 这样有序的,选 1 作为枢轴时,就会发生:
第一轮划分:选 1,它小于所有其他元素,所以它就“被迫”排到最前面了,然后剩下 [2, 3, 4, 5] 继续。
第二轮划分:然后在 [2, 3, 4, 5] 里选枢轴,选出来的可能是 2,然后剩下 [3, 4, 5]。每次划分其实就“排掉了一个元素”,其他的还要继续做排序。

总结

预告:下一篇是归并排序。

http://www.lryc.cn/news/604937.html

相关文章:

  • 数据结构(7)单链表算法题OVA
  • LLM 模型部署难题的技术突破:从轻量化到分布式推理的全栈解决方案
  • 【数据结构初阶】--二叉树(五)
  • 数据结构——单链表1
  • jmeter读取上游接口并遍历数组数据并进行压测
  • Vulnhub靶场:ica1
  • 【网络运维】 Linux:使用 Cockpit 管理服务器
  • IO复用实现并发服务器
  • 2025年7月技术问答第6期
  • 无人机入门--个人笔记
  • 电力设施通道防外破防异物实时监控预警装置的核心功能是什么
  • C 语言与 C++、Java、Python 等编程语言的区别
  • 国产音频DA转换芯片DP7361支持192K六通道24位DA转换器
  • Android RTMP推送|轻量级RTSP服务同屏实践:屏幕+音频+录像全链路落地方案
  • 工业计算机ARM-如何实现工业数字化升级EC100!
  • 论文阅读|NeurIPS 2024|Mamba进一步研究|MSVMamba
  • 原生微信小程序实现语音转文字搜索---同声传译
  • NAT技术与代理服务
  • SNR-Aware Low-light Image Enhancement 论文阅读
  • 【网络工程师软考版】路由协议 + ACL
  • 15、点云<—>深度图转换原理
  • rabbitmq--默认模式(点对点)
  • 【深度学习新浪潮】3D城市建筑多样化生产的研发进展调研
  • vulhub-Thales靶机练习
  • STL学习(?、常用的算数算法和集合算法)
  • SAP-ABAP:SAP ABAP OpenSQL JOIN 操作权威指南高效关联多表数据
  • xxljob-快速上手
  • 亚马逊云科技:赋能企业数字化转型,解决实际发展难题
  • 【7】串口编程三种模式(查询/中断/DMA)韦东山老师学习笔记(课程听不懂的话试着来看看我的学习笔记吧)
  • 飞算科技:原创技术重塑 Java 开发,引领行业数智化新浪潮