基数排序
桶排序
首先来看看桶排序:
如果学生有1000个,我们只需要设置100个桶即可,然后按照桶从0-100来按顺序排序即可。
为什么桶排序的时间复杂度是O(M+N)?
因为分配的时间复杂度是M(也就是桶的数量),而收集起来的时间复杂度是N,因此最终时间复杂度是O(M+N)。
上面那种是理想的桶排序,因为最终成绩的范围是在小范围内。
但是如果成绩的范围很大:
对于一个数,例如523,排序的思想很简单,比它大的排它后面,比它小的排它前面。
那么一个数如果比523小,可以分为三种情况:
- 百位数比5小
- 百位数=5,十位数比2小
- 百位数=5,十位数=2,个位数比3小
那接下来来介绍一下基数排序,它是在桶排序的基础上进行优化的。基数排序就是对上面这三种情况都进行了排序。
基数排序又称多关键字排序,分为两种,MSD(Most Significant Digit)和LSD(Least Significant Digit)。
其中我们看MSD,花色是更为重要的,其次看面值,而MSD就是给花色建4个桶,然后在桶内再用别的排序方法。
LSD就相反:
在大多数情况下LSD会比MSD的效率更高。
算法原理
那详细讲下多关键字排序的算法(这里采用LSD):
拿数字做例子(注意结合上面我说的523):
链式基数排序
在基数排序的实现方法中,通常用链式基数排序来实现:
使用链表效率更高,更方便些。当收集时,只需要修改指针,不需要移动元素
例如对三位数进行排序,我们可以这样:
(这里的队列其实不是stl的队列,而是类似桶的存在)
队列头尾指针也可以使用二级指针:
node **f;
node **r;f=new node*[10]; r=new node*[10];
或者使用队列: queue q[10];
我们需要进行n趟分配和收集,n为数字的长度,每次分配和收集需要做这些事情:
看下面这个例子
而每次分配完之后需要将结果收集起来,从左至右(也就是从小到大、也就是升序)的收集起来。
然后接下来从最次位一直到第一位,对每种情况都分配桶。
最后收集的时候就可以得到排序的结果了,排序的次数是固定的,是你这个数的长度。
其实不难理解,对于上面的第一种情况,在第三趟分配和收集的时候会解决。
对于上面的第二种情况(百位相同十位不同),在第二趟分配和收集的时候会解决。
而上面的第三种情况,在第三趟分配和收集的时候会解决。
收集的作用
为什么分配完后要收集?
收集有什么作用?为什么可以体现这种作用?
在每次分配完桶的时候,要把结果从前往后串联下来,(可以想象一下按顺序整理扑克牌,A在最左边,K在最右边,然后从左往右把牌都收起来)。
例如523和521
收集的目的是为了针对上面所说的第三种情况,因为我们可以知道如果百位和十位相同,那么百位和十位分配桶的时候,是会分配到一个桶里面的。
那么我们希望最终结果中,百位和十位相同的情况下,个位小的,应该在数组的前面。
因此这时候在最终的分配中,因为百位和十位相同,所以这两个在一个桶里,这时候我们就希望最终个位小的应该在队列更靠近前面的地方,这通过什么来实现呢?在链表结构中是表尾插入,那这就看,谁先插入,谁就在前面。
那这靠什么来实现呢?就靠最开始个位分配桶的时候,我们是从小的(也就是左边)开始往大的(也就是右边)串联起来,这就保证了,个位小的会出现在前面,也就是我们重新进行分配的时候,个位小的会更先进行分配。
性能分析
代码实现
要注意几个问题:
1、给定一组数据,到底要进行多少趟?
输入数据的同时,选择出最大的数,然后转换成字符串,求其长度
#include<bits/stdc++.h>
stringstream sstr;
sstr << max;
string s= sstr.str(); //或者string s; sstr>>s;
weishu=s.length();
或者写个循环,每次除以10(同时计数),直到商为0
2、如何求第i位的值?
for(i=1;i<=weishu;i++)
for(j=1;j<=len;j++)
int temp=(data[j] / (int)pow(10,i-1)) % 10;
class node{
public: //结点结构
int e;
node *next;
node(){next=NULL;}
};
class Sort{
int len; //数据长度
node *head; //头结点
node *f[10];//队头指针
node *r[10];//队尾指针
int weishu; //处理位数
public: //各种方法
void init(){//将队头队尾指针设置成NULL
for(int i=0;i<10;i++){
f[i]=NULL;
r[i]=NULL;
}
}
void RadixSort(){
int i; node* p;
for(i=1;i<=weishu;i++){
init(); //将队头队尾指针设置成NULL
p=head->next; //开始一趟分配的过程
while(p){
int w=( p->e /(int)pow(10,i-1))%10; //取得第i位的数据
if(f[w]==NULL) f[w]=p; //若是该队列的第一个数据
else r[w]->next=p; //否则,在队尾插入
r[w]=p; //队尾指针指向新插入的数据
p=p->next;
}
p=head; //开始一趟收集的过程
for(int i=0;i<10;i++){ //共10个队列
if(f[i]){ //若第i个队列不空
p->next=f[i];
p=r[i];
}
}
p->next=NULL; //收集结束,链表加上结束标志。
}
}
}
设立队尾指针的原因
看图举例
如果不设立队尾指针,那么我们遍历十个队列的每一个队列的时候,比如遍历f[0],从f[0]的第一个元素到最后一个元素,此时遍历:008 063 083 184 589,会把不在f[0]的但是在f[0]最后一个元素后面链接的元素也遍历了!
因此需要设立队尾指针作为哨兵来查看是否遍历到了最后一个元素。
例题及完整代码
样例输入
2
10 278 109 63 930 589 184 505 269 8 83
6 57 0 93 19 18 99
样例输出
0:->930->^
1:NULL
2:NULL
3:->63->83->^
4:->184->^
5:->505->^
6:NULL
7:NULL
8:->278->8->^
9:->109->589->269->^
930 63 83 184 505 278 8 109 589 269
0:->505->8->109->^
1:NULL
2:NULL
3:->930->^
4:NULL
5:NULL
6:->63->269->^
7:->278->^
8:->83->184->589->^
9:NULL
505 8 109 930 63 269 278 83 184 589
0:->8->63->83->^
1:->109->184->^
2:->269->278->^
3:NULL
4:NULL
5:->505->589->^
6:NULL
7:NULL
8:NULL
9:->930->^
8 63 83 109 184 269 278 505 589 930
0:->0->^
1:NULL
2:NULL
3:->93->^
4:NULL
5:NULL
6:NULL
7:->57->^
8:->18->^
9:->19->99->^
0 93 57 18 19 99
0:->0->^
1:->18->19->^
2:NULL
3:NULL
4:NULL
5:->57->^
6:NULL
7:NULL
8:NULL
9:->93->99->^
0 18 19 57 93 99
答案代码:
#include<iostream>
#include<cmath>
using namespace std;
class node{
public: //结点结构
int e;
node *next;
node(){next=NULL;}
};
class Sort{
int len; //数据长度
node *head; //头结点
node *f[10];//队头指针
node *r[10];//队尾指针
int weishu; //处理位数
public: //各种方法
Sort(){
weishu=0;
cin>>len;
head=new node;
int max=0;
for(int i=0;i<len;i++){
int num;
cin>>num;
if(num>max)max=num;
node *p=new node;
p->e=num;
node *q=head;
for(int j=0;j<i;j++)q=q->next;
q->next=p;
}
while(max/10){
max/=10;
weishu++;
}
weishu++;
//display();
}
void displayData(){
node *p=head->next;
while(p){
cout<<p->e<<" ";
p=p->next;
}
cout<<endl;
//cout<<weishu<<endl;
}
void init(){//将队头队尾指针设置成NULL
for(int i=0;i<10;i++){
f[i]=NULL;
r[i]=NULL;
}
}
void RadixSort(){
int i; node* p;
for(i=1;i<=weishu;i++){
init(); //将队头队尾指针设置成NULL
p=head->next; //开始一趟分配的过程
while(p){
int w=( p->e /(int)pow(10,i-1))%10; //取得第i位的数据
if(f[w]==NULL) f[w]=p; //若是该队列的第一个数据
else r[w]->next=p; //否则,在队尾插入
r[w]=p; //队尾指针指向新插入的数据
p=p->next;
}
p=head; //开始一趟收集的过程
for(int i=0;i<10;i++){ //共10个队列
if(f[i]){ //若第i个队列不空
p->next=f[i];
p=r[i];
}
}
p->next=NULL; //收集结束,链表加上结束标志。
displayQueue();
displayData();
}
cout<<endl;
}
void displayQueue(){
for(int i=0;i<10;i++){
cout<<i<<":";
if(f[i]==NULL)cout<<"NULL";
else{
node *p=f[i];
while(p&&p!=r[i]){
cout<<"->"<<p->e;
p=p->next;
}
cout<<"->"<<r[i]->e<<"->^";
}
cout<<endl;
}
}
};
int main(){
int t;
cin>>t;
while(t--){
Sort test;
test.RadixSort();
//test.display();
}
return 0;
}