冒泡排序是因为越小的元素会经由交换以升序或降序的方式慢慢浮
到数列的顶端,就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名冒泡排序。
以顺序为例
a[1]>a[2]
,就彼此交换。一遍这样的操作
为一趟冒泡排序。a[i]<a[i+1]
,即完成冒泡排序。以数组数据{ 70,50,30,20,10,70,40,60}为例:
如图,每一次排序把一个最大的数被放在了最后,然后按照这个趋势逐渐往前,直到按从小到大的顺序依次排序。
到了第4轮的时候,整个数据已经排序结束了,但此时程序仍然在进行。
直到第5,6,7轮程序才算真正的结束,这其实是一种浪费算力的表现。
void bubble_sort(int a[],int n) {
for(int i=0; i<n; i++) {
for(int j=0; j<n-i; j++) {
if(a[j]>a[j+1]) {
swap(a[j],a[j+1]); //交换数据
}
}
}
}
注意,由于C++的namespace std
命名空间的使用,std自带了交换函数swap(a,b)
,可以直接使用,其功能是交换a与b的两个值,当然你可以自定义swap函数,其模板代码为:
template<class T> //模板类,可以让参数为任意类型
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;
}
选择排序是一种简单直观的排序算法,它从待排序的数据元素中选出最小或最大的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小或最大元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。
以数据{2,10,9,4,8,1,6,5}为例
如图所示,每次交换的数据使用红颜色标记出,已经排好序的数据使用蓝底标注,
每一趟从待排序的数据元素中选出最小的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。
我们只需要进行n-1趟排序即可,因为最后剩下的一个数据一定是整体数据中最大的数据。
void select_sort(int a[],int n){
int temp;
for(int i=0;i<n-1;i++){
temp=i; //利用一个中间变量temp来记录需要交换元素的位置
for(int j=i+1;j<n;j++){
if(a[temp]>a[j]){ //选出待排数据中的最小值
temp=j;
}
}
swap(a[i],a[temp]); //交换函数
}
}
相比冒泡排序的不断交换,简单选择排序是等到合适的关键字出现后再进行交换,并且交换一次就可以达到一次冒泡的效果。
插入排序是一种最简单的排序方法,对于少量元素的排序,它是一个有效的算法。
首先将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。
每一步将一个待排序的元素,按其排序码的大小,插入到前面已经排好序的一组元素的适当位置上去,直到元素全部插入为止。
可以选择不同的方法在已经排好序数据表中寻找插入位置。根据查找方法不同,有多种插入排序方法,下面要介绍的是直接插入排序。
以数组数据{ 70,50,30,20,10,70,40,60}为例:
将红色的数据依次插入组成一个逐渐有序的数组
void insert_sort(int a[],int n) {
int i,j;
//外层循环标识并决定待比较的数值
for(i=1; i<n; i++) { //循环从第2个元素开始
if(a[i]<a[i-1]) {
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;
}
}
}
希尔排序又称缩小增量排序
,是直接插入排序算法的一种更高效的改进版本。
希尔排序是非稳定排序算法,在对几乎已经排好序的数据操作时,效率极高,即可以达到线性排序的效率。
先将整个待排序的记录序列分组,对若干子序列分别进行直接插入排序,随着增量逐渐减少即整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
过程如下:
可以看见,相比直接插入排序由于可以每趟进行分段操作,故整体效率体现较高。
void shellSort(int arr[], int n) {
int i, j, gap;
for (gap = n / 2; gap > 0; gap /= 2) {
for (i = 0; i < gap; i++) {
for (j = i + gap; j < n; j += gap) {
for (int k = j; k > i && arr[k] < arr[k-gap]; k -= gap) {
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) < len && heap[i] < heap[2*i+1]) { /* 根节点大于左子树 */
temp = heap[i];
heap[i] = heap[2*i+1];
heap[2*i+1] = temp;
/* 检查交换后的左子树是否满足大顶堆性质 如果不满足 则重新调整子树结构 */
if ((2*(2*i+1)+1 < len && heap[2*i+1] < heap[2*(2*i+1)+1]) || (2*(2*i+1)+2 < len && heap[2*i+1] < heap[2*(2*i+1)+2])) {
BuildMaxHeap(heap, len);
}
}
if ((2*i+2) < len && heap[i] < heap[2*i+2]) { /* 根节点大于右子树 */
temp = heap[i];
heap[i] = heap[2*i+2];
heap[2*i+2] = temp;
/* 检查交换后的右子树是否满足大顶堆性质 如果不满足 则重新调整子树结构 */
if ((2*(2*i+2)+1 < len && heap[2*i+2] < heap[2*(2*i+2)+1]) || (2*(2*i+2)+2 < len && heap[2*i+2] < heap[2*(2*i+2)+2])) {
BuildMaxHeap(heap, len);
}
}
}
}
归并排序是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法的一个非常典型的应用,其核心思想是将两个有序的数列合并成一个大的有序的序列。
注:归并排序需要创建一个与原数组相同长度的数组来辅助排序
首先将已有序的子序列合并,得到完全有序的序列,即先使每个子序列有序,再使子序列段间有序。
过程如下:
第一次排序将数据分为“两个”一组,组内顺序,其次再逐个的将各组进行整合,最终完成归并排序
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]<aux[j-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<n; i+=sz+sz) { //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年提出,是考察次数最多的排序,无论是在大学专业课的期末考试,还是在公司的面试测试题目中,快速排序都极大的被使用,在实际中快速排序也极大的被使用。
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
以后采用递归的方式分别对前半部分和后半部分排序,当前半部分和后半部分均有序时该数组就自然有序了。
可以看出,在第四趟时已经达到顺序,但是仍然还是会继续计算几趟直到完成全部运算
void qucik_sort(int a[],int low,int high) {
int i,j,temp;
i=low;
j=high;
if(low<high) {
temp=a[low]; //设置枢轴
while(i!=j) {
while(j>i&&a[j]>=temp) {
--j;
}
if(i<j) {
a[i]=a[j];
++i;
}
while(i<j&&a[i]<temp) {
++i;
}
if(i<j) {
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))
的时候其效率反而不如基于比较的排序。
如下图,A为待排序的数组,C记录A中各个值的数目。
将C[i]转换为值小于等于i的元素个数。
为数组A从后向前的每个元素找到对应的B中的位置,每次从A中复制一个元素到B中,C中相应的计数减一。
当A中的元素都复制到B中后,B就是排序好的结果,如图所示。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void CountSort(int *arr, int len){
if(arr == NULL) return;
int max = arr[0], min = arr[0];
for(int i = 1; i < len; i++){
if(arr[i] > max) max = arr[i];
if(arr[i] < min) min = 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 < len; i++)
count[arr[i] - min]++;//包含了自己!
for(int i = 1; i < size; 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 < len; 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<len; 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<iterator>
#include<iostream>
#include<vector>
using 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<ListNode*> buckets(BUCKET_NUM,(ListNode*)(0));
for(int i=0;i<n;++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<BUCKET_NUM;++i){
head = Merge(head,buckets.at(i));
}
for(int i=0;i<n;++i){
arr[i] = head->mData;
head = head->mNext;
}
}
基数排序是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,在某些时候,基数排序法的效率高于其它的稳定性排序法。
设有一个初始序列为: R {50, 123, 543, 187, 49, 30, 0, 2, 11, 100}
。
任何一个阿拉伯数,它的各个位数上的基数都是以0~9来表示的。所以我们不妨把0~9视为10个桶。
{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 < radix; i++) {
count[i] = 0;
}
// 统计各个桶将要装入的数据个数
for (i = begin; i <= end; i++) {
j = getDigit(list[i], d);
count[j]++;
}
// count[i]表示第i个桶的右边界索引
for (i = 1; i < radix; 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种排序算法对比,我们了解到了各种排序的原理及优缺点,记住任何一种排序都不是十全十美的,因此在我们实际应用中,最好的方式就是扬长避短。
今天排序基础就讲到这里,下一期,我们再见!
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8