前言:
最近正好在学习字符串相关的算法,写了下LSD、MSD以及三向快速排序算法,写完了过后有很大的感触,所以决定把这几天所学习的东西记录下来。
原理:
键索引计数法:
在开始介绍LSD以及MSD算法之前,我们要介绍一个算法——键索引计数法,这是LSD以及MSD的基础。
键索引计数法允许我们通过一定的键值来对字符串进行排序,一个很好的例子就是
分组——每个学生对应一个小组,将他们按照小组分开。在这个问题中,学生姓名就是字符串,而小组的组号就是对应的键值,我们需要将相同小组的同学分到一起(如图一)
为了完成上述的功能,我们先统计每个键值的个数并计入一个Count数组中,
Count[n + 1]中统计键值为n的字符串的个数,在上图中Count[2] = 2,Count[3] = 1,Count[4] = 2;在统计完成后,我们需要将频数转换为索引,这是至关重要的一步,在这里你也会发现为什么我们用Count[n + 1]来统计键值n而不是Count[n]。在这一步中我们
使Count[n + 1] += Count[n],这样你会发现Count[0] = 0,Count[1] = 0,Count[2] = 2,Count[3] = 3;可以发现这样变换了以后Count[n]正是下一个键值为n的字符串的索引下标!最后我们只需要根据索引将字符串存入我们预先准备的字符串数组及可以完成分组,具体的代码我们可以在后面的LSD以及MSD中看到!
低位优先排序(LSD):
低位优先排序在我们的生活中经常见到,比如银行卡号的排序、车牌的排序以及电话号码的排序等,我们可以发现一个规律,那就是这些东西的长度都是相等的!这并不是巧合,而是LSD算法的性质决定的,LSD适用于较短的定长字符。
LSD算法要求用于排序的字符串数组中的每一个字符串的长度相同,然后从最低位(即最右边的字符)开始,用键索引计数法对字符串排序,一直到用最高位进行键索引排序,则退出算法完成排序(如图2所示)
从图中我们可以发现LSD算法正如其名字一样,从低位开始保证其有序(红框内部分);当然,只要熟练掌握键索引计数法,LSD的编写将会很简单;具体代码如下:
/* 低位优先排序:将字符串进行低位优先排序(试用情况: 字符串的长度相同)
* 参数:a:参与排序的字符串数组,N:字符串中元素的个数
* 返回值:无
*/
void LSD(string *a, int N) {
int W = a[0].length(), R = 256;
string *Aux = new string[N];
int *Count = new int[R + 1];
for (int k = W - 1; k >= 0; k--) {
// 初始化索引数组
for (int i = 0; i < R + 1; i++)
Count[i] = 0;
// 计算位置k上字符频数
for (int i = 0; i < N; i++)
Count[a[i][k] + 1]++;
// 将频数转换为索引
for (int i = 0; i < R; i++)
Count[i + 1] += Count[i];
// 将元素分类
for (int i = 0; i < N; i++)
Aux[Count[a[i][k]]++] = a[i];
// 回写
for (int i = 0; i < N; i++)
a[i] = Aux[i];
}
Aux = NULL;
delete Aux;
Aux = NULL;
delete Count;
Count = NULL;
}
PS:在上述代码中,我认为最重要的是
Aux[Count[a[i][k]]++] = a[i]这一步,即将元素进行分类,因为后面是通过a[i]赋值,所以保持原数组的相对有序性,即
在同一更高位下,低位是有序的,这样才能完成LSD算法!
高位优先排序(MSD):
MSD与LSD比较起来,拥有更强的普适性,它不需要字符串的长度相同即可对字符串数组进行排序;在生活中的使用也比
LSD更多一些,比如字典里的排序就是MSD的情况,当然还有很多,这里就不再举例了。
MSD的核心思想是分治算法,即将大问题分为小问题来解决,其思想与快速排序类似,具体做法是:先对最高位的字符进行排序,将排序后的字符串进行分组——最高位相同的在一组;在对同一组的进行MSD排序,不过此时以第二位字符进行排序,直到排完最低位,算法结束。(如图3所示)
思想讲起来总是很简单,不过当中的一些细节确实我们需要注意的。
一个显而易见的问题是怎么处理结尾字符的问题,因为MSD运行字符的长度不同,那么总会有字符串先结束,这是我们就需要对这些字符串进行处理。如果我们每个字符都去判断显然会很麻烦,因此我们选择一种巧妙的方式使用一个CharAt(string, int)函数来返回字符串对应下标的字符,当对应下标不存在的时候我们返回-1;
/* 转换函数:返回字符串中对于索引的字符
* 参数:s:想要进行转换的字符串,i:字符索引
* 返回值:对应索引的字符,若超出字符串长度返回-1
*/
char CharAt(string s, int i) {
if (i < s.length())
return s[i];
else
return -1;
}
这样我们就可以把字符串结尾的情况同其余情况一起处理,同时保证了已结尾的字符串会在未结尾的字符串之前!
完成了这些我们就可以开始编写MSD算法了,具体代码如下:
/* 高位优先排序驱动
* 参数:a:进行排序的数组,Aux:用于储存本次排序结果的中间数组,Count:用于储存索引的数组,lo:排序的起始位置,hi:排序终止位置,d:进行排序的字符下标
* 返回值:无
*/
void MSD(string *a, string *Aux, int *Count,int lo, int hi, int d) {
int const M = 3, R = 256;
// 小的子数组进行插入排序
// 与快速排序相同
if (hi <= lo + M) {
InsertionSort(a, lo, hi, d);
return;
}
// 初始化索引数组
for (int i = 0; i < R + 2; i++)
Count[i] = 0;
// 统计频数
// 特别说明:Count[0]:无用;Count[1]:长度为d的字符串个数;
for (int i = lo; i <= hi; i++)
Count[CharAt(a[i], d) + 2]++;
// 转换频数为索引
for (int i = 0; i < R + 1; i++)
Count[i + 1] += Count[i];
// 数组分类
for (int i = lo; i <= hi; i++)
Aux[Count[CharAt(a[i], d) + 1]++] = a[i];
// 回写
for (int i = lo; i <= hi; i++)
a[i] = Aux[i - lo];
// 递归排序
// 特别说明:Count[0]:长度为d的字符串索引的终止坐标
for (int i = 0; i < R; i++)
MSD(a, Aux, Count, lo + Count[i], lo + Count[i + 1] - 1, d + 1);
}
/* 高位优先排序:将目标字符串进行高位优先排序
* 参数:a:想要进行高位优先排序的数组,N:数组中元素的个数
* 返回值:无
*/
void MSD(string *a, int N) {
int R = 256;
int *Count = new int[R + 2];
string *Aux = new string[N];
MSD(a, Aux, Count, 0, N - 1, 0);
Aux = NULL;
delete Aux;
Aux = NULL;
delete Count;
Count = NULL;
}
PS:MSD算法我们分为了两部分来写,是因为我们要多次使用Count以及Aux数组,这样可以
避免多次申请内存空间,节省效率;同时这用我们可以便于递归操作的时候又可以
将底层与接口分开,更利于抽象!
PPS:可以发现,当子字符串少的时候,我们直接调用插入排序来对字符串数组进行排序,这和快速排序是很类似的,因为
MSD以及快速排序在字符串少的时候并没有什么优势,我们可以直接使用现成的排序,如插入排序来进行排序。
C++实现:
接下来是整个LSD以及MSD实现的代码,同时包含了插入排序的代码:
#ifndef STRINGSORT_H
#define STRINGSORT_H
#include
#include
using namespace std;
/* 交换函数:交换两个字符串
* 参数:s1:进行交换的字符串1,s2:进行交换的字符串2
* 返回值:无
*/
void exch(string &s1, string &s2) {
string tmp = s1;
s1 = s2;
s2 = tmp;
}
/* 小于函数:判断前d位相等的字符串的大小
* 参数:s1:进行比较的字符串1,s2:进行比较的字符串2,d:不同字符开始出现的索引
* 返回值:若s1 lo && Less(a[j], a[j - 1], d); j--)
exch(a[j], a[j - 1]);
}
/* 低位优先排序:将字符串进行低位优先排序(试用情况: 字符串的长度相同)
* 参数:a:参与排序的字符串数组,N:字符串中元素的个数
* 返回值:无
*/
void LSD(string *a, int N) {
int W = a[0].length(), R = 256;
string *Aux = new string[N];
int *Count = new int[R + 1];
for (int k = W - 1; k >= 0; k--) {
// 初始化索引数组
for (int i = 0; i < R + 1; i++)
Count[i] = 0;
// 计算位置k上字符频数
for (int i = 0; i < N; i++)
Count[a[i][k] + 1]++;
// 将频数转换为索引
for (int i = 0; i < R; i++)
Count[i + 1] += Count[i];
// 将元素分类
for (int i = 0; i < N; i++)
Aux[Count[a[i][k]]++] = a[i];
// 回写
for (int i = 0; i < N; i++)
a[i] = Aux[i];
}
Aux = NULL;
delete Aux;
Aux = NULL;
delete Count;
Count = NULL;
}
/* 高位优先排序驱动
* 参数:a:进行排序的数组,Aux:用于储存本次排序结果的中间数组,Count:用于储存索引的数组,lo:排序的起始位置,hi:排序终止位置,d:进行排序的字符下标
* 返回值:无
*/
void MSD(string *a, string *Aux, int *Count,int lo, int hi, int d) {
int const M = 3, R = 256;
// 小的子数组进行插入排序
// 与快速排序相同
if (hi <= lo + M) {
InsertionSort(a, lo, hi, d);
return;
}
// 初始化索引数组
for (int i = 0; i < R + 2; i++)
Count[i] = 0;
// 统计频数
// 特别说明:Count[0]:无用;Count[1]:长度为d的字符串个数;
for (int i = lo; i <= hi; i++)
Count[CharAt(a[i], d) + 2]++;
// 转换频数为索引
for (int i = 0; i < R + 1; i++)
Count[i + 1] += Count[i];
// 数组分类
for (int i = lo; i <= hi; i++)
Aux[Count[CharAt(a[i], d) + 1]++] = a[i];
// 回写
for (int i = lo; i <= hi; i++)
a[i] = Aux[i - lo];
// 递归排序
// 特别说明:Count[0]:长度为d的字符串索引的终止坐标
for (int i = 0; i < R; i++)
MSD(a, Aux, Count, lo + Count[i], lo + Count[i + 1] - 1, d + 1);
}
/* 高位优先排序:将目标字符串进行高位优先排序
* 参数:a:想要进行高位优先排序的数组,N:数组中元素的个数
* 返回值:无
*/
void MSD(string *a, int N) {
int R = 256;
int *Count = new int[R + 2];
string *Aux = new string[N];
MSD(a, Aux, Count, 0, N - 1, 0);
Aux = NULL;
delete Aux;
Aux = NULL;
delete Count;
Count = NULL;
}
/* 三向快速排序驱动
* 参数:a:进行排序的数组,lo:排序的起始位置,hi:排序的终止位置,d:进行排序的字符下标
* 返回值:无
*/
void Quick3Sort(string *a, int lo, int hi, int d) {
// 排序终止条件
if (hi <= lo)
return;
// lt:小子串的结束位置,gt:大子串的开始位置
// v:枢纽元
int lt = lo, gt = hi;
int v = CharAt(a[lo], d);
int i = lo + 1;
// 以枢纽元为界进行区分
while (i <= gt) {
int t = CharAt(a[i], d);
if (t < v)
exch(a[lt++], a[i++]);
else if (t > v)
exch(a[i], a[gt--]);
else
i++;
}
// 对小子串进行排序
Quick3Sort(a, lo, lt - 1, d);
// 对当前字符串进行进一步排序
if (v >= 0)
Quick3Sort(a, lt, gt, d + 1);
// 对大子串进行排序
Quick3Sort(a, gt + 1, hi, d);
}
/* 三向快速排序:对目标数组进行三向快速排序
* 参数:a:进行排序的字符串数组,N:字符串的个数
* 返回值:无
*/
void Quick3Sort(string *a, int N) {
Quick3Sort(a, 0, N - 1, 0);
}
#endif // !STRINGSORT_H
那么LSD以及MSD排序就讲完了,下次有空的时候我再来补上一个三向快速排序,这也是一个很好的字符串排序算法!
转载请注明出处哦~~
参考文献:《算法 —— 第四版》