十种常见排序算法可以分为两大类:
这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。
它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。
冒泡排序算法的原理如下:
#include
using namespace std;
template<typename T>
//整数或浮点数皆可使用
void bubble_sort(T arr[], int len)
{
int i, j; T temp;
for (i = 0; i < len - 1; i++)
for (j = 0; j < len - 1 - i; j++)
if (arr[j] > arr[j + 1])
{
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
int main()
{
int arr[] = { 61, 17, 29, 22, 34, 60, 72, 21, 50, 1, 62 };
int len = (int) sizeof(arr) / sizeof(*arr);
bubble_sort(arr, len);
for (int i = 0; i < len; i++)
cout << arr[i] << ' ';
cout << endl;
float arrf[] = { 17.5, 19.1, 0.6, 1.9, 10.5, 12.4, 3.8, 19.7, 1.5, 25.4, 28.6, 4.4, 23.8, 5.4 };
len = (int) sizeof(arrf) / sizeof(*arrf);
bubble_sort(arrf, len);
for (int i = 0; i < len; i++)
cout << arrf[i] << ' ';
return 0;
}
快速排序(Quicksort)是对冒泡排序算法的一种改进。
冒泡排序是将具有“相对大/小”的这种性质的数字进行逐个位置确认并交换位置,而快速排序在这种位置确认操作模式的基础上,引入了“分治”的思想。
快速排序使用分而治之的思想,通过一趟排序将待排序列分割成两部分,其中一部分均比另一部分小。之后分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
快速排序算法通过多次比较和交换来实现排序,其排序流程如下:
对于分治算法,当每次划分时,算法若都能分成两个等长的子序列时,那么分治算法效率会达到最大。通过上述流程我们可以发现,分界值的选择是很重要的。选择分界值的方式决定了两个分割后两个子序列的长度,进而对整个算法的效率产生决定性影响。
最理想的方法是,选择的分界值恰好能把待排序序列分成两个等长的子序列
一般的选择方法有三种:
方法(1):固定位置
方法(2):随机选取基准
方法(3):三数取中(median-of-three)
快排的确定是面对已经排好的序列与全部相等的序列进行排序时效率很差,因此我们要对它进行优化,可以参考这篇博客快速排序优化
朴素版:
#define _CRT_SECURE_NO_WARNINGS
#include
using namespace std;
const int N = 1000010;
int num[N];
void QUICK_sort(int num[], int l, int r) {
if (l >= r) return;
int i = l - 1, j = r + 1, x = num[l + r >> 1];
while (i < j)
{
do i++; while (num[i] < x);
do j--; while (num[j] > x);
if (i < j) swap(num[i], num[j]);
}
QUICK_sort(num, l, j);
QUICK_sort(num, j + 1, r);
}
最终优化版本:
/*函数作用:取待排序序列中low、mid、high三个位置上数据,选取他们中间的那个数据作为枢轴*/
int SelectPivotMedianOfThree(int arr[],int low,int high)
{
int mid = low + ((high - low) >> 1);//计算数组中间的元素的下标
//使用三数取中法选择枢轴
if (arr[mid] > arr[high])//目标: arr[mid] <= arr[high]
{
swap(arr[mid],arr[high]);
}
if (arr[low] > arr[high])//目标: arr[low] <= arr[high]
{
swap(arr[low],arr[high]);
}
if (arr[mid] > arr[low]) //目标: arr[low] >= arr[mid]
{
swap(arr[mid],arr[low]);
}
//此时,arr[mid] <= arr[low] <= arr[high]
return arr[low];
//low的位置上保存这三个位置中间的值
//分割时可以直接使用low位置的元素作为枢轴,而不用改变分割函数了
}
void QSort(int arr[],int low,int high)
{
int first = low;
int last = high;
int left = low;
int right = high;
int leftLen = 0;
int rightLen = 0;
if (high - low + 1 < 10)
{
InsertSort(arr,low,high);
return;
}
//一次分割
int key = SelectPivotMedianOfThree(arr,low,high);//使用三数取中法选择枢轴
while(low < high)
{
while(high > low && arr[high] >= key)
{
if (arr[high] == key)//处理相等元素
{
swap(arr[right],arr[high]);
right--;
rightLen++;
}
high--;
}
arr[low] = arr[high];
while(high > low && arr[low] <= key)
{
if (arr[low] == key)
{
swap(arr[left],arr[low]);
left++;
leftLen++;
}
low++;
}
arr[high] = arr[low];
}
arr[low] = key;
//一次快排结束
//把与枢轴key相同的元素移到枢轴最终位置周围
int i = low - 1;
int j = first;
while(j < left && arr[i] != key)
{
swap(arr[i],arr[j]);
i--;
j++;
}
i = low + 1;
j = last;
while(j > right && arr[i] != key)
{
swap(arr[i],arr[j]);
i++;
j--;
}
QSort(arr,first,low - 1 - leftLen);
QSort(arr,low + 1 + rightLen,last);
}
插入排序,一般也被称为直接插入排序。对于少量元素的排序,它是一个有效的算法。
插入排序是一种最简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动。
算法过程:
这种算法关键词相同的数据元素将保持原有位置不变,所以该算法是稳定的。
插入排序适用于已经有部分数据已经排好,并且排好的部分越大越好。一般在输入规模大于1000的场合下不建议使用插入排序。
void sort(int a[],int len)
{
//将a[]按升序排列
for (int i=1;i<len;i++)
{
//将a[i]插入到a[i-1],a[i-2],a[i-3]……之中
for(int j=i;j>0&&(a[j]<a[j-1);j--)
{
int temp=a[j];
a[j]=a[j-1];
a[j-1]=temp;
}
}
}
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。
运行过程:
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
void swap(int *a,int *b) //交換兩個變數
{
int temp = *a;
*a = *b;
*b = temp;
}
void selection_sort(int arr[], int len)
{
int i,j;
for (i = 0 ; i < len - 1 ; i++)
{
int min = i;
for (j = i + 1; j < len; j++) //走訪未排序的元素
if (arr[j] < arr[min]) //找到目前最小值
min = j; //紀錄最小值
swap(&arr[min], &arr[i]); //做交換
}
}
希尔排序(Shell’s Sort)是插入排序的一种,又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本,该算法是冲破O(n2)的第一批算法之一,十分经典的体现了算法的进步。希尔排序是非稳定排序算法。该方法因 D.L.Shell 于 1959 年提出而得名。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
算法步骤:
void shell_sort(int arr[], int len) {
int gap, i, j;
int temp;
for (gap = len >> 1; gap > 0; gap >>= 1)
for (i = gap; i < len; i++) {
temp = arr[i];
for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap)
arr[j + gap] = arr[j];
arr[j + gap] = temp;
}
}
在《算法导论》这本书中,讲到分治思想时举得例子就是归并排序(Merge Sort),归并排序是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
算法步骤:
申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
设定两个指针,最初位置分别为两个已经排序序列的起始位置;
比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
重复步骤 3 直到某一指针达到序列尾;
将另一序列剩下的所有元素直接复制到合并序列尾。
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。
迭代版本:
int min(int x, int y) {
return x < y ? x : y;
}
void merge_sort(int arr[], int len) {
int *a = arr;
int *b = (int *) malloc(len * sizeof(int));
int seg, start;
for (seg = 1; seg < len; seg += seg) {
for (start = 0; start < len; start += seg * 2) {
int low = start, mid = min(start + seg, len), high = min(start + seg * 2, 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++];
}
int *temp = a;
a = b;
b = temp;
}
if (a != arr) {
int i;
for (i = 0; i < len; i++)
b[i] = a[i];
b = a;
}
free(b);
}
递归版本:
#define _CRT_SECURE_NO_WARNINGS
#include
using namespace std;
const int N = 1000010;
int num[N],tmp[N];
void GB_sort(int num[], int l, int r) {
if (l >= r)return;
int mid = l + r >> 1;
GB_sort(num, l, mid);
GB_sort(num, mid + 1, r);
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r)
if (num[i] <= num[j]) tmp[k++] = num[i++];
else tmp[k++] = num[j++];
while (i <= mid) tmp[k++] = num[i++];
while (j <= r)tmp[k++] = num[j++];
for (i = l, j = 0; i <= r; i++, j++)
num[i] = tmp[j];
}
int main() {
int x;
scanf("%d", &x);
for (int i = 0; i < x; i++)
scanf("%d", &num[i]);
GB_sort(num, 0, x - 1);
for (int i = 0; i < x; i++)
printf("%d ", num[i]);
return 0;
}
堆排序(Heap Sort)是指利用堆这种数据结构所设计的一种排序算法。
堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
算法过程:
#include
#include
using namespace std;
const int N =1e5+10;
int n,m,Size=0;
int d[N];
void down(int x){
int t = x;
if(2*x<=Size&&d[2*x]<d[t])t=2*x;
if(2*x+1<=Size&&d[2*x+1]<d[t])t=2*x+1;
if(t != x){
swap(d[t],d[x]);
down(t);
}
return ;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>d[i];
Size = n;
for(int i = Size/2 ; i>=1 ; i--)down(i);
for(int i=1;i<=m;i++)
{
printf("%d ",d[1]);
d[1]=d[Size];
Size--;
down(1);
}
return 0;
}
计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。
当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(n* log(n))的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(n* log(n)), 如归并排序,堆排序)
算法过程:
#include
using namespace std;
const int MAXN = 100000;
const int k = 1000; // range(范围)
int a[MAXN], c[MAXN], ranked[MAXN];
int main() {
int n;
cin >> n;
for (int i = 0; i < n; ++i) {
cin >> a[i];
++c[a[i]];
}
for (int i = 1; i < k; ++i)
c[i] += c[i-1];
for (int i = n-1; i >= 0; --i)
ranked[--c[a[i]]] = a[i];//如果是i表达的是原数标号,a[i]就是排序后的正确序列
for (int i = 0; i < n; ++i)
cout << ranked[i] << endl;
return 0;
}
桶排序 (Bucket sort)或所谓的箱排序是计数排序的改进,工作的原理是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。
桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(O(n))。但桶排序并不是比较排序,他不受到 O(n * log n) 下限的影响。
桶排序算法要求,数据的长度必须完全一样,程序过程要产生长度相同的数据
数据结构设计:链表可以采用很多种方式实现,通常的方法是动态申请内存建立结点,但是针对这个算法,桶里面的链表结果每次扫描后都不同,就有很多链表的分离和重建。如果使用动态分配内存,则由于指针的使用,安全性低。
所以,笔者设计时使用了数组来模拟链表(当然牺牲了部分的空间,但是操作却是简单了很多,稳定性也大大提高了)。
平均情况下桶排序以线性时间运行。像基数排序一样,桶排序也对输入作了某种假设, 因而运行得很快。具体来说,基数排序假设输入是由一个小范围内的整数构成,而桶排序则假设输入由一个随机过程产生,该过程将元素一致地分布在区间[0,1)上。 桶排序的思想就是把区间[0,1)划分成n个相同大小的子区间,或称桶,然后将n个输入数分布到各个桶中去。因为输入数均匀分布在[0,1)上,所以一般不会有很多数落在一个桶中的情况。
为得到结果,先对各个桶中的数进行排序,然后按次序把各桶中的元素列出来即可。
#include
#include
#include
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;
}
}
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或(bin sort),顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog®m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
实现原理:
基数排序的发明可以追溯到1887年赫尔曼·何乐礼在打孔卡片制表机(Tabulation Machine)上的贡献。它是这样实现的:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
基数排序的方式可以采用LSD(Least significant digital)或最高位优先MSD(Most Significant Digit first)法,LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。
MSD法:先按k1排序分组,同一组中记录,关键码k1相等,再对各组按k2排序分成子组,之后,对后面的关键码继续这样的排序分组,直到按最次位关键码kd对各子组排序后。再将各组连接起来,便得到一个有序序列。
LSD法:先从kd开始排序,再对kd-1进行排序,依次重复,直到对k1排序后便得到一个有序序列
#include
testBS()
{
inta[] = {2, 343, 342, 1, 123, 43, 4343, 433, 687, 654, 3};
int *a_p = a;
//计算数组长度
intsize = sizeof(a) / sizeof(int);
//基数排序
bucketSort3(a_p, size);
//打印排序后结果
inti;
for(i = 0; i < size; i++)
{
printf("%d\n", a[i]);
}
intt;
scanf("%d", t);
}
//基数排序
voidbucketSort3(int *p, intn)
{
//获取数组中的最大数
intmaxNum = findMaxNum(p, n);
//获取最大数的位数,次数也是再分配的次数。
intloopTimes = getLoopTimes(maxNum);
inti;
//对每一位进行桶分配
for(i = 1; i <= loopTimes; i++)
{
sort2(p, n, i);
}
}
//获取数字的位数
intgetLoopTimes(intnum)
{
intcount = 1;
inttemp = num / 10;
while(temp != 0)
{
count++;
temp = temp / 10;
}
returncount;
}
//查询数组中的最大数
intfindMaxNum(int *p, intn)
{
inti;
intmax = 0;
for(i = 0; i < n; i++)
{
if(*(p + i) > max)
{
max = *(p + i);
}
}
returnmax;
}
//将数字分配到各自的桶中,然后按照桶的顺序输出排序结果
voidsort2(int *p, intn, intloop)
{
//建立一组桶此处的20是预设的根据实际数情况修改
intbuckets[10][20] = {};
//求桶的index的除数
//如798个位桶index=(798/1)%10=8
//十位桶index=(798/10)%10=9
//百位桶index=(798/100)%10=7
//tempNum为上式中的1、10、100
inttempNum = (int)pow(10, loop - 1);
inti, j;
for(i = 0; i < n; i++)
{
introw_index = (*(p + i) / tempNum) % 10;
for(j = 0; j < 20; j++)
{
if(buckets[row_index][j] == NULL)
{
buckets[row_index][j] = *(p + i);
break;
}
}
}
//将桶中的数,倒回到原有数组中
intk = 0;
for(i = 0; i < 10; i++)
{
for(j = 0; j < 20; j++)
{
if(buckets[i][j] != NULL)
{
*(p + k) = buckets[i][j];
buckets[i][j] = NULL;
k++;
}
}
}
}
参考地址:
https://www.runoob.com/
https://www.baike.baidu.com/