常见的五类排序算法图解和实现(多关键字排序:基数排序以及各个排序算法的总结)

基数排序思想

完全不同于以前的排序算法,可以说,基数排序也叫做多关键字排序,基数排序是一种借助“多关键字排序”的思想来实现“单关键字排序”的内部排序算法。

两种方式:

1、最高位优先,先按照最高位排成若干子序列,再对子序列按照次高位排序

2、最低位优先:不必分子序列,每次排序全体元素都参与,不比较,而是通过分配+收集的方式。

多关键字排序

例:将下表所示的学生成绩单按数学成绩的等级由高到低排序,数学成绩相同的学生再按英语成绩的高低等级排序。

      

第一个关键字是数学成绩,第二个关键字是英语成绩,每个记录最终的位置由两个关键字决定。我们将它称之为复合关键字,即多关键字排序是按照复合关键字的大小排序。

多关键字排序的方法:

n 个记录的序列 {R1, R2, …, Rn} 对关键字 (Ki0, Ki1, …, Kid-1) 有序是指:对于序列中任意两个记录 Ri 和 Rj  (1≤i < j≤n) 都满足下列(词典)有序关系:(Ki0, Ki1, …, Kid-1) <  (Kj0, Kj1, …, Kjd-1) ,其中:K 0  被称为 最主位关键字, Kd-1  被称为最次位关键字。多关键字排序按照从最主位关键字到最次位关键字或从最次位关键字到最主位关键字的顺序逐次排序,分两种方法:

最高位优先法,简称 MSD 法:先按 k 0 排序分组,同一组中记录,关键字 k 0 相等,再对各组按 k 1 排序分成子组,之后,对后面的关键字继续这样的排序分组,直到按最次位关键字  k d  对各子组排序后,再将各组连接起来,便得到一个有序序列。

最低位优先法,简称 LSD 法:先从 k d-1 开始排序,再对 k d-2 进行排序,依次重复,直到对 k 0 排序后便得到一个有序序列。

例:学生记录含三个关键字:系别、班号和班内的序列号, 其中以系别为最主位关键字。

LSD的排序过程如下:

对 Ki (0≤i≤d -2)进行排序时,只能用稳定的排序方法。用 LSD 法进行的排序,在一定的条件下(即对 Ki 的不同值Ki+1 均取相同值),可通过若干次“分配”和“收集”来实现。

例:先将学生记录按英语等级由高到低分成 A、B、C、D、E 五个组:

 

然后按从左向右,从上向下的顺序将它们收集起来得到关键字序列:AA,EA,AB,BB,DB,CB,BC,CD 

再按数学成绩由高到低分成 A、B、C、D、E 五个组:

按从上向下,从左向右的顺序将其收集起来得到关键字序列:AA,AB,BB,BC,CB,CD,DB,EA 

可以看出,这个关键字序列已经是有序的了, 对每个关键字都是将整个序列按关键字分组,然后按顺序收集,显然LSD法,操作比较简单。

MSD 与 LSD 的不同特点

MSD:必须将序列逐层分割成若干子序列,然后对各子序列分别排序。

LSD:不必分成子序列,对每个关键字都是整个序列参加排序;通过若干次分配与收集实现排序。

例:对于关键字序列 (101, 203, 567, 231, 478, 352)进行基数排序

可以将每个关键字 K 看成由三个单关键字组成,即 K= k1k2k3, 每个关键字的取值范围为 0≤ki≤9,所以每个关键字可取值的数目为 10。通常将关键字取值的数目称为基数,用 r 表示,在本例中 r =10。对于关键字序列(AB, BD, ED)可以将每个关键字看成是由二个单字母关键字组成的复合关键字,并且每个关键字的取值范围为 “A~Z”,所以关键字的基数 r = 26。

基数排序可用多关键字的LSD方法排序,即对待排序的记录序列按复合关键字从低位到高位的顺序交替地进行“分组”、“收集”,最终得到有序的记录序列。在此我们将一次“分组”、“收集”称为一趟。

对于由 d 位关键字组成的复合关键字,需要经过d 趟的“分配”与“收集”。 因此,若 d 值较大,基数排序的时间效率就会随之降低。

链式的基数排序算法  

在计算机上实现基数排序时,为减少所需辅助存储空间,应采用链表作存储结构,即链式基数排序,具体作法为:

1、以静态链表存储待排记录,并令表头指针指向第一个记录; 

2、“分配” 时,按当前“关键字位”所取值,将记录分配到不同的 “链队列” 中,每个队列中记录的 “关键字位” 相同;

3、“收集”时,按当前关键字位取值从小到大将各队列首尾相链成一个链表; 

4、对每个关键字位均重复 2 和 3 两步。 

例:链式基数排序,下面以静态链表存储待排记录,并令表头指针指向第一个记录。

 

“分配” 时,按当前“关键字位”所取值,将记录分配到不同的“链队列”中,每个队列中记录的 “关键字位” 相同。 因为是 LSD,故从地位开始 ,也就是kd-1位开始,进行一趟分配:

然后xx9,xx3,xx0

又遇到了 xx9,那么按照链式队列的存储方式,先进先出的入队(类似一个桶,数据从上面进入,从下面露出)

第一趟收集:按当前关键字位取值从小到大将各队列首尾相链成一个链表;(从队列的下面出去,先进先出)

进行第二趟分配,kd-2位

进行第二题收集

进行第三趟分配,也就是 kd-3位。本例子是 k1位关键字

进行第三趟收集

序列按照多关键字从小到大的排序有序了

具体实现代码如下:

  1 //链式队列的节点结构,模拟桶
  2 struct Node
  3 {
  4     int data;//数据域
  5     Node *next;//指针域
  6 };
  7
  8 //定义程序所需的特殊队列
  9 class Queue
 10 {
 11 private:
 12     Node *front;//链式对列的头指针
 13     Node *rear;//链队的尾指针
 14
 15 public:
 16     //构造函数,初始化队列(带头结点的链式队列)
 17     Queue()
 18     {
 19         //开始先构造一个空结点,没有数据元素存储
 20         Node *p = new Node;
 21         p->data = NULL;
 22         p->next = NULL;
 23         //开始是空链队,首尾指针分别去指向队头结点
 24         front = p;
 25         rear = p;
 26     }
 27     //析构函数,销毁链队的结点占据的内存
 28     ~Queue()
 29     {
 30         //标记指针
 31         Node *p = front;
 32         //辅助的标记指针,作用是删除结点
 33         Node *q;
 34         //循环遍历整个队列,直到标记指针 p 为 null
 35         while (p != NULL)
 36         {
 37             //比较常见的删除结点内存的写法
 38             q = p;
 39             //指向队列的下一个结点
 40             p = p->next;
 41             //销毁之
 42             delete q;
 43         }
 44     }
 45     //入队方法,从尾进入,节点不存在,需要自行创建结点的方法
 46     void push(int e)
 47     {
 48         Node *p = new Node;
 49         p->data = e;
 50         //本结点作为了队列的尾结点
 51         p->next = NULL;
 52         //然后连接结点到队尾
 53         rear->next = p;
 54         //最后尾指针指向新的末位结点
 55         rear = p;
 56     }
 57     //入队方法,尾进入,节点原来就存在的方法,不需要再新建结点和存储结点的内容
 58     void push(Node *p)
 59     {
 60         //设置此结点为尾结点
 61         p->next = NULL;
 62         //链接结点
 63         rear->next = p;
 64         //尾指针指向新的尾结点
 65         rear = p;
 66     }
 67     //求数据元素的最大位数的方法,也就是求出需要分配和收集的次数
 68     int lengthData()
 69     {
 70         int length = 0;//保存数据元素的 最大位数
 71         int n = 0;   //单个数据元素具有的位数
 72         int d;      //用来存储待比较的数据元素
 73         //指示指针
 74         Node *p = front->next;
 75         //遍历
 76         while (p != NULL)
 77         {
 78             //取出结点的数据,也就是代比较的数据元素
 79             d = p->data;
 80             //如果 d 为正数,很重要的一个技巧,必须是 d 大于 0 的判断
 81             while (d > 0)
 82             {
 83                 //数据位数分离算法
 84                 d /= 10;
 85                 //单个数据元素的位数存储在此
 86                 n++;
 87             }
 88             //沿着链队后移一个元素
 89             p = p->next;
 90             //找出数据元素的最大位数
 91             if (length < n)
 92             {
 93                 length = n;
 94             }
 95             //重新循环往复,n 设置为0
 96             n = 0;
 97         }
 98         //返回最终位数
 99         return length;
100     }
101     //判断队列是否为空
102     bool empty()
103     {
104         //队头指针和队尾指针重合,说明空
105         if (front == rear)
106         {
107             return true;
108         }
109         //否则为不空
110         return false;
111     }
112     //清除队列中的元素
113     void clear()
114     {
115         //直接把头结点之后的链接断开
116         front->next = NULL;
117         //设置尾指针指向头结点即可,回到了构造函数初始化的情景
118         rear = front;
119     }
120     //输出队列中的元素,传入引用参数比较好
121     void print(Queue &que)
122     {
123         //第一个结点是头结点,next 才是第一个存储元素的结点
124         Node *p = que.front->next;
125         //直到尾结点为止
126         while (p != NULL)
127         {
128             cout << p->data << " ";
129             //遍历所有结点
130             p = p->next;
131         }
132     }
133     //基数排序过程
134     void RadixSort(Queue& que)
135     {
136         //声明一个指针数组,该指针数组中存放十个指针,这十个指针需要分别指向十个队列,这是模拟10个桶,因为是0-9的数字,取值范围为10
137         Queue *arr[10];
138         //初始化这十个队列
139         for (int i = 0; i < 10; i++)
140         {
141             //初始化建立头结点
142             arr[i] = new Queue;
143         }
144         //取得待排序数据元素中的最大位数
145         int maxLen = que.lengthData();
146         //因为是 LSD 方式,从后到前,开始比较关键字,然后分配再收集,故开始设置数据分离算法中的除数为 1
147         int d = 1;
148         //将初始队列中的元素分配到十个队列中,maxlen 代表了需要分配和收集的次数
149         for(int i = 0; i < maxLen; i++)
150         {
151             Node *p = que.front->next;
152             //辅助指针 q
153             Node *q;
154             //余数为k,则存储在arr[k]指向的链式队列(桶)中
155             int k;
156             //遍历原始序列
157             while (p != NULL)
158             {
159                 //重要的技巧,数据分离算法过程,最后勿忘模10,取余数,分离出需要的关键字位
160                 k = (p->data / d) % 10;
161                 q = p->next;
162                 //把本结点 p 加入对应的队列中
163                 arr[k]->push(p);
164                 //指针后移,指向下一个结点
165                 p = q;
166             }
167             //清空原始队列
168             que.clear();
169             //分配完毕,马上将十个队列中的数据收集到原始队列中
170             for (int i = 0; i < 10; i++)
171             {
172                 if (!arr[i]->empty())
173                 {
174                     //从首节点开始遍历,不是头结点开始
175                     Node *p = arr[i]->front->next;
176                     //辅助指针 q
177                     Node *q;
178                     while (p != NULL)
179                     {
180                         q = p->next;
181                         //收集到原始队列中,这就是为什么每次分配完毕,需要清除原始队列
182                         que.push(p);
183                         p = q;
184                     }
185                 }
186             }
187             //一趟的分配收集完毕,最后要清空十个队列
188             for (int i = 0; i < 10; i++)
189             {
190                 arr[i]->clear();
191             }
192             //进行下一趟的分配和收集
193             d *= 10;
194         }
195         //输出队列中排好序的元素
196         print(que);
197     }
198 };
199
200 int main(void)
201 {
202     Queue oldque;
203     int i;
204
205     cout << "输入 int 类型的待排序的整数序列:输入 ctrl+z 结束输入,再回车即可" << endl;
206     //顺序输入元素
207     while (cin >> i)
208     {
209         oldque.push(i);
210     }
211     //基数排序
212     oldque.RadixSort(oldque);
213
214     return 0;
215 }

输入 int 类型的待排序的整数序列:输入 ctrl+z 结束输入,再回车即可

505 800 109 930 630 662 663 269 278 287 299 200 830 184 187 528 112 125 589

109 112 125 184 187 200 269 278 287 299 505 528 589 630 662 663 800 830 930

Program ended with exit code: 0

链式基数排序的时间复杂度和空间复杂度分析

假设:n —— 记录数 , d —— 关键字数,  rd —— 关键字取值范围(如十进制为10)

分配(每趟):T(n)=O(n) ,每次分配,分配的都是所有的关键字,故是 n

收集(每趟):T(n)=O(rd) ,收集的是桶里的数据,也就是关键字的取值范围大小rd,是桶的数目

总的时间复杂度:因为一次完整的排序是分配+收集,也就是 n+rd ,而一共需要的排序趟数,恰恰就是关键字的数目 d,故T(n)=O(d(n+rd)) 

空间复杂度:S(n)=2rd 个队列指针 + n 个指针域空间,因为一个桶本质是一个链式队列,一共 rd 个桶,每个队列有队头和队尾两个指针,就是2rd 个队列指针。又原来的代拍序列是一个单链表,那么自然需要 n 个next指针控件。

排序小结

一、时间性能 

平均情况下,记住一个口诀:快些以 n log2 n 的速度归队

快=快速排序,些=希尔排序,归=归并排序,队=堆排序

这四种排序算法,时间都是 n log2 n 的,除了这四个之外,其他的排序算法平均时间都为 n^2

记住一个特殊的排序算法:基数排序的时间复杂度

d(n+rd),其中 d 是分配和收集的趟数,n 是原始序列的数目(分配次数),rd 是桶的个数,也就是关键字最大位数(收集次数)

最坏的情况下,这四个中,快速排序为 n^2,其余的和平均时间相同,还是 n log2 n

二、空间复杂度

快速排序是 log2 n

归并排序是 n

基数排序是 2rd

其余的都是1

又有口诀:直接插的好,为 n,起泡起的好为 n

三、稳定性

心情不稳定,快些选一堆好友来陪我

快=快速排序

些=希尔排序

选=简单选择排序

堆=堆排序

其余的排序算法都稳定

四、一趟排序,保证一个元素为最终位置的有两类排序算法:交换类(冒泡和快速)排序和选择类排序(简单和堆)

五、元素比较次数和原始序列无关的算法:简单选择排序,折半插入排序

六、排序趟数和原序列有关的算法:交换类,其余类无关

七、借助于比较进行排序的算法,在最坏的时候,最好的时间复杂度为 n log2 n

八、堆排序和简单选择排序的时间复杂度和初始序列无关

注意,这些东西,不必要必须死记硬背,明白每个算法的类别和实现原理,在理解的基础上,记忆,在脑海里推导演示排序的过程,帮助记忆。

 

辛苦的劳动,转载请注明出处,谢谢……

http://www.cnblogs.com/kubixuesheng/p/4374225.html

时间: 2024-10-31 18:04:09

常见的五类排序算法图解和实现(多关键字排序:基数排序以及各个排序算法的总结)的相关文章

常见的五类排序算法图解和实现(选择类:简单选择排序,锦标赛排序,树形选择排序,堆排序)

选择类的排序算法 简单选择排序算法 采用最简单的选择方式,从头到尾扫描待排序列,找一个最小的记录(递增排序),和第一个记录交换位置,再从剩下的记录中继续反复这个过程,直到全部有序. 具体过程: 首先通过 n –1 次关键字比较,从 n 个记录中找出关键字最小的记录,将它与第一个记录交换. 再通过 n –2 次比较,从剩余的 n –1 个记录中找出关键字次小的记录,将它与第二个记录交换. 重复上述操作,共进行 n –1 趟排序后,排序结束. 如图   过程图解 令 k=i:j = i + 1: 目

常见的五类排序算法图解和实现(归并类:二路归并排序)

归并类的排序算法 归并:将两个或两个以上的有序表组合成一个新的有序表. 内部排序中,通常采用的是 2-路归并排序.即:将两个位置相邻的记录有序子序列归并为一个记录有序的序列.归并排序是建立在归并操作上的一种有效的排序算法.该算法是采用分治法(Divide and Conquer)的一个非常典型的应用. 图解如下 看成是 n 个有序的子序列(长度为 1),然后两两归并. 得到 n/2 个长度为2 或 1 的有序子序列.继续亮亮归并 最后一趟 代码如下: 1 //二路一次归并过程的算法 2 //lo

常见的五类排序算法图解和实现(插入类:直接插入排序,折半插入排序,希尔排序)

基本的五类排序算法(插入,选择,交换,归并,基数排序).排序:将数据元素的一个任意序列,重新排列成一个按关键字有序的序列. 排序的稳定性:待排序列中有大于等于2个相同的项,且排序前后,相同项的相对位置是否发生了变化(如果变化了就是不稳定的排序,不变化就是稳定的) 内部排序:若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序:(待排序列全部放入内存) 插入累排序:(直接插入,折半插入,希尔排序) 直接插入排序: 先将序列中第 1 个记录看成是一个有序子序列, 然后从第 2 个记录开始

常见的五类排序算法图解和实现(交换类:冒泡排序,递归的快速排序)

冒泡排序算法: 总的来说就是两两交换,反复直到有序,第一个记录和第二个记录,若逆序则交换,然后比较第二个和第三个记录,以此类推,直到第 n 个记录和第 n-1个记录比较完毕为止,第一趟排序,结果关键字最大的记录被安排在最后一个位置.对前 n-1个记录继续冒泡排序,使得关键字次大的记录安排在第 n-1个位置.如此重复,直到没有需要交换的记录为止(仅仅是第一个和第二个交换过为止).整个一趟趟的选出最值的过程,仿佛是那水里的气泡,咕嘟咕嘟的往上翻的过程. 递增冒泡排序过程图解: 一般先比较第一个元素和

算法系列15天速成——第三天 七大经典排序【下】

今天跟大家聊聊最后三种排序: 直接插入排序,希尔排序和归并排序.   直接插入排序:        这种排序其实蛮好理解的,很现实的例子就是俺们斗地主,当我们抓到一手乱牌时,我们就要按照大小梳理扑克,30秒后,    扑克梳理完毕,4条3,5条s,哇塞......  回忆一下,俺们当时是怎么梳理的.        最左一张牌是3,第二张牌是5,第三张牌又是3,赶紧插到第一张牌后面去,第四张牌又是3,大喜,赶紧插到第二张后面去,    第五张牌又是3,狂喜,哈哈,一门炮就这样产生了.      

盘点监视器最常见的五种故障

 监视器常见故障一: 视频传输中,最常见的故障现象表现在监视器的画面上出现一条黑杠或白杠,并且或向上或向下慢慢滚动.因此,在分析这类故障现象时,要分清产生故障的两种不同原因.要分清是电源的问题还是地环路的问题,一种简易的方法是,在控制主机上,就近只接入一台电源没有问题的摄像机输出信号,如果在监视器上没有出现上述的干扰现象,则说明控制主机无问题.接下来可用一台便携式监视器就近接在前端摄像机的视频输出端,并逐个检查每台摄像机.如有,则进行处理.如无,则干扰是由地环路等其它原因造成的. 监视器常见故障

OSPF常见的五个疑难问题

我是51CTO学院讲师满国栋,在51CTO学院 "4.20 IT充电节"(4月19~20日) 到来之际,和大家分享一下OSPF常见疑难问题.正文来啦~~~ 1.OSPF特殊区域中如果存在两个ABR,那么这两个ABR都下发缺省路由,不是会形成环路吗? 不会形成环路,当特殊区域中的ABR接收到同一区域ABR发来的带有默认路由的SLA时,它只会将其放入LSDB(LSA Database)里,但不会用其计算路由,从而避免环路. 如果OSPF路由器已经发布了含缺省路由的LSA,就不会再学习其它路

市消协列出五类“霸王条款”

今年3月初,市消协对社会公开征集不公平的格式合同条款,也就是消费者常说的"霸王条款".昨天,记者从市消协获悉,截止到今年4月30日,市消协在征集不公平格式合同条款活动中,共收到消费者发来的信件.邮件279件,通过其他方式收集到各类格式合同3100余份,存在的问题主要涉及美容美发.商业零售(以商场.超市为主).机动车销售.物业管理.电信.健身.房地产中介.洗染.沐浴.洗车等行业. 昨天,市消协对本次征集中消费者集中反映的5类"霸王条款"进行了公开点评.这些"

算法系列15天速成 第三天 七大经典排序【下】_相关技巧

直接插入排序:        这种排序其实蛮好理解的,很现实的例子就是俺们斗地主,当我们抓到一手乱牌时,我们就要按照大小梳理扑克,30秒后,    扑克梳理完毕,4条3,5条s,哇塞......  回忆一下,俺们当时是怎么梳理的.        最左一张牌是3,第二张牌是5,第三张牌又是3,赶紧插到第一张牌后面去,第四张牌又是3,大喜,赶紧插到第二张后面去,    第五张牌又是3,狂喜,哈哈,一门炮就这样产生了.      怎么样,生活中处处都是算法,早已经融入我们的生活和血液.