跟着Datawhale重学数据结构与算法(3)---排序算法

devtools/2024/10/20 10:05:20/

开源链接:【 教程地址 】【电子网站】
【写博客的目的是记录自己学习过程,方便自己复盘,专业课复习】

数组排序:

数组排序
冒泡排序
选择排序
插入排序
归并排序
希尔排序
快速排序
堆排序
计数排序
桶排序
基数排序

1.冒泡排序

1.1 算法思想
其实就跟线性代数里面的求逆序数的思想一样,ε=(1234),数当前数字与前面数字相比,有几个比它大的,然后加起来就是交换次数。
ε=(2143)=0+1+0+1=2,那么要成从小打到排列就要交换2次。
冒泡排序的本质也是两两交换,再用上面举例子,最坏的情况是ε=(4321)=0+1+2+3=6,我得交换6次。
step1:把最大的元素通过交换到正确的位置,我需要交换三次。
step2:把第二大元素通过俩俩交换到正确的位置,我需要交换俩次。
......
last step:最后剩的那个元素就是最小元素,无需排序,因此就完成了排序。
你可以推广到n个元素的排序。

可见,如果我们要用程序实现这个算法,就要使用双层循环,内层循环用来保证每次将一个元素放到对的位置,外层循环用来遍历整个数组。

1.2代码实现:
/* 冒泡排序 */
void swap(int &a, int &b) {int temp = a;a = b;b = temp;
}
void bubbleSort(vector<int> &nums) {for (int i = nums.size() - 1; i > 0; i--) {for (int j = 0; j < i; j++) {if (nums[j] > nums[j + 1]) {swap(nums[j], nums[j + 1]);}}}
}
1.3 优化代码

我们想这样一个问题,假如还是上面那个逆序数,如果这个数组他没有排好,那么他一定存在一个元素会发生交换操作,那反过来说,(逆否命题也成立),如果元素没有发生交换,那么说明这个数组元素是排列好了的。因此,我们就可以用一个bool变量放在内层交换函数后面,如果发生了交换,那么继续,如果没发生交换,那么直接退出for循环。

/* 冒泡排序 */
void swap(int &a, int &b) {int temp = a;a = b;b = temp;
}
void bubbleSortWithFlag(vector<int> &nums) {for (int i = nums.size() - 1; i > 0; i--) {bool flag = false; // 初始化标志位for (int j = 0; j < i; j++) {if (nums[j] > nums[j + 1]) {swap(nums[j], nums[j + 1]);flag = true;}}if (!flag)break; // 此轮“冒泡”未交换任何元素,直接跳出}
}

要掌握的一些算法>排序算法的复杂度:
在这里插入图片描述

2.选择排序

2.1 算法思想
就是分为两个状态空间,一个是未排序的状态空间,一个是已经排序好的状态空间,原理也非常简单,就是
每轮从未排序的区间选择最小的元素放在排序好的空间的末尾。,直到未排序好的状态空间仅剩一个元素肯定为数组的最大值。
2.2 代码实现
void selectionSort(vector<int> &nums) {int n = nums.size();for (int i = 0; i < n - 1; i++) {int minIndex = i;for (int j = i + 1; j < n; j++) {if (nums[j] < nums[minIndex]) {minIndex = j;}}if (minIndex != i) {swap(nums[i], nums[minIndex]);}}
}

3.插入排序

3.1 算法思想
其实插入排序跟我们打扑克的时候整理扑克的时候很像,所以他的算法流程其实也很简单。
step1:先假设我们揭到的第一张牌为基准。
step2:从第二张牌开始,依次与基准进行比较将他插入到合适的位置,这样前两个元素就排好序了。
以此类推.......
依次将当前元素插入到已经排序好的子数组中。

插入排序的核心思想是不断地将当前元素插入到已经排序好的子数组中,以保持子数组的有序性。

3.2 代码实现:
void insertionSort(vector<int> &nums) {int n = nums.size();for (int i = 1; i < n; i++) {int key = nums[i]; // 当前待插入的元素int j = i - 1;//排序好的子序列个数// 将当前元素插入到已经排序好的子数组中的合适位置// 从后往前进行比较,不符合条件就插入到此位置中。while (j >= 0 && nums[j] > key) {nums[j + 1] = nums[j]; // 将比当前元素大的元素往后移动一个位置j--;}nums[j + 1] = key; // 插入当前元素到合适的位置}
}

4.归并排序

4.1 算法思想
分割数组:将待排序数组递归地分割成长度大约相等的两个子数组,直到每个子数组只包含一个元素为止。
合并数组:将两个有序的子数组合并成一个有序的数组。合并过程中,逐个比较两个子数组的元素,并将较小的元素放入临时数组中,直到两个子数组都被合并完毕。
重复步骤 1 和步骤 2:对分割后的子数组进行递归排序,并合并已经排序好的子数组,直到所有子数组都合并成一个完整的有序数组。

归并排序的时间复杂度为 O(n log n),其中 n 是待排序数组的大小。它的优点是稳定性好、适用于大型数据集和链表等数据结构
我感觉这个方法很amazing。用到了递归的思想,分而治之,再合并。跟深度学习中的深度可分离卷积(Depthwise Convolution)或者说shuffleNet很像,其实就是分别卷积,然后再使用1x1卷积共享参数,保证信息的交流。算法是互通的,对于归并排序来说,先拆分着进行排序,然后再合并起来进行排序。
在这里插入图片描述

4.2 代码实现
// 合并两个有序数组
void merge(vector<int> &nums, int left, int mid, int right) {int n1 = mid - left + 1; // 左侧子数组的长度int n2 = right - mid; // 右侧子数组的长度// 创建临时数组用于存放合并后的结果vector<int> temp(n1 + n2);// 将左右两个子数组合并到临时数组中int i = left, j = mid + 1, k = 0;while (i <= mid && j <= right) {if (nums[i] <= nums[j]) {temp[k++] = nums[i++];} else {temp[k++] = nums[j++];}}// 将剩余的元素复制到临时数组中while (i <= mid) {temp[k++] = nums[i++];}while (j <= right) {temp[k++] = nums[j++];}// 将临时数组的元素复制回原数组中for (int p = 0; p < k; p++) {nums[left + p] = temp[p];}
}// 归并排序
void mergeSort(vector<int> &nums, int left, int right) {if (left < right) {int mid = left + (right - left) / 2;mergeSort(nums, left, mid); // 对左侧子数组进行排序mergeSort(nums, mid + 1, right); // 对右侧子数组进行排序merge(nums, left, mid, right); // 合并两个有序子数组}
}

5.希尔排序

5.1 算法思想

希尔排序是一种改进的插入算法>排序算法,也称为缩小增量排序。它的基本思想是将待排序数组按一定步长进行分组,对每组进行插入排序,然后逐步减小步长,重复上述步骤,直到步长为 1,此时数组基本有序,最后再进行一次插入排序。

希尔排序的关键在于选择合适的步长序列,也就是元素间隔数gap,不同的步长序列会影响排序的效率。通常使用的步长序列有希尔原始提出的序列(逐步除以 2)、Hibbard 序列、Sedgewick 序列等。

我感觉哈其实有种层次聚类的意思,通过逐步减小步长来逐渐减小逆序对,来使得数组变得有序,来减少元素的移动次数。我记得层次聚类有种方法就是从大到小每次选择一个阈值进行聚类,逐渐减小阈值,直到所有样本都得到合理的分类即可。

5.2 代码实现
class Solution {
public:std::vector<int> shellSort(std::vector<int>& nums) {int size = nums.size();int gap = size / 2;while (gap > 0) {for (int i = gap; i < size; i++) {int temp = nums[i];int j = i;while (j >= gap && nums[j - gap] > temp) {nums[j] = nums[j - gap];j -= gap;}nums[j] = temp;}gap /= 2;}return nums;}std::vector<int> sortArray(std::vector<int>& nums) {return shellSort(nums);}
};

6.快速排序

6.1 算法思想
快速排序并不快,它使用分治策略来对一个数组进行排序。快速排序的基本思想是选择一个元素作为基准(pivot),通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据要小,(左边比基准都小,右边比基准都大),然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。(由大到小)。

在这里插入图片描述

6.2 代码实现:
void quickSort(vector<int>& arr, int low, int high) {if (low < high) {// 选取基准元素为左端点int pivot = arr[low];int i = low;// 将小于基准的元素移动到分割点左侧,大于基准的元素移动到右侧for (int j = low + 1; j <= high; j++) {if (arr[j] < pivot) {i++;swap(arr[i], arr[j]);}}// 基准元素交换到中间swap(arr[low], arr[i]);// 递归排序分割点左侧和右侧的子数组quickSort(arr, low, i - 1);quickSort(arr, i + 1, high);}
}

7.堆排序

7.1 算法理解

堆得结构分为大根堆和小根堆,是一个完全二叉树(从形式上讲它是个缺失的的三角形,但所缺失的部分一定是右下角某个连续的部
分,最后那一行可能不是完整的)。大小根堆从字面意思上就可以理解为树从根节点向下排列的顺序。
在这里插入图片描述在这里插入图片描述
采用顺序结构(数组)的形式来表示完全二,能够chong分利用存储空间。

7.2 代码实现
//使用数组来存储大顶堆(左孩子和哟孩子都小于父节点)
//维护堆的性质:如果父节点不满足大顶堆的性质,那就将孩子的最大值与父节点进行交换
//这样就满足了大顶堆的性质,复杂度O(logn)
建堆:(变成大顶堆),复杂度O(n)//SO:首先将数列变成大顶堆的形式,然后自下而上,从底部与根节点进行交换,交换后底部元素
//从树中删除,删除完后放在数列的最右端,更新此时的二叉树和数组,然后继续将第二个元素与根节点
//元素进行交换,继续....直到树只剩一个元素,操作完成。
void heapify(int arr[], int n, int i){
/*数组,数组长度,待维护节点下标*/
int largest = i;
int lson = i*2 +1;
int rson = i*2 +2;if(lson < n && arr[largest] < arr[lson])largest = lson;
if(rson < n && arr[largest] < arr[rson])largest = rson;
if (largest !=i)
{swap(&arr[largest],&arr[i]);heapify(arr,n,largest);
}
}
//堆排序入口
void heap_sort(int arr[], int n){int i;//建堆for(i=n/2-1;i>=0;i--)heapify(arr,n,i);//排序for(i=n-1;i>0;i--){swap(&arr[i],&arr[0]);heapify(arr,i,0);}
}

稳定性:不稳定

8. 计数排序

8.1算法原理

通过计数而不是比较来进行排序,适用于范围比较小的整数序列
原始数组->计数数组->累计数组->最终结果
在这里插入图片描述

8.2 代码实现:
void counting_sort(int arr[], int len)
{if (len < 1) return;// 寻找最大的元素int max = arr[0];for (size_t i = 1; i < len; i++)if (arr[i] > max) max = arr[i];// 分配一个长度为max+1的数组存储计数,并初始化为0int *count = (int *)malloc(sizeof(int) * (max + 1));memset(count, 0, sizeof(int) * (max + 1));// 计数for (size_t i = 0; i < len; i++)count[arr[i]]++;// 统计计数的累计值for (size_t i = 1; i < max + 1; i++)count[i] += count[i - 1];// 创建一个临时数组保存结果int *output = (int *)malloc(sizeof(int) * len);// 将元素放到正确的位置上for (size_t i = 0; i < len; i++){output[count[arr[i]] - 1] = arr[i];count[arr[i]]--;}// 将结果复制回原数组for (size_t i = 0; i < len; i++)arr[i] = output[i];
}

9.桶排序

9.1 算法原理

将待排序的元素分到有限数量的桶中,每个桶再分别进行排序,最后将各个桶中的元素合并得到排序结果。桶排序的核心思想是将一组数据分割成一定数量的桶,然后对每个桶中的数据进行排序,最后将所有桶中的数据按照顺序合并起来。首先,初始化一个足够数量的空桶。然后遍历待排序的数组,将每个元素放入相应的桶中;对每个非空的桶中的元素进行排序(可以使用其他算法>排序算法,如插入排序);按照桶的顺序将所有非空桶中的元素合并得到排序结果。

9.2 代码实现
// 桶排序函数
void bucketSort(vector<float>& arr) {int n = arr.size();// 创建足够数量的空桶vector<vector<float>> buckets(n);// 将每个元素放入相应的桶中for (int i = 0; i < n; ++i) {int bucket_index = n * arr[i];buckets[bucket_index].push_back(arr[i]);}// 对每个非空桶中的元素进行排序(这里使用插入排序)for (int i = 0; i < n; ++i) {sort(buckets[i].begin(), buckets[i].end());}// 合并所有非空桶中的元素得到排序结果int index = 0;for (int i = 0; i < n; ++i) {for (int j = 0; j < buckets[i].size(); ++j) {arr[index++] = buckets[i][j];}}
}

10.基数排序

10.1 算法思想

基数排序是一种非比较性的算法>排序算法,它按照数字的位数来进行排序。基数排序可以分为 LSD最低位优先(Least Significant Digit)和 MSD最高位优先(Most Significant Digit)两种实现方式。下面我们来介绍 LSD 基数排序的算法思想和实现。

LSD 基数排序的基本思想是从数字的最低位(个位)开始,依次对数字进行排序,直到最高位(最高位数的位数)。在每一轮排序过程中,对所有数字根据当前位上的数字进行桶排序。通过这样的逐位排序过程,最终可以得到排好序的结果。
这个方法跟人类很像,我们通常来比较两个事物所有的特征(差异)来区分,LSD这个就相当于先从个位进行排序,个位排完后,在向上观察最大的特征差异,然后到第一位的比较。

10.2 代码实现
//代码也非常简单
void radixSort(vector<int>& arr) {// 获取数组中的最大值int max_num = *max_element(arr.begin(), arr.end());// LSD 基数排序for (int digit = 1; max_num / digit > 0; digit *= 10) {// 创建 10 个桶,用于存放当前位上的数字vector<vector<int>> buckets(10);// 将数组中的元素按照当前位上的数字放入相应的桶中for (int num : arr) {int digit_value = getDigit(num, digit);buckets[digit_value].push_back(num);}// 从桶中按顺序取出元素,替换原数组中的元素int index = 0;for (auto& bucket : buckets) {// auto 是为了让编译器自动推导出 bucket 的类型for (int num : bucket) {arr[index++] = num;}bucket.clear();}}
}

Leetcode之旅~:

【1】leetcode.cn/problems/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof/" rel="nofollow">LCR.164破解闯关密码

直接偷懒,写到这累的不行了,我直接sort(快速排序)+拼接字符串。

class Solution {
public:// 自定义比较函数,用于确定两个数字拼接后的结果的大小static bool compare(const int& x, const int& y) {string xy = to_string(x) + to_string(y);string yx = to_string(y) + to_string(x);return xy < yx;}string crackPassword(vector<int>& password) {// 将数组按照自定义的比较函数排序sort(password.begin(), password.end(), compare);// 将排序后的数组拼接成字符串string result = "";for (int num : password) {result += to_string(num);}return result;}
};

【2】leetcode.cn/problems/move-zeroes" rel="nofollow">移动0

直接双指针遍历,移动非零元素,然后遍历完后,直接left后面全部赋0.

class Solution {
public:void moveZeroes(vector<int>& nums) {int left = 0; // left 指针int n = nums.size();// 遍历数组for (int right = 0; right < n; right++) {if (nums[right] != 0) {// 将非零元素放到 left 指针位置nums[left++] = nums[right];}}// 将 left 指针后面的所有元素置为 0while (left < n) {nums[left++] = 0;}}
};

leetcodecnproblemssortanarray_448">【3】[数组排序](https://leetcode.cn/problems/sort-an-array/)

直接用前面冒泡排序的代码-》超出时间限制emmmm
快速排序也超出时间限制了,这怎么搞,我想想,数组太大了,那就小规模用插入排序更快一些,大数组用快速。

class Solution {
public:vector<int> sortArray(vector<int>& nums) {quickSort(nums, 0, nums.size() - 1);return nums;}// 快速排序函数void quickSort(vector<int>& nums, int low, int high) {if (low < high) {// 对小规模数组(<=10)使用插入排序if (high - low + 1 <= 10) {insertionSort(nums, low, high);return;}// 随机选择基准元素int pivotIndex = rand() % (high - low + 1) + low;swap(nums[pivotIndex], nums[high]);pivotIndex = partition(nums, low, high);quickSort(nums, low, pivotIndex - 1);quickSort(nums, pivotIndex + 1, high);}}// 划分函数,返回分割点的位置int partition(vector<int>& nums, int low, int high) {int pivot = nums[high]; // 基准元素int i = low - 1; // 定义分割点的位置for (int j = low; j < high; j++) {if (nums[j] < pivot) {i++;swap(nums[i], nums[j]);}}swap(nums[i + 1], nums[high]); // 将基准元素放到正确的位置return i + 1;}// 插入排序函数void insertionSort(vector<int>& nums, int low, int high) {for (int i = low + 1; i <= high; i++) {int key = nums[i];int j = i - 1;while (j >= low && nums[j] > key) {nums[j + 1] = nums[j];j--;}nums[j + 1] = key;}}
};

通过了,累了,不敲了

参考资料:

[1] 【教程地址 】[电子网站]
[2] Hello 算法教程
[3] https://blog.csdn.net/qq_39181839/article/details/109478094
[4] https://blog.csdn.net/u010452388/article/details/81283998

感谢:DataWhale社区


http://www.ppmy.cn/devtools/10266.html

相关文章

日语对话构建调查研究

日语对话构建调查研究 一&#xff0c;OKWave&#xff08;オウケイウェイヴ&#xff09;网站NLP数据调研 1.OKWave速递 OKWave网址&#xff1a;OKWave 网站印象图 2.调研结论 &#xff08;1&#xff09;可行性&#xff1a;无特殊反爬手段&#xff0c;可直接从OKWave网站抓…

CSS3新增特性(一)

目录 一、CSS3 新增选择器 1. 子级选择器 2. 兄弟选择器 相邻兄弟选择器 其他兄弟选择器 3. 结构伪类选择器 ① E:first-child ② E:last-child ③ nth-child&#xff08;n&#xff09; n为数字&#xff1a; n为关键字&#xff1a; n为公式&#xff1a; ④ E: firs…

授人以渔 选购篇九:扫地机器人(扫拖机器人)选购要点

文章目录 系列文章自动上下水导航技术&#xff1a;立体激光导航视觉导航&#xff0c;多传感器清洁能力&#xff1a;胶条刷、旋转拖布健康卫生&#xff1a;热水洗拖布、热风烘干智能功能品牌其他 系列文章 授人以渔 选购篇一&#xff1a;信用卡选购要点 授人以渔 选购篇二&…

【机器学习300问】77、什么是梯度消失和梯度爆炸?

一、梯度消失&#xff08;Vanishing gradients&#xff09; &#xff08;1&#xff09;定义 在训练深度神经网络时&#xff0c;随着误差梯度从输出层向输入层逐层回传&#xff0c;梯度可能因为连乘效应逐渐减小。当使用激活函数的导数的最大值小于1时&#xff0c;深度网络中越…

设计模式之访问者模式(上)

访问者模式 1&#xff09;概述 1.概念 访问者模式包含访问者和被访问元素两个主要组成部分。 处方单中的各种药品信息就是被访问的元素&#xff0c;而划价人员和药房工作人员就是访问者&#xff0c;被访问的元素通常具有不同的类型&#xff0c;且不同的访问者可以对它们进行…

vi, vim,data,wc,系统常用命令-读书笔记(十)

vi 文本编辑器 基本上 vi 共分为三种模式&#xff0c;分别是“一般指令模式”、“编辑模式”与“命令行命令模式”。这三种模式的作用分别是&#xff1a; 一般指令模式&#xff08;command mode&#xff09;以 vi 打开一个文件就直接进入一般指令模式了&#xff08;这是默认的…

Java后端中如何随意接收参数

目录 一、参数名相同 二、参数名不同&#xff0c;使用RequestParam注解 大概访问流程是&#xff1a;先访问test控制器&#xff0c;test控制器跳转到index页面&#xff08;此时index页面收到了test控制器传来的数据&#xff09;&#xff0c;然后在index页面跳转到t5控制器&…

ChatGPT引领:打造独具魅力的论文

ChatGPT无限次数:点击直达 ChatGPT引领&#xff1a;打造独具魅力的论文 在数字化时代&#xff0c;人工智能技术的快速发展不仅改变了我们生活的方方面面&#xff0c;还在学术研究领域展现出更广阔的可能性。其中&#xff0c;自然语言生成模型ChatGPT凭借其强大的生成能力和智能…