​  在本文中,我将介绍10种经典的排序方法,一个很简单的排序,却有很多方法可以解决,每种方法也各有优缺点,每种方法也体现了一种思想,我认为算法中最重要的就是学会一些思想,这样才能做到以不变应万变。本文中的代码均是使用的C/C++(文章中的动图参考 https://github.com/hustcc/JS-Sorting-Algorithm 。若是侵权,我立刻删除)

冒泡排序

算法思想

  • 从列表第一个元素开始,比较相邻两个元素值的大小,让小的到前面。
  • 当遍历完一轮后,最大的元素就已经到最后一个了,所以共需遍历n-1次。

动图展示

冒泡排序动态演示

代码部分

#include<iostream>
using namespace std;
void bubblesort(int data [],int n)
{
	int temp;
	for(int j=0;j<n-1;j++)
	{
		for(int i=0;i<n-1;i++)
		{
			if(data[i]>data[i+1])
			{
				temp=data[i];
				data[i]=data[i+1];
				data[i+1]=temp;
			}
		}
	}
}
int main()
{
	int a;
	cin>>a;
	int*b=new int[a];
	for(int i=0;i<a;i++)
	{
		cin>>b[i];
	}
	bubblesort(b,a);
	for(int j=0;j<a;j++)
	{
		cout<<b[j]<<" ";
	}
	delete []b;
}

这个代码理解起来很容易,n个数需要进行n-1轮遍历,每轮遍历需要进行n-1次,然后就是比较大小,让小的到前面即可。

选择排序

算法思想

  • 选择排序,顾名思义,就是选出来一个进行排序。该算法分为已排序部分和未排序部分。
  • 具体而言,对于未排序的部分,遍历出来最小值,让其到最前面,归到已排序的部分。
  • 遍历n-1轮后完成整个数组的排序。

动图展示

image1

代码部分

void selectsort(int arr[],int n)
{
	int temp;
	for(int i=0;i<n;i++)
	{
		for(int j=i+1;j<n;j++)
		{
			if(arr[j]<arr[i]);
			temp=arr[i];
			arr[i]=arr[j];
			arr[j]=temp;
		}
	}	
}
int main()
{
	int a;
	cin>>a;
	int *b=new int[a];
	for(int i=0;i<a;i++)
	{
		cin>>b[i];
	}
	selectsort(b,a);
	for(int i=0;i<a;i++)
	{
		cout<<b[i]<<" ";
	}
}

该代码理解起来也比较容易,就不过多叙述了。

插入排序

  • 顾名思义,就是对于一个数,把他放到一个合适的位置。整个数组分为已排序部分和未排序部分。
  • 初始时,第一个数相当于是有序的,故从第二个数开始插,将其插入到合适的位置。
  • 重复上述步骤,直至整个数组均为有序。

动图展示

image1

代码部分

void insertion_sort(int arr[],int len)
{
	for(int i=1;i<len;i++)
	{
		int key=arr[i];
		int j=i-1;
		while((j>=0)&&(key<arr[j]))
		{
			arr[j+1]=arr[j];
			j--;
		}
		arr[j+1]=key;
	 } 
}
  • 第一个for循环中的i表示的对第i个数进行插入操作,往下的j=i-1,是i前面的数。
  • 循环体条件((j>=0)&&(key < arr[j])),j是下标,其最小是0,如果key要是比arr[j]小,那么arr[j]就往后移(给key让位置,因为它得往前面插),key再接着比较前面的数。
  • 当第j个数不满足循环体条件时,则key就找到位置了,就到第j个数的后面。

希尔排序

Shell Sort是插入排序的一种改进的版本,其先将数组分成若干字数组,对每个子数组进行插入排序,逐步缩小子数组的间隔,最终完成整个数组的排序。

  • 先选择一个增量序列,将列表分成若干子列表。
  • 对每个子列表进行插入排序。
  • 逐步缩小增量,重复上述分组和排序过程,直到增量为1。

代码部分

void shellsort(T array [],int n){
	int h=1;
	while(h<n/3)
	{
		h=h*3+1;
	}
	while(h>=1)
	{

		for(int i=h;i<n;i++)
		{
			for(int j=i;j>=h&&array[j]<array[j-h];j-=h)
			{
				std:swap(array[j],array[j-h]);
			}
		}
	h/=3;
	}
}

归并排序

归并排序的核心思想是将一个大问题分解成若干小问题,然后分别解决这些小问题,然后将结果合并起来,最终得到整个问题的解。

算法思想

  • 分解:将待排序的数组分成两个子数组,每个子数组包含大约一半的元素。
  • 解决,递归的对每个子数组进行排序。
  • 合并,将以排序的子数组合并成一个有序的数组。

动图展示

image1

代码部分

1.迭代的方法

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*2)
        {
            int low=start;
            int mid=min(start+seg,len);
            int 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&&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;
}
  • 先建立了a和b两个指针,一个指向传入数组的首地址,一个指向新建数组的首地址。
  • seg可以理解为已序子数组的长度,一开始每个元素可以看成是一个已序的子数组,所以seg开始设为1,每次合并时已序部分的长度会×2。
  • start1和start2指向相邻两个已序字数组的第一个元素,下面三个while循环就是将这两个已序子数组合并成一个已序子数组
  • 25行的判断语句存在的意义:因为a和b会进行交换,如果交换了奇数次,a会指向新建的数组,b会指向arr,而a是排序好的数组,所以在此把a中的值赋给b,并让b指向a,最后再delete b,这样不会影响到arr。

2.递归的方法

void merge_sort_recursive(int arr[],int reg[],int start,int end){
	if (start>=end)
	{
		return;
	}
	int len=end-start,mid=start+(len>>1);
	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];
	}
}
void merge_sort_recursion(int arr[],const int len)
{
	int reg[len];
	merge_sort_recursive(arr,reg,0,len-1);
}
  • 先把整个数组进行分割,划分成两部分,然后对左边这部分进行排序后再对右边这部分进行排序,最后将这两部分进行合并
  • 终止条件就是start和end的大小关系,最小的子数组的长度是2,再划分后就会终止,然后对这个子数组排序

快速排序

Quick Sort是一种高效的排序散发,基于分治法的核心思想。他的核心思想是选择一个基准元素(pivot),将列表分为两部分,一部分小于基准元素,一部分大于基准元素,然后递归地对这两部分进行排序,快速排序的时间复杂度是O(n log n),在实际应用中有优异的性能。

算法思想

  • 从列表中随机选择一个元素作为基准(可以随机选择),一下代码中选择的是第一个元素。
  • 将列表重新排序,小于基准元素的放到基准的左侧,大于基准元素的放到基准元素的右侧。
  • 对基准元素的左侧和右侧的字数组分别进行快速排序
  • 递归结束后也就对整个数组进行了排序。

动图展示

image1

代码部分

int Partition(int A[], int left, int right) {
     int pivot=A[left];
     while(left<right)
     {
		while(left<right && A[right]>=pivot)
		{
			right--;
		}
		A[left]=A[right];
		while(left<right && A[left]<=pivot)
		{
			left++;
		}
		A[right]=A[left];
	 }
	 A[left]=pivot;
	 return left;
}
// 快速排序函数
void QuickSort(int A[], int low, int high) 
{
    if(low<high)
    {
    	int pivot=Partition(A,low,high);
    	QuickSort(A,low,pivot-1);
    	QuickSort(A,pivot+1,high);
	}
}
  • Partition()这个函数是用于返回选中元素的下标,是一个标准分割数组的函数
  • 然后递归的对左侧和右侧进行排序

堆排序

Heapsort是利用堆这种数据结构所设计的一种排序方法。堆积是一种近似完全二叉树,并满足堆积的性质:即子节点的值总是小于(或者大于)它的父节点。

算法步骤

  • 先创建一个堆[0…..n-1];
  • 把堆首(最大值)和堆尾互换,让堆首成为已序部分;
  • 把堆的尺寸缩小1,然后把新的数组顶端的数据调整至相应的部分
  • 重复步骤2,直到堆的尺寸为1

动图展示

image1

代码部分

void max_heapify(int arr[],int start,int end)
{
	int dad=start;
	int son=dad*2+1;
	while(son<=end)
	{
		if(son+1<=end && arr[son+1]>arr[son])
		{
			son++;
		}
		if(arr[dad]>arr[son])
		{
			return;
		}
		else
		{
			swap(arr[dad],arr[son]);
			dad=son;
			son=2*dad+1;
		}
	}

}
void heap_sort(int arr[],int len)   //堆排序
{
    for(int i=len/2-1;i>=0;i--)
    {
    	max_heapify(arr,i,len-1);
 	}
	for(int i=len-1;i>=0;i--)
	{
		swap(arr[0],arr[i]);
		max_heapify(arr,0,i-1);
	}
}
  • max_heapify()函数的作用是把从start到end的这些数据形成一个大顶推
  • i=len/2-1,代表的是最后一个非叶子结点,第一个for循环的目的就是形成一个堆
  • 第二个for循环的目的是把二叉树的最后一个元素和第一个元素交换位置,并去掉归到已序部分,然后max_heapify(arr,0,i-1)是将顶部的元素移动到合适的位置

基数排序

基数排序通过逐位比较元素的每一位(从最低位到最高位)来实现排序,相当于第一次排序将个位数排好,第二次排序将十位数排好,以此类推。

算法步骤

  • 找到列表中最大数字的位数,确定需要排序的轮数
  • 从最低位开始,依次对每一位进行排序
  • 每一轮排序后,更新列表的排序,直到所有位数排序完成

动图展示

image1

代码部分

int maxbit(int data [],int n)
{
	int maxdata=data[0];
	for(int i=1;i<n;i++)
	{
		if(maxdata<data[i])
		{
			maxdata=data[i];
		}
	}
	int d=1;
	while(maxdata>=10)
	{
		maxdata/=10;
		d++;
	}
	return d;
}

void radixsort(int data[],int n)
{
	int d=maxbit(data,n);
	int *temp=new int[n];
	int *count=new int[10];
	int i,j,k;
	int radix=1;
	for(int i=1;i<=d;i++)
	{
		for(j=0;j<10;j++)
		{
			count[j]=0;
		}
		for(j=0;j<n;j++)
		{
			k=(data[j]/radix)%10;
			count[k]++;
		}
		for(j=1;j<10;j++)
		{
			count[j]=count[j]+count[j-1];
		}
		for(j=n-1;j>=0;j--)
		{
			k=(data[j]/radix)%10;
			temp[count[k]-1]=data[j];
			count[k]--;
		}
		for(j=0;j<n;j++)
		{
			data[j]=temp[j];;
		}
		radix*=10;
	}
	delete []temp;
	delete []count;
}
  • maxbit()先return 数组中最大值的位数,temp[]是每轮排序结束后把数据暂时放到这个数组中,count[10]如动图中,底部的10个桶
  • 每轮排序前先将桶中的元素清零,然后再根据这一轮中的某一位数将数据放到桶中,接下来的两个for循环是关键,这样确定了排序前的d[j]在排序后应该在temp中的哪个位置,这个设计很巧妙
  • 最后再把排序后temp[]中的数值赋给data[]用于下一轮中的排序

桶排序

桶排序是一种分布式算法,它将待排序的元素分配到若干个桶中,然后对每个桶中的元素进行排序,最后将所有桶中的元素按顺序合并

算法步骤

  • 根据数据的范围和分布,创建若干个桶
  • 遍历待排序的列表,将每个元素分配到对应的同中
  • 对每个桶中的元素进行排序
  • 将所有桶中的元素按顺序合并,得到最终的排序结果

代码部分

#include <iostream>
#include <algorithm>
#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;
    dummyNode.mNext = head;
    ListNode* pre = &dummyNode;
    ListNode* curr = head;
    ListNode* newNode = new ListNode(val);

    while (curr != NULL && 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* tail = &dummyNode;

    while (head1 != NULL && head2 != NULL) {
        if (head1->mData < head2->mData) {
            tail->mNext = head1;
            head1 = head1->mNext;
        } else {
            tail->mNext = head2;
            head2 = head2->mNext;
        }
        tail = tail->mNext;
    }

    if (head1 != NULL) tail->mNext = head1;
    if (head2 != NULL) tail->mNext = head2;

    return dummyNode.mNext;
}

void BucketSort(int n, int arr[]) {
    vector<ListNode*> buckets(BUCKET_NUM, NULL);
    int maxData = *max_element(arr, arr + n);

    for (int i = 0; i < n; i++) {
        int index = (arr[i] * BUCKET_NUM) / (maxData + 1);
        buckets[index] = insert(buckets[index], arr[i]);
    }

    ListNode* head = buckets[0];
    for (int i = 1; i < BUCKET_NUM; i++) {
        head = Merge(head, buckets[i]);
    }

    ListNode* curr = head;
    for (int j = 0; j < n; j++) {
        arr[j] = curr->mData;
        ListNode* temp = curr;
        curr = curr->mNext;
        delete temp;
    }
}
  • int index=(arr[i]*BUCKET_NUM)/(maxData+1)来确定arr[i]应该分配到那个桶当中(根据桶的个数和最大值来确定)
  • 通过链表的插入来对每个桶内的元素排序
  • 通过合并链表来有序的合并桶内的元素