数据结构与算法导论妖怪 之 十大经典排序算法 妖

2024-03-18 15:20

本文主要是介绍数据结构与算法导论妖怪 之 十大经典排序算法 妖,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

数据结构与算法导论妖怪 之

十大经典排序算法 妖

说起排序,没有人感觉陌生,从小到大我们都一路被形形色色的人怀着各种各样的目的所排名!尤其是学习成绩(手动狗头)。作为驯妖师的我来说,那时被排名所支配的心酸苦辣就是人生!不过今儿给大家念叨的是在计算机编程涉及到的排序算法,就是说,如何运用自己学到的编程知识,实现从数学概念中众多关系中找出排序这种关系,且越快越好,资源利用越高效越好。

首先给简单介绍一下有关算法复杂度的概念,因为在众多排序算法介绍之前,我们想要找到一个合适的算法的时候,该怎么考核呢?这就要涉及算法复杂度的概念,具体包括时间复杂度和空间复杂度,分别表示该算法执行程序语句次数的程度和占用内存空间大小的程度,这里统一用O()来表示,后续给大家详细介绍算法复杂度这只妖怪。

直奔主题,一一降伏这几只排序小妖:

1.冒泡排序:

冒泡排序这只妖怪是个近视眼,因为它只看重局部相邻的两个元素的大小关系,将不符合排序规则的两个相邻元素交换位置,这样经它过滤一遍后的序列,最大值或最小值就确定了(因为只有这样,才符合它过滤的结果,即局部大小关系符合排序规则)。而人们会可爱地称呼它“冒泡排序”,就好比气泡在水中冒出来一样(最值通过每次遍历一个个依次冒了出来)。
在这里插入图片描述(图片来自 https://mp.weixin.qq.com/s/QaflNoxiI_FS4Ij8jg1KrQ)

下面快来看代码实现(python VS C++):

def bubbleSort(arr):for i in range(1, len(arr)):for j in range(0, len(arr)-i):if arr[j] > arr[j+1]:arr[j], arr[j + 1] = arr[j + 1], arr[j]return arr
void bubbleSort(int* arr, int len) {for (int i = 1; i < len; i ++) {for (int j = 0; j < len - i; j ++) {if (arr[j] > arr[i]) {int tmp = arr[j];arr[j] = arr[i];arr[i] = tmp;}}}
}

2.选择排序:

选择排序妖类似于冒泡排序,但是它目的性很强,每次过滤序列的时候,只认准最大值或最小值,虽然结果与冒泡一样,但是遍历的意义不一样,它是携带着当前它认为最大值或最小值来与当前元素进行比较,然后对比找当前最大值或最小值,继续向前,颇有“一旦选择了你,就要找到你”的爱情观。

在这里插入图片描述(图片来自 https://mp.weixin.qq.com/s/QaflNoxiI_FS4Ij8jg1KrQ)

下面快来看代码实现(python VS C++):

def selectionSort(arr):for i in range(len(arr) - 1):# 记录最小数的索引minIndex = ifor j in range(i + 1, len(arr)):if arr[j] < arr[minIndex]:minIndex = j# i 不是最小数时,将 i 和最小数进行交换if i != minIndex:arr[i], arr[minIndex] = arr[minIndex], arr[i]return arr
void selectionSort(int* arr, int len) {for (int i = 0; i < len - 1; i ++) {for (int j = i + 1; j < len; j ++) {if (arr[j] < arr[i]) {int tmp = arr[j];arr[j] = arr[i];arr[i] = tmp;}}}
}

3.插入排序:

插入排序妖不难理解,它就是通过不断的新增元素来与之前排好序的元素进行比较,”见缝插针“式地找到自己的容身之地。有一形象的说法就好比大家玩扑克,一张一张起牌然后码牌一样,摸完了你的JQKA顺序也排好了,或者出现”JQKKA“,这个就比较纠结怎么出牌了,哈哈哈

在这里插入图片描述

来康康代码怎么实现:

def insertionSort(arr):for i in range(len(arr)):preIndex = i-1current = arr[i]while preIndex >= 0 and arr[preIndex] > current:arr[preIndex+1] = arr[preIndex]preIndex-=1arr[preIndex+1] = currentreturn arr
void insertionSort(int* arr, int len) {for (int i = 0; i < len; i ++) {int current = arr[i];int index = i - 1;while (index >= 0 && arr[index] > current) {arr[index + 1] = arr[index];index--;}arr[index + 1] = current;}
}

4.希尔排序:

希尔排序妖,也叫递减增量排序妖,是插入排序妖的近亲,它嫌弃插入排序每次只能移动一位效率低,因此它喜欢将原来序列分组,组内插入,因为组内元素个数较少,插入比较快,然后迭代着增加组内元素个数地再次分组,此时组内元素看是增多了,但是基本有序,这样就比直接插入快多了,直到最后整个序列分为一组计算结束。

在这里插入图片描述

什么?!你问我代码怎么写:

def shellSort(arr):import mathgap=1while(gap < len(arr)/3):gap = gap*3+1while gap > 0:for i in range(gap,len(arr)):temp = arr[i]j = i-gapwhile j >=0 and arr[j] > temp:arr[j+gap]=arr[j]j-=gaparr[j+gap] = tempgap = math.floor(gap/3)return arr
}
def shellSort(arr):import mathgap=1while(gap < len(arr)/3):gap = gap*3+1while gap > 0:for i in range(gap,len(arr)):temp = arr[i]j = i-gapwhile j >=0 and arr[j] > temp:arr[j+gap]=arr[j]j-=gaparr[j+gap] = tempgap = math.floor(gap/3)return arr
}

5.归并排序:

归并排序妖是分治法老妖的徒子徒孙,既可以自上而下的递归,又可以自下而上的迭代实现,和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。归并排序就是将原序列递归分解成各个子分组,然后简单比较大小就互换位置,然后再迭代着由小分组合并成大的有序分组,直到之后成为一个大分组,就是原来序列的有序序列。
在这里插入图片描述(图片来自 https://mp.weixin.qq.com/s/QaflNoxiI_FS4Ij8jg1KrQ)

来一起看看代码:

def mergeSort(arr):import mathif(len(arr)<2):return arrmiddle = math.floor(len(arr)/2)left, right = arr[0:middle], arr[middle:]return merge(mergeSort(left), mergeSort(right))def merge(left,right):result = []while left and right:if left[0] <= right[0]:result.append(left.pop(0));else:result.append(right.pop(0));while left:result.append(left.pop(0));while right:result.append(right.pop(0));return result
// 归并排序(C++-迭代版)
template<typename T>
void merge_sort(T arr[], int len) {T* a = arr;T* b = new T[len];for (int seg = 1; seg < len; seg += seg) {for (int start = 0; start < len; start += seg + seg) {int low = start, mid = min(start + seg, len), high = min(start + seg + seg, len);int k = low;int start1 = low, end1 = mid;int start2 = mid, end2 = high;while (start1 < end1 && start2 < end2)b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++];while (start1 < end1)b[k++] = a[start1++];while (start2 < end2)b[k++] = a[start2++];}T* temp = a;a = b;b = temp;}if (a != arr) {for (int i = 0; i < len; i++)b[i] = a[i];b = a;}delete[] b;
}
// 归并排序(C++-递归版)
template<typename T>
void merge_sort_recursive(T arr[], T reg[], int start, int end) {if (start >= end)return;int len = end - start, mid = (len >> 1) + start;int start1 = start, end1 = mid;int start2 = mid + 1, end2 = end;merge_sort_recursive(arr, reg, start1, end1);merge_sort_recursive(arr, reg, start2, end2);int k = start;while (start1 <= end1 && start2 <= end2)reg[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];while (start1 <= end1)reg[k++] = arr[start1++];while (start2 <= end2)reg[k++] = arr[start2++];for (k = start; k <= end; k++)arr[k] = reg[k];
}// merge_sort
template<typename T>
void merge_sort(T arr[], const int len) {T reg[len];merge_sort_recursive(arr, reg, 0, len - 1);
}

6.快速排序:

快速排序妖可是鼎鼎大名,快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。由于这个比较复杂也相对重要(面试老师经常会拿它来让你现场抓捕,嘤嘤嘤),所以这里简单总结思路:

(1)从数列中挑出一个元素,称为 “基准”(pivot);

(2)重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;

(3)递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;

在这里插入图片描述(图片来自 https://mp.weixin.qq.com/s/QaflNoxiI_FS4Ij8jg1KrQ)

快来学习代码:

def quickSort(arr, left=None, right=None):left = 0 if not isinstance(left,(int, float)) else leftright = len(arr)-1 if not isinstance(right,(int, float)) else rightif left < right:partitionIndex = partition(arr, left, right)quickSort(arr, left, partitionIndex-1)quickSort(arr, partitionIndex+1, right)return arrdef partition(arr, left, right):pivot = leftindex = pivot+1i = indexwhile  i <= right:if arr[i] < arr[pivot]:swap(arr, i, index)index+=1i+=1swap(arr,pivot,index-1)return index-1def swap(arr, i, j):arr[i], arr[j] = arr[j], arr[i]
void quickSort(int* arr, int left, int right) {if (left < right) {int tmp = arr[left];int i = left;int j = right;while (i < j) {while (i < j && arr[j] > tmp) {j--;}arr[i] = arr[j];while (i < j && arr[i] <= tmp) {i++;}arr[j] = arr[i];}arr[i] = tmp;quickSort(arr, left, i - 1);quickSort(arr, i + 1, right);}
}void quickSort(int* arr, int len) {quickSort(arr, 0, len - 1);
}

这里啰嗦一句在《算法艺术与信息学竞赛》的话:

快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。

7.堆排序:

推排序妖可是这十个经典排序算法里面最复杂也最难的一只妖怪,不过理解和运用熟练之后,大有可为,实乃行走江湖利器一只!它可以利用堆这种数据结构妖,堆是一个近似完全二叉树的结构妖,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为大根堆和小根堆,依次用于升序和降序排列。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RFovXhbK-1593932053149)(D:\创业\公众号平台\数据结构与算法导论\推排序.gif)]

在这里插入图片描述(图片来自 https://mp.weixin.qq.com/s/QaflNoxiI_FS4Ij8jg1KrQ)

其实驯妖师也没觉得很难,看降伏堆排序妖的代码:

def buildMaxHeap(arr):import mathfor i in range(math.floor(len(arr)/2),-1,-1):heapify(arr,i)def heapify(arr, i):left = 2*i+1right = 2*i+2largest = iif left < arrLen and arr[left] > arr[largest]:largest = leftif right < arrLen and arr[right] > arr[largest]:largest = rightif largest != i:swap(arr, i, largest)heapify(arr, largest)def swap(arr, i, j):arr[i], arr[j] = arr[j], arr[i]def heapSort(arr):global arrLenarrLen = len(arr)buildMaxHeap(arr)for i in range(len(arr)-1,0,-1):swap(arr,0,i)arrLen -=1heapify(arr, 0)return arr
void heapSwap(int* arr, int a, int b) {int tmp = arr[a];arr[a] = arr[b];arr[b] = tmp;
}void adjustHeap(int* arr, int i, int len) {int tmp = arr[i];for (int k = i * 2 + 1; k < len; k = k * 2 + 1) {if (k + 1 < len && arr[k] < arr[k + 1]) {k++;}if (arr[k] > tmp) {arr[i] = arr[k];i = k;}else {break;}}arr[i] = tmp;
}void heapSort(int* arr, int len) {for (int i = len / 2 - 1; i >= 0; i --) {adjustHeap(arr, i, len);}for (int i = len - 1; i > 0; i --) {heapSwap(arr, 0, i);adjustHeap(arr, 0, i);}
}

8.计数排序:

计数排序妖是个不安常路出牌的小家伙,它将原始序列的元素值转化为键存储在额外开辟的数组空间中。同时它还挑食,输入的数据必须是有确定范围的整数才可以。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EH5S4KVY-1593932053150)(D:\创业\公众号平台\数据结构与算法导论\计数排序.gif)]

在这里插入图片描述

捕捉代码:

def countingSort(arr, maxValue):bucketLen = maxValue+1bucket = [0]*bucketLensortedIndex =0arrLen = len(arr)for i in range(arrLen):if not bucket[arr[i]]:bucket[arr[i]]=0bucket[arr[i]]+=1for j in range(bucketLen):while bucket[j]>0:arr[sortedIndex] = jsortedIndex+=1bucket[j]-=1return arr
void countSort(int* arr, int len) {int max = INT_MIN;for (int i = 0; i < len; i ++) {if (max < arr[i]) {max = arr[i];}}int* count = new int[max + 1];memset(count, 0, sizeof(int) * (max + 1));for (int i = 0; i < len; i++) {count[arr[i]]++;}int index = 0;for (int i = 0; i < (max + 1); i ++) {while (count[i]> 0) {arr[index++] = i;count[i]--;}}
}

9.桶排序

桶排序妖是计数排序的进化妖。它利用了函数的映射关系来将数据值转为K值,高效与否的关键就在于这个映射函数的确定。为了能够快速和准确抓住这只进化版本的妖怪,我们可以设计尽量多的桶(其实就是放数字的抽屉)和尽可能地均分这些数字。

在这里插入图片描述

很简单吧,看看代码:

def bucket_sort(s):"""桶排序"""min_num = min(s)max_num = max(s)# 桶的大小bucket_range = (max_num-min_num) / len(s)# 桶数组count_list = [ [] for i in range(len(s) + 1)]# 向桶数组填数for i in s:count_list[int((i-min_num)//bucket_range)].append(i)s.clear()# 回填,这里桶内部排序直接调用了sortedfor i in count_list:for j in sorted(i):s.append(j)
bool compare(int a, int b) {return a <= b;
}void bucketSort(int* arr, int len) {int min = INT_MAX;int max = INT_MIN;for (int i = 0; i < len; i ++) {if (min > arr[i]) {min = arr[i];}if (max < arr[i]) {max = arr[i];}}float size = (max - min) /1.0 / len;std::vector<std::vector<int> > count_list(len + 1);for (int i = 0; i < len; i ++) {count_list[(arr[i] - min)/size].push_back(arr[i]);}int index = 0;for (int i = 0; i < count_list.size(); i ++) {std::sort(count_list[i].begin(), count_list[i].end());for (int j = 0; j < count_list[i].size(); j ++) {arr[index++] = count_list[i][j];}}
}

10.基数排序:

基数排序妖是计数排序,桶排序的近亲,它一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。我们可以简单区分一下它们:

  • 基数排序:根据键值的每位数字来分配桶;
  • 计数排序:每个桶只存储单一键值;
  • 桶排序:每个桶存储一定范围的数值;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cESoxf31-1593932053151)(D:\创业\公众号平台\数据结构与算法导论\基数排序.gif)]

在这里插入图片描述(图片来自 https://mp.weixin.qq.com/s/QaflNoxiI_FS4Ij8jg1KrQ)

代码:

def RadixSort(list):i = 0                                    #初始为个位排序n = 1                                     #最小的位数置为1(包含0)max_num = max(list) #得到带排序数组中最大数while max_num > 10**n: #得到最大数是几位数n += 1while i < n:bucket = {} #用字典构建桶for x in range(10):bucket.setdefault(x, []) #将每个桶置空for x in list: #对每一位进行排序radix =int((x / (10**i)) % 10) #得到每位的基数bucket[radix].append(x) #将对应的数组元素加入到相 #应位基数的桶中j = 0for k in range(10):if len(bucket[k]) != 0: #若桶不为空for y in bucket[k]: #将该桶中每个元素list[j] = y #放回到数组中j += 1i += 1
return  list
int maxbit(int* data, int n) {int d = 1;for (int i = 0; i < n; i ++) {int c = 1;int p = data[i];while (p / 10) {p /= 10;c++;}if (c > d) {d = c;}}return d;
}void radixSort(int*arr, int len) {int d = maxbit(arr, len);int r = 1;int count[10] = {0};int tmp[10] = {0};for (int i = 0; i < d; i ++) {for (int j = 0; j < 10; j++) {count[j] = 0;}for (int j = 0; j < len; j ++) {int k = arr[j] / r;int q = k % 10;count[q]++;}for (int j = 1; j < 10; j ++) {count[j] += count[j - 1];}for (int j = len - 1; j >= 0; j --) {int p = arr[j] / r;int s = p % 10;tmp[count[s] - 1] = arr[j];count[s]--;}for (int j = 0; j < len; j ++) {arr[j] = tmp[j];}r *= 10;}
}

介绍了以上十种经典的排序算法,在这里,驯妖师给大家补充一下其各自算法复杂度和稳定性质(排序后 2 个相等键值的顺序和排序之前它们的顺序相同,就说这个排序算法是稳定的,否则就是不稳定的)的对比表,以加强区分:
在这里插入图片描述(图片来自 https://mp.weixin.qq.com/s/QaflNoxiI_FS4Ij8jg1KrQ)

n:数据规模 k:“桶”的个数 In-place:占用常数内存,不占用额外内存 Out-place:占用额外内存

以上就是今儿驯妖师给大家的叨叨,这只是初步认识,比较浅显地让大家有个感性和一点点理性认识,在实际项目中,如果有用到排序场景,应该依据每种排序算法的特点选择适合的算法,或者几种算法组合的版本,还有一些局部的改进版本等等,还是那句话编程如抓妖,键盘在手,抓妖不止! 哈哈哈

参考链接:

https://mp.weixin.qq.com/s/QaflNoxiI_FS4Ij8jg1KrQ

https://zhuanlan.zhihu.com/p/124356219

这篇关于数据结构与算法导论妖怪 之 十大经典排序算法 妖的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

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

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “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 的前面。该序列说明了顶点表示的事件或状态发生的整体顺序。比较经典的是在工程活动上,某些工程完成后,另一些工程才能继续,此时

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)