我们按照习惯都是从左到右对比各个字符,然后进行排序,上一节叫的低位优先排序,虽然也能理解,但是需要字符串等长,在实际生活中,字符串一般都不会等长的,所以这次我们来学习一下不等长的,高位优先字符串排序。

3.1 高位优先的字符串排序

3.1.1 对字符串末尾的约定

在高位优先的字符串排序算法中,要特别注意到达字符串末尾的情况。(因为现在的字符串不是等长的了)

我们在之前就使用at的方法来把字符串索引转换成数组索引,当指定的位置超过了字符串的末尾,这个方式返回-1。

接下来只要我们在所有返回值加1,这样就可以得到一个非负的int值并用它作为count[]索引。(是不是感觉有回到上一节了)。这样的结果会产生:0表示字符串结尾,1:表示字母表中的第一个字符,2:表示字母表的第二个字符。所以我们在申请记录索引的数组也需要增大 int *count= new int[R+2]

3.1.2 代码

本来不想上代码的,奈何递归太多,还是按照代码分析分析,理解清楚思维把。

// MSD.h
class MSD
{
public:
MSD();
~MSD();

void sort(string *a, int a_len);
void sort(string *a, int lo, int hi, int d);
int charAt(string s, int d);

private:
static const int R = 256; // 基数
static const int M = 0; // 小数组的切换阈值
string *aux; // 数据分类的辅助数组

protected:

};
// MSD.cpp
/**
* @brief MSD构造函数
* @param
* @retval
*/
MSD::MSD()
{

}

/**
* @brief MSD析构函数
* @param
* @retval
*/
MSD::~MSD()
{

}

int MSD::charAt(string s, int d)
{
if(d < s.length()) return s.at(d); else return -1;
}

void MSD::sort(string *a, int a_len)
{
int N = a_len;
aux = new string[N];
sort(a, 0, N-1, 0);
}

// a 0 13 0
void MSD::sort(string *a, int lo, int hi, int d)
{
// 以第d个字符为键将a[lo] 至 a[hi]排序
if (hi <= lo + M)
{
// 插入排序是退出的
return ;
}

int *count = new int[R+2];
memset(count, 0, (R+2)*4);
for (int i=lo; i<=hi; i++) // 计算频率
{
count[charAt(a[i], d) + 2]++;
}

for (int i=0; i<257; i++)
{
if (count[i] != 0)
printf("%d %d\n", i, count[i]);
}

for (int r=0; r<R+1; r++) // 将频率转换为索引
{
count[r+1] += count[r];
}

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];
}

for (int i=0; i<14; i++)
{
printf("%d %s\n", i, a[i].c_str());
}

// 递归的以每个字符为键进行排序
for (int r=0; r<R; r++)
{
printf("sort = %d %d %d\n", lo+count[r], lo+count[r+1] - 1, d+1);
sort(a, lo+count[r], lo+count[r+1] - 1, d+1);
}

printf("\n\n\n\n");
delete[] count; //我也不知道释放完内存了没,太多
}

接下来分析一下代码:刚开始第一轮的话,是排序第0个字母

数据结构和算法 <字符串>(三、字符串排序 下)_字符串

然后开始 执行for中递归的以每个字符为键进行排序

a-s已经排好序了,所以就不用递归了,等到s的时候就需要再次排序了

数据结构和算法 <字符串>(三、字符串排序 下)_高位优先字符串排序_02

接着递归

数据结构和算法 <字符串>(三、字符串排序 下)_三向字符串排序_03

数据结构和算法 <字符串>(三、字符串排序 下)_字符串排序_04

数据结构和算法 <字符串>(三、字符串排序 下)_字符串_05

数据结构和算法 <字符串>(三、字符串排序 下)_字符串排序_06

数据结构和算法 <字符串>(三、字符串排序 下)_高位优先字符串排序_07

数据结构和算法 <字符串>(三、字符串排序 下)_高位优先字符串排序_08

数据结构和算法 <字符串>(三、字符串排序 下)_字符串_09

数据结构和算法 <字符串>(三、字符串排序 下)_字符串排序_10

数据结构和算法 <字符串>(三、字符串排序 下)_高位优先字符串排序_11

数据结构和算法 <字符串>(三、字符串排序 下)_三向字符串排序_12

算了,接下来的图就不搞了,意思就这样。不断的递归排序,感觉这个递归很多次了。

3.1.3 小型子数组

之前没有写代码分析之前,就一直不明白这个小型子数组是啥,写完代码并且分析了一遍之后,就都明白了。

我们从上面就递归过程也明白了,3和4明明字符串相同的很多,然后还要一遍一遍的递归处理,这不是折磨人么?所以我们需要在一定的范围后选择进入小数组的插入排序。为了避免重复检查已知相同的字符所带来的成本,我们就以d以后的字符串数组进行插入排序。

和快速排序以及归并排序一样,一个较小的转换阈值就能将性能提高很多。

public static void sort(String[] a, int lo, int hi, int d)
{
//从第d个字符开始对a[lo]到a[hi]排序
for(int i=lo; i<=hi; i++)
{
for(int j=i; j > lo && less(a[j], a[j-1], j--))
{
exch(a, j, j-1); // 感觉是字符串数组中,交换两个字符串
}
}
}

private static boolean less(String v, String w, int d) // 判断两个字符是否需要交换
{ return v.substring(d).compareTo(w.substring(d)) < 0; }

3.1.4 等值键

高位优先的字符串排序中的第二个陷阱是,对于含有大量等值键的子数组的排序会较慢。

排序算法并不知道这些字符是相同的,所以都需要递归调用。

3.1.5 额外空间

在写代码就发现了,我们在递归里面也申请了数组 ,并且每次递归都申请,所以这个需要额外的空间。

int *count = new int[R+2];

3.2 三向字符串快速排序

这个三向字符串快速排序就简单了,看着感觉就像是三向快速排序一样,不需要统计什么字符频率,然后转换成下标,然后进行回写,这个三向字符串排序就是需要把字符串数组中的字符串都和一个v比较,然后分为3个数组,然后3个数组又开始递归,依次循环。

public class Quick3string
{
private static int charAt(String s, int d)
{ if(d < s.length()) return s.chatAt(d); else return -1; }

public static void sort(String[] a)
{ sort(a, 0, a.length -1, 0); }

private static void sort(String[] a, int lo, int hi, int d)
{
if(hi <= lo) return ;
int lt = lo, gt = hi;
int v = charAt(a[lo], d);
int i = lo + i;
while(i <= gt) // 把所有的字符串,跟v判断,然后进行移动,形成三分天下
{
int t = charAt(a[i], d);
if (t < v) exch(a, lt++, i++);
else if(t > v) exch(a, i, gt--)
else i++;
}

// a[lo .. lt-1] < v = a[lt .. gt] < a[gt+1 .. hi]
// 下面就是三分天下之后,再次递归,然后再次分,然后。。。。。
sort(a, lo, lt-1, d);
if(v >= 0) sort(a, lt, gt, d+1);
sort(a, gt+1, hi, d);
}
}

这个就比较简单了,就不描述了,都学到了字符串,应该对快排有所了解了。