c++ 不插入重复元素但也不排序_面试官爱问的 10 大经典排序算法,20+ 张图来搞定...
(给算法爱好者加星标,修炼编程内功)
作者:技术让梦想更伟大 / 李肖遥 (本文来自作者投稿)
冒泡排序
简介
冒泡排序是因为越小的元素会经由交换以升序或降序的方式慢慢浮
到数列的顶端,就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名冒泡排序。
复杂度与稳定性
思路原理
以顺序为例
从第一个元素开始一个一个的比较相邻的元素,如果第一个比第二个大即
a[1]>a[2]
,就彼此交换。从第一对到最后一对,对每一对相邻元素做一样的操作。此时在最后的元素应该会是最大的数,我们也称呼
一遍这样的操作
为一趟冒泡排序。针对所有的元素重复以上的步骤,每一趟得到的最大值已放在最后,下一次操作则不需要将此最大值纳入计算。
持续对每次对越来越少的元素,重复上面的步骤。
直到所有的数字都比较完成符合
a[i],即完成冒泡排序。
图示过程
以数组数据{ 70,50,30,20,10,70,40,60}为例:
如图,每一次排序把一个最大的数被放在了最后,然后按照这个趋势逐渐往前,直到按从小到大的顺序依次排序。
到了第4轮的时候,整个数据已经排序结束了,但此时程序仍然在进行。
直到第5,6,7轮程序才算真正的结束,这其实是一种浪费算力的表现。
主要代码实现
void bubble_sort(int a[],int n) { for(int i=0; i for(int j=0; j if(a[j]>a[j+1]) { swap(a[j],a[j+1]); //交换数据 } } }}
注意,由于C++的namespace std
命名空间的使用,std自带了交换函数swap(a,b)
,可以直接使用,其功能是交换a与b的两个值,当然你可以自定义swap函数,其模板代码为:
template //模板类,可以让参数为任意类型void swap(T &a,T &b) { T c(a); a=b; b=c;}
或者指定类型修改为整形,如:
void swap(int &a, int &b) { //指定类型 int temp = a; a = b; b = temp;}
选择排序
简介
选择排序是一种简单直观的排序算法,它从待排序的数据元素中选出最小或最大的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小或最大元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。
复杂度与稳定性
过程介绍(以顺序为例)
首先设置两个记录i和j,i从数组第一个元素开始,j从(i+1)个元素开始。
接着j遍历整个数组,选出整个数组最小的值,并让这个最小的值和i的位置交换。
i选中下一个元素(i++),重复进行每一趟选择排序。
持续上述步骤,使得i到达(n-1)处,即完成排序 。
图示过程:
以数据{2,10,9,4,8,1,6,5}为例
如图所示,每次交换的数据使用红颜色标记出,已经排好序的数据使用蓝底标注,
每一趟从待排序的数据元素中选出最小的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。
我们只需要进行n-1趟排序即可,因为最后剩下的一个数据一定是整体数据中最大的数据。
代码实现
void select_sort(int a[],int n){ int temp; for(int i=0;i temp=i; //利用一个中间变量temp来记录需要交换元素的位置 for(int j=i+1;j if(a[temp]>a[j]){ //选出待排数据中的最小值 temp=j; } } swap(a[i],a[temp]); //交换函数 }}
相比冒泡排序的不断交换,简单选择排序是等到合适的关键字出现后再进行交换,并且交换一次就可以达到一次冒泡的效果。
插入排序
简介
插入排序是一种最简单的排序方法,对于少量元素的排序,它是一个有效的算法。
复杂度与稳定性
过程介绍
首先将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。
每一步将一个待排序的元素,按其排序码的大小,插入到前面已经排好序的一组元素的适当位置上去,直到元素全部插入为止。
可以选择不同的方法在已经排好序数据表中寻找插入位置。根据查找方法不同,有多种插入排序方法,下面要介绍的是直接插入排序。
每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表仍然有序。
第一趟比较前两个数,然后把第二个数按大小插入到有序表中;
第二趟把第三个数据与前两个数从后向前扫描,把第三个数按大小插入到有序表中;
依次进行下去,进行了(n-1)趟扫描以后就完成了整个排序过程。
图示过程
以数组数据{ 70,50,30,20,10,70,40,60}为例:
将红色的数据依次插入组成一个逐渐有序的数组
代码实现
void insert_sort(int a[],int n) { int i,j; //外层循环标识并决定待比较的数值 for(i=1; i if(a[i] int temp=a[i]; //待比较数值确定其最终位置 for(j=i-1; j>=0 && a[j]>temp; j--) { a[j+1]=a[j]; } a[j+1]=temp;//此处就是a[j+1]=temp; } }}
希尔排序
简介
希尔排序又称缩小增量排序
,是直接插入排序算法的一种更高效的改进版本。
希尔排序是非稳定排序算法,在对几乎已经排好序的数据操作时,效率极高,即可以达到线性排序的效率。
复杂度与稳定性
过程介绍
先将整个待排序的记录序列分组,对若干子序列分别进行直接插入排序,随着增量逐渐减少即整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
过程如下:
选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
按增量序列个数 k,对序列进行 k 趟排序;
每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。
仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
图示过程
可以看见,相比直接插入排序由于可以每趟进行分段操作,故整体效率体现较高。
主要代码实现
void shellSort(int arr[], int n) { int i, j, gap; for (gap = n / 2; gap > 0; gap /= 2) { for (i = 0; i for (j = i + gap; j for (int k = j; k > i && arr[k] swap(arr[k-gap], arr[k]); } } } }}
堆排序
简介
堆排序是指利用堆这种数据结构所设计的一种排序算法,它是一个近似完全二叉树的结构。
同时堆满足堆积的性质:即子结点的键值或索引总是小于或大于它的父节点。
复杂度与稳定性
什么是堆?
由于堆排序比较特殊,我们先了解一下堆是什么。
堆是一种非线性的数据结构,其实就是利用完全二叉树的结构来维护的一维数组,利用这种结构可以快速访问到需要的值,堆可以分为大顶堆和小顶堆。
大顶堆:每个结点的值都大于或等于其左右孩子结点的值
小顶堆:每个结点的值都小于或等于其左右孩子结点的值
过程介绍
首先把待排序的元素按照大小在二叉树位置上排列,且要满足堆的特性,如果根节点存放的是最大的数,则叫做大根堆,反之就叫做小根堆了。
根据这个特性就可以把根节点拿出来,然后再堆化下,即用父节点和他的孩子节点进行比较,取最大的孩子节点和其进行交换,再把根节点拿出来,一直循环到最后一个节点,就排序好了。
由于堆的实现图解需要很长篇幅,故这里不画图,肖遥会单独出一篇堆的图解,感谢关注。其代码实现如下。
主要代码实现
/* Function: 交换交换根节点和数组末尾元素的值*/void Swap(int *heap, int len) { int temp;
temp = heap[0]; heap[0] = heap[len-1]; heap[len-1] = temp;}
/* Function: 构建大顶堆 */void BuildMaxHeap(int *heap, int len) { int i,temp; for (i = len/2-1; i >= 0; i--) { if ((2*i+1) temp = heap[i]; heap[i] = heap[2*i+1]; heap[2*i+1] = temp; /* 检查交换后的左子树是否满足大顶堆性质 如果不满足 则重新调整子树结构 */ if ((2*(2*i+1)+1 BuildMaxHeap(heap, len); } } if ((2*i+2) temp = heap[i]; heap[i] = heap[2*i+2]; heap[2*i+2] = temp; /* 检查交换后的右子树是否满足大顶堆性质 如果不满足 则重新调整子树结构 */ if ((2*(2*i+2)+1 BuildMaxHeap(heap, len); } } }}
归并排序
简介
归并排序是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法的一个非常典型的应用,其核心思想是将两个有序的数列合并成一个大的有序的序列。
复杂度与稳定性
注:归并排序需要创建一个与原数组相同长度的数组来辅助排序
过程介绍
首先将已有序的子序列合并,得到完全有序的序列,即先使每个子序列有序,再使子序列段间有序。
过程如下:
申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
设定两个指针,最初位置分别为两个已经排序序列的起始位置
比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
重复步骤c直到某一指针超出序列尾
将另一序列剩下的所有元素直接复制到合并序列尾
图示过程
第一次排序将数据分为“两个”一组,组内顺序,其次再逐个的将各组进行整合,最终完成归并排序
主要代码实现
void merge(int arr[],int l,int mid,int r) { int aux[r-l+1];//开辟一个新的数组,将原数组映射进去 for(int m=l; m<=r; m++) { aux[m-l]=arr[m]; }
int i=l,j=mid+1;//i和j分别指向两个子数组开头部分
for(int k=l; k<=r; k++) { if(i>mid) { arr[k]=aux[j-l]; j++; } else if(j>r) { arr[k]=aux[i-l]; i++; } else if(aux[i-l]-l]) { arr[k]=aux[i-l]; i++; } else { arr[k]=aux[j-l]; j++; } }}void merge_sort(int arr[],int n) {for(int sz=1; sz<=n; sz+=sz) {for(int i=0; i+sz //对局部:arr[i...sz-1]和arr[i+sz.....i+2*sz-1]进行排序 merge(arr,i,i+sz-1,min(i+sz+sz-1,n-1)); //min函数防止越界 } }}
快速排序
简介
快速排序在1960年提出,是考察次数最多的排序,无论是在大学专业课的期末考试,还是在公司的面试测试题目中,快速排序都极大的被使用,在实际中快速排序也极大的被使用。
复杂度与稳定性
过程介绍
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
在数组中选择一个基准点
分别从数组的两端扫描数组,设两个指示标志
从后半部分开始,如果发现有元素比该基准点的值小,就交换位置
然后从前半部分开始扫描,发现有元素大于基准点的值,继续交换位置
如此往复循环,然后把基准点的值放到high这个位置,排序完成
以后采用递归的方式分别对前半部分和后半部分排序,当前半部分和后半部分均有序时该数组就自然有序了。
图示过程
可以看出,在第四趟时已经达到顺序,但是仍然还是会继续计算几趟直到完成全部运算
主要代码实现
void qucik_sort(int a[],int low,int high) { int i,j,temp; i=low; j=high; if(low temp=a[low]; //设置枢轴 while(i!=j) { while(j>i&&a[j]>=temp) { --j; } if(i a[i]=a[j]; ++i; }
while(i ++i; } if(i a[j]=a[i]; --j; } } a[i]=temp; qucik_sort(a,low,i-1); qucik_sort(a,i+1,high); }}
计数排序
简介
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
计数排序算法不是基于元素比较,而是利用数组下标来确定元素的正确位置。
它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k),快于任何比较排序算法。
当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(n*log(n))
的时候其效率反而不如基于比较的排序。
复杂度与稳定性
过程介绍
找出待排序的数组中最大和最小的元素
统计数组中每个值为i的元素出现的次数,存入数组C的第i项
对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
图示过程
如下图,A为待排序的数组,C记录A中各个值的数目。
将C[i]转换为值小于等于i的元素个数。
为数组A从后向前的每个元素找到对应的B中的位置,每次从A中复制一个元素到B中,C中相应的计数减一。
当A中的元素都复制到B中后,B就是排序好的结果,如图所示。
代码实现
#include#include#include
void CountSort(int *arr, int len){ if(arr == NULL) return; int max = arr[0], min = arr[0]; for(int i = 1; i if(arr[i] > max) max = arr[i]; if(arr[i] } int size = max - min + 1; int *count =(int*)malloc(sizeof(int)*size); memset(count, 0, sizeof(int)*size);
for(int i = 0; i count[arr[i] - min]++;//包含了自己! for(int i = 1; i count[i] += count[i - 1];
int* psort =(int*)malloc(sizeof(int)*len); memset(psort, 0, sizeof(int)*len);
for(int i = len - 1; i >= 0; i--){ count[arr[i] - min]--;//要先把自己减去 psort[count[arr[i] - min]] = arr[i]; }
for(int i = 0; i arr[i] = psort[i]; }
free(count); free(psort); count = NULL; psort = NULL;}
void print_array(int *arr, int len){ for(int i=0; i printf("%d ", arr[i]); printf("\n");}
int main(){ int arr[8] = {2, 5, 3, 0, 2, 3, 0, 3}; CountSort(arr, 8); print_array(arr, 8);
return 0;}
桶式排序
简介
桶排序也称箱排序,原理是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。
桶排序是鸽巢排序
的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。
复杂度与稳定性
过程介绍
根据待排序集合中最大元素和最小元素的差值范围和映射规则,确定申请的桶个数;
遍历待排序集合,将每一个元素移动到对应的桶中;
对每一个桶中元素进行排序,并移动到已排序集合中。
图示过程
元素分布在桶中:
然后,元素在每个桶中排序:
代码实现
#include#include#includeusing namespace std;const int BUCKET_NUM = 10;
struct ListNode{ explicit ListNode(int i=0):mData(i),mNext(NULL){} ListNode* mNext; int mData;};
ListNode* insert(ListNode* head,int val){ ListNode dummyNode; ListNode *newNode = new ListNode(val); ListNode *pre,*curr; dummyNode.mNext = head; pre = &dummyNode; curr = head; while(NULL!=curr && curr->mData<=val){ pre = curr; curr = curr->mNext; } newNode->mNext = curr; pre->mNext = newNode; return dummyNode.mNext;}
ListNode* Merge(ListNode *head1,ListNode *head2){ ListNode dummyNode; ListNode *dummy = &dummyNode; while(NULL!=head1 && NULL!=head2){ if(head1->mData <= head2->mData){ dummy->mNext = head1; head1 = head1->mNext; }else{ dummy->mNext = head2; head2 = head2->mNext; } dummy = dummy->mNext; } if(NULL!=head1) dummy->mNext = head1; if(NULL!=head2) dummy->mNext = head2;
return dummyNode.mNext;}
void BucketSort(int n,int arr[]){ vector buckets(BUCKET_NUM,(ListNode*)(0));for(int i=0;i int index = arr[i]/BUCKET_NUM; ListNode *head = buckets.at(index); buckets.at(index) = insert(head,arr[i]); } ListNode *head = buckets.at(0);for(int i=1;i head = Merge(head,buckets.at(i)); }for(int i=0;i arr[i] = head->mData; head = head->mNext; }}
基数排序
简介
基数排序是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,在某些时候,基数排序法的效率高于其它的稳定性排序法。
复杂度与稳定性
图示过程
设有一个初始序列为: R {50, 123, 543, 187, 49, 30, 0, 2, 11, 100}
。
过程介绍
任何一个阿拉伯数,它的各个位数上的基数都是以0~9来表示的。所以我们不妨把0~9视为10个桶。
我们先根据序列的个位数的数字来进行分类,将其分到指定的桶中。
分类后,我们在从各个桶中,将这些数按照从编号0到编号9的顺序依次将所有数取出来。
得到的序列就是个位数上呈递增趋势的序列。
按照上图个位数排序:
{50, 30, 0, 100, 11, 2, 123, 543, 187, 49}
。接下来对十位数、百位数也按照这种方法进行排序,最后就能得到排序完成的序列。
主要代码实现
public class RadixSort { // 获取x这个数的d位数上的数字 // 比如获取123的1位数,结果返回3 public int getDigit(int x, int d) { int a[] = {1, 1, 10, 100}; // 本实例中的最大数是百位数,所以只要到100就可以了 return ((x / a[d]) % 10); }
public void radixSort(int[] list, int begin, int end, int digit) { final int radix = 10; // 基数 int i = 0, j = 0; int[] count = new int[radix]; // 存放各个桶的数据统计个数 int[] bucket = new int[end - begin + 1]; // 按照从低位到高位的顺序执行排序过程 for (int d = 1; d <= digit; d++) { // 置空各个桶的数据统计 for (i = 0; i count[i] = 0; } // 统计各个桶将要装入的数据个数 for (i = begin; i <= end; i++) { j = getDigit(list[i], d); count[j]++; } // count[i]表示第i个桶的右边界索引 for (i = 1; i count[i] = count[i] + count[i - 1]; } // 将数据依次装入桶中 // 这里要从右向左扫描,保证排序稳定性 for (i = end; i >= begin; i--) { j = getDigit(list[i], d); // 求出关键码的第k位的数字, 例如:576的第3位是5 bucket[count[j] - 1] = list[i]; // 放入对应的桶中,count[j]-1是第j个桶的右边界索引 count[j]--; // 对应桶的装入数据索引减一 } // 将已分配好的桶中数据再倒出来,此时已是对应当前位数有序的表 for (i = begin, j = 0; i <= end; i++, j++) { list[i] = bucket[j]; } } }
public int[] sort(int[] list) { radixSort(list, 0, list.length - 1, 3); return list; }
// 打印完整序列 public void printAll(int[] list) { for (int value : list) { System.out.print(value + "\t"); } System.out.println(); }
public static void main(String[] args) { int[] array = {50, 123, 543, 187, 49, 30, 0, 2, 11, 100}; RadixSort radix = new RadixSort(); System.out.print("排序前:\t\t"); radix.printAll(array); radix.sort(array); System.out.print("排序后:\t\t"); radix.printAll(array); }}
总结
10种排序算法对比,我们了解到了各种排序的原理及优缺点,记住任何一种排序都不是十全十美的,因此在我们实际应用中,最好的方式就是扬长避短。
- EOF -
推荐阅读 点击标题可跳转
1、图解排序算法:归并排序
2、图解排序算法:快速排序
3、深入理解快速排序和 STL 的 sort 算法
觉得本文有帮助?请分享给更多人
关注「算法爱好者」加星标,修炼编程内功
点赞和在看就是最大的支持❤️
c++ 不插入重复元素但也不排序_面试官爱问的 10 大经典排序算法,20+ 张图来搞定...相关推荐
- 面试官爱问的10大经典排序算法,20+张图来搞定
冒泡排序 简介 冒泡排序是因为越小的元素会经由交换以升序或降序的方式慢慢浮到数列的顶端,就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名冒泡排序. 复杂度与稳定性 思路原理 以顺序为例 从第一 ...
- 10大经典排序算法,20+张图就搞定
作者 | 李肖遥 来源 | 技术让梦想更伟大 冒泡排序 简介 冒泡排序是因为越小的元素会经由交换以升序或降序的方式慢慢浮到数列的顶端,就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名冒泡排序. ...
- 实现时间排序_面试官:手撕十大排序算法,你会几种?
推荐阅读: 去面试大厂被 Kafka 虐了,后悔没有早点看到这份Kafka手写笔记 面试阿里,京东,百度,快手归来,三年Java开发总结了这些经验 阿里,字节,腾讯,面试题都涵盖了,这一份Java面试 ...
- 交换排序图解_10大经典排序算法,20+张图就搞定
作者 | 李肖遥 来源 | 技术让梦想更伟大 冒泡排序 简介 冒泡排序是因为越小的元素会经由交换以升序或降序的方式慢慢浮到数列的顶端,就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名冒泡排序. ...
- c 语言从大到小排序算法,10 大经典排序算法(动图演示+ C 语言代码)
原标题:10 大经典排序算法(动图演示+ C 语言代码) 来源:C语言与CPP编程 以前也零零碎碎发过一些排序算法,但排版都不太好,又重新整理一次,排序算法是数据结构的重要部分,系统地学习很有必要. ...
- 算法大总结之----10大经典排序算法(从小到大排列)
目录 1. 冒泡排序 1.1. 算法讲解 1.2. 代码实现 2. 选择排序 2.1. 算法讲解 2.2. 代码实现 3 插入排序 2.1. 算法讲解 2.2. 代码实现 4 希尔排序 2.1. 算法 ...
- 面试必问:十大经典排序算法总结
0.排序算法的说明0.1 排序的定义 对一序列对象根据某个关键字进行排序. 0.2术语说明 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面:不稳定:如果a原本在b前面,而a=b,排序之后 ...
- 归并排序验证性实验_攻略 | 10 大经典排序算法(Python 版)
全国信息学大神和家长在这里 排序算法是<数据结构与算法>中最基本的算法之一. 排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能 ...
- 数据结构:10大经典排序
排序 1.冒泡排序 2.选择排序 3.插入排序 4.希尔排序 5.快速排序 6.归并排序 7.堆排序 8.计数排序 9.桶排序 10.基数排序 1.冒泡排序 // 冒泡排序 #include < ...
最新文章
- 理解特征统计偏差、方差、平均值、中位数、百分数等等
- Windows10开发手记-RelativePanel使用详解
- 腾讯再次开源三项技术,提升企业运维效率
- 全面预测我国量子通信市场规模及发展趋势
- 跨境商品的进口税额显示
- 【Elasticsearch】elasticsearch shard 分片
- php定位和天气,基于thinkphp实现根据用户ip判断地理位置并提供对应天气信息的应用_PHP教程...
- cisco2960开启snmp
- OpenCV实现图像翻转
- oracle 丁勇 从零开始学_8.1.6 BETWEEN、IN和LIKE范围查询(1)
- echarts数据包坐标拾取工具
- Win10 安装IE11失败错误代码0x80070490(未解决)
- def demo什么意思python_你知道Python的所有入门级知识吗?,这些,都,会,了
- 如何创建一个基本的魔兽全图外挂 HowTo create a basic Maphack by Chaotic
- 英语语言标准C1,【CEFR】国际通用的学生英语能力水平评测标准
- 进制转换应用场景_【Android】单位换算软件来袭,帮你解决生活中所有的进制换算问题,让你轻松秒变大神!...
- 用Moment.js 计算两个时间直接的间隔
- 云计算具有哪些优势 如何快速系统学习云计算
- GitHub下载代理设置
- Android Studio完成简单UI设计
热门文章
- yml和properties的加载顺序
- go加载python_python培训 | python调用go语言来提速
- numpy 矩阵与向量相乘_高能!8段代码演示Numpy数据运算的神操作
- JS去除字符串去除最后的逗号
- 基本类型理解巩固及补码原理总结
- LeetCode算法入门- Compare Version Numbers -day14
- client中周期性边界_(整理)周期性边界条件.
- python gamma函数_python gamma矫正
- php post请求后端拿不到值_Ajax 提交POST后,后台php 无法获取$POST值
- delphi 执行长时间存储过程 显示进度_项目管理_十大管理体系之「项目进度管理」知识整理及心得分享...