排序算法——朝花夕拾

2024-02-29 10:18
文章标签 算法 排序 朝花夕拾

本文主要是介绍排序算法——朝花夕拾,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1 冒泡排序

1-1 思路

冒泡排序说实话我还挺烦的,虽然说是最简单的排序算法,但我还真没把握一次就写出来,因为距离上一次写冒泡排序还是在4年前。而且冒泡的细节还是比较多的。

冒泡排序的思想主要就是一个个比较,先确定最后一个元素,再确定倒数第二个元素。记也比较好记,外层循环是,循环的次数,没有任何逻辑在里面,内层循环是控制冒泡。

首先,10个元素要循环9次才能确定位置,因此外层的循环次数是 n-1

for (int i=1; i<len; ++i) {}

为什么是 1~n-1 而不是 0~n-1 呢?因此,第一次循环需要9次,第二次循环就需要8次,可以发现第 n 次循环与 需要的次数 m 互补, n + m = 10。这样内层循环可以写成len-i 而不是 len-i-1 比较好看。

for (int i=1; i<len; ++i) {for (int j=0; j<len-i; ++j) {}
}

然后是冒泡,冒泡是以过程为核心的,而不是一个元素为核心的,每次冒泡都是从第一个位置的元素开始,一直与下一个位置的元素比较,如果大于就交换,注意这里为什么要把位置加粗,因为,很多新手在写的时候,以为是以元素为核心的,当遇到冒泡不下去的时候就停止本轮循环,这是不对的,要一直比完,每一轮循环都会确定一个最后的元素。因此内层循环是从 j 开始 到 len-i 结束,比较的是 jj+1,且 j 每次都是从0 开始。

for (int i=1; i<len; ++i) {for (int j=0; j<len-i; ++j) {temp = arr[j];arr[j] = arr[j+1];arr[j+1] = temp;}
}

1-2 代码实现

let arr = [49, 38, 65, 97, 76, 13, 27, 49];function popSort(arr) {let len = arr.length, temp;let newArr = [...arr];for (let i=1; i<len; ++i) {for (let j=0; j<len-i; ++j) {if (newArr[j] > newArr[j+1]) {temp = newArr[j+1];newArr[j+1] = newArr[j];newArr[j] = temp;}}}return newArr;
}console.log(popSort(arr));

1-3 稳定性

稳定。

可以看到,最内层的 if 语句是大于小于号,而没有等于在里面,因此当原数组的元素在冒泡的过程中遇到相同的元素,是不会继续交换位置的,因此是稳定的。

2 快速排序

2-1 思路

思路就是在一组数据中选取一个中间大小的轴枢元素 pivot,然后对整个数组进行划分,把比 pivot小的元素划分到pivot左边,把大的划分到右边。这样pivot 的位置就已经固定了,然后递归地划分pivot的左侧和右侧,视为一个一模一样的子问题,直到全部解决。

具体实现细节可以使用双指针,分别指向数组的两头,保存最左侧的值,然后开始循环,具体见代码实现部分。

2-2 代码实现

而且注意这里直接选取 lo 作为 轴枢 是不合理的,具体的理由以及优化手段见 OI WIKI 中快速排序部分。

这里要注意,函数参数中的 lowhigh 不能在函数内部直接使用,要复制它的 lo, hi 作为拷贝使用,否则在递归时,关于边界将无法处理。

关于内层的两个 while 中的第二个判断条件,一定要用 >= and <=,因为假设给你数据 [3,1,4,2,5,2 3] 进行排序,两头的元素一致,你直接就死循环了。

let arr = [49, 38, 65, 97, 76, 13, 27, 49];function quickSort(arr, low = 0 ,high = arr.length - 1) {let lo = low, hi = high;if (lo >= hi) return;let pivot = arr[lo];while (lo < hi) {// 遇到比pivot大的,hi停止,并赋值给arr[lo],arr[lo] 已经在上面保存过了while (lo < hi && arr[hi] >= pivot) hi --;arr[lo] = arr[hi];// 遇到比pivot小的,lo停止,并赋值给arr[hi]while (lo < hi && arr[lo] <= pivot) lo ++;arr[hi] = arr[lo];}// 当while结束后,恢复pivot信息arr[lo] = pivot;quickSort(arr, low, lo - 1);quickSort(arr, lo + 1, high);return arr;
}console.log(quickSort(arr));

2-3 优化思路

关于稳定性以及为什么算法会退化的讨论。

上面的文章简要提到了算法目前的不足。算法的理想最优情况下是:每次找到的 pivot元素的下标 在一轮划分后,刚好处于数组最中间的位置

考虑一个很差的情况,不论数组是 [1,1,1,1,1,1,1] 还是 [1,2,3,4,5,6,7],长度设为 n ,在我上面给出的代码中,内部的第一个while循环会直接把 hi 拉到 lo 的位置,进行一次交换后,一轮划分完成后,下一次待划分的部分的长度将是 n-1,这样的时间复杂度很容易理解,将会是 n*(n-1)*(n-2)* ... *1退化到 O(n^2) 的复杂度。

完整优化策略:见 OI WIKI 中快速排序部分。

这里我细说一个优化策略,就是在待排序部分选择 arr[lo], arr[mid], arr[hi] 三者中中间的那个数作为轴枢元素,然后和 arr[lo] 进行交换,最后再执行后面的循环。

例如:1, 4, 3, 6, 2, 7,如果我们始终选 arr[lo] 作为轴枢元素,那么我们第一次就会选到 1 ,划分后的数组不变,这样就会发生我们上面提到的退化。

如果我们在这里执行上面的优化手段。arr[lo] = 1, arr[mid] = 3, arr[hi] = 7 ,数组在交换后为 3, 4, 1, 6, 2, 7 ,在一轮划分后为 2, 1, 3, 6, 4 ,7, 这样左边的长度为2,右边的长度为3,就更接近于最理想的情况。

while (lo < hi) {int num1 = nums[lo],num2 = nums[(lo + hi) / 2],num3 = nums[hi],swapPivot = lo;if ((num1 <= num2 && num2 <= num3) || (num3 <= num2 && num2 <= num1)) {swapPivot = (lo + hi) / 2;} else if ((num1 <= num3 && num3 <= num2) || (num2 <= num3 && num3 <= num1)) {wapPivot = hi;}swap(nums[swapPivot], nums[lo]);...
}

2-4 JavaScript中代码级别的优化

我们尽量这里做一个纯函数,一个函数的返回结果只依赖其参数,并且执行过程中没有副作用

因此我们将代码做一些小小的改动

let arr = [49, 38, 65, 97, 76, 13, 27, 49];function quickSort(arr, low = 0 ,high = arr.length - 1) {const sort = function (arr, low, high) {let lo = low, hi = high;if (lo >= hi) return;let pivot = arr[lo];while (lo < hi) {while (lo < hi && arr[hi] >= pivot) hi --;arr[lo] = arr[hi];while (lo < hi && arr[lo] <= pivot) lo ++;arr[hi] = arr[lo];}arr[lo] = pivot;sort(arr, low, lo - 1);sort(arr, lo + 1, high);return arr;}newArr = arr.concat()return sort(newArr, low, high);
}console.log(quickSort(arr));

2-5 稳定性

这个很容易看出来,快速排序使用的是交换,而不是插入,因此是不稳定,但我觉得如果使用额外的空间,还是可以使其变得稳定的。

选择排序

思路

整体来说维护一个空的序列(已排序序列)和一个未排序序列,在一开始的时候未排序的序列为数组本身,先在未排序序列中选择一个最小的元素,与未排序序列队头元素进行交换,并把该元素从未排序序列中移除,加入已排序序列。重复此过程。

在这里插入图片描述

在这里插入图片描述

代码实现

let arr= [3,2,5,1,5,6,7];function selectSort(arr) {let len = arr.length;for (let i=0; i<len-1; i++) {let minIdx = i;for (let j=i+1; j<len; j++) {if (arr[j] < arr[minIdx]) {minIdx = j;}}let temp = arr[i];arr[i] = arr[minIdx];arr[minIdx] = temp;}return arr;
}console.log(selectSort(arr));

插入排序

思路

插入排序与选择排序相反,选择排序是在未排序的数组中选出一个数放入已排序数组,而插入排序是拿出第一个未排序数组中的数逐个与已排序数组中的数比较,若小于,就交换位置,直到大于。

代码实现

let arr= [3,2,5,1,5,6,7];function insertSort(arr) {let len = arr.length;for (let i=1; i<len; i++) {let j = i;for (let j=i; j>0; j--) {if (arr[j] < arr[j-1]) {let temp = arr[j-1];arr[j-1] = arr[j];arr[j] = temp;}}}return arr;
}console.log(insertSort(arr));

总结

关于稳定性

算法稳定性
选择不稳定,因为要交换
快速不稳定,因为要交换
堆排不稳定
希尔不稳定
冒泡稳定,因为是逐个比较,当相等时不进行操作,不会破坏原有结构
插入稳定,因为是逐个比较
归并稳定
基数稳定

这篇关于排序算法——朝花夕拾的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/758434

相关文章

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

康拓展开(hash算法中会用到)

康拓展开是一个全排列到一个自然数的双射(也就是某个全排列与某个自然数一一对应) 公式: X=a[n]*(n-1)!+a[n-1]*(n-2)!+...+a[i]*(i-1)!+...+a[1]*0! 其中,a[i]为整数,并且0<=a[i]<i,1<=i<=n。(a[i]在不同应用中的含义不同); 典型应用: 计算当前排列在所有由小到大全排列中的顺序,也就是说求当前排列是第

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个

综合安防管理平台LntonAIServer视频监控汇聚抖动检测算法优势

LntonAIServer视频质量诊断功能中的抖动检测是一个专门针对视频稳定性进行分析的功能。抖动通常是指视频帧之间的不必要运动,这种运动可能是由于摄像机的移动、传输中的错误或编解码问题导致的。抖动检测对于确保视频内容的平滑性和观看体验至关重要。 优势 1. 提高图像质量 - 清晰度提升:减少抖动,提高图像的清晰度和细节表现力,使得监控画面更加真实可信。 - 细节增强:在低光条件下,抖

【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值 基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。 快速排序实现主框架: //快速排序 void QuickSort(int* arr, int left, int rig

usaco 1.3 Mixing Milk (结构体排序 qsort) and hdu 2020(sort)

到了这题学会了结构体排序 于是回去修改了 1.2 milking cows 的算法~ 结构体排序核心: 1.结构体定义 struct Milk{int price;int milks;}milk[5000]; 2.自定义的比较函数,若返回值为正,qsort 函数判定a>b ;为负,a<b;为0,a==b; int milkcmp(const void *va,c

poj 3974 and hdu 3068 最长回文串的O(n)解法(Manacher算法)

求一段字符串中的最长回文串。 因为数据量比较大,用原来的O(n^2)会爆。 小白上的O(n^2)解法代码:TLE啦~ #include<stdio.h>#include<string.h>const int Maxn = 1000000;char s[Maxn];int main(){char e[] = {"END"};while(scanf("%s", s) != EO

秋招最新大模型算法面试,熬夜都要肝完它

💥大家在面试大模型LLM这个板块的时候,不知道面试完会不会复盘、总结,做笔记的习惯,这份大模型算法岗面试八股笔记也帮助不少人拿到过offer ✨对于面试大模型算法工程师会有一定的帮助,都附有完整答案,熬夜也要看完,祝大家一臂之力 这份《大模型算法工程师面试题》已经上传CSDN,还有完整版的大模型 AI 学习资料,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

hdu 1285(拓扑排序)

题意: 给各个队间的胜负关系,让排名次,名词相同按从小到大排。 解析: 拓扑排序是应用于有向无回路图(Direct Acyclic Graph,简称DAG)上的一种排序方式,对一个有向无回路图进行拓扑排序后,所有的顶点形成一个序列,对所有边(u,v),满足u 在v 的前面。该序列说明了顶点表示的事件或状态发生的整体顺序。比较经典的是在工程活动上,某些工程完成后,另一些工程才能继续,此时

dp算法练习题【8】

不同二叉搜索树 96. 不同的二叉搜索树 给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。 示例 1: 输入:n = 3输出:5 示例 2: 输入:n = 1输出:1 class Solution {public int numTrees(int n) {int[] dp = new int