[数据结构拾遗]字符串排序算法总结

前言

本专题旨在快速了解常见的数据结构和算法。

在需要使用到相应算法时,能够帮助你回忆出常用的实现方案并且知晓其优缺点和适用环境。并不涉及十分具体的实现细节考究。

字符串排序算法简介

对于许多排序应用,决定顺序的键都是字符串。

其主要思想是利用比较,根据字符的有限性通过计数的方式来划分字符串的排名位置。

主要介绍以下几种方式:

  • 预备知识:键索引计数法
  • 低位优先的字符串排序 LSD string sort
  • 高位优先的字符串排序 MSD string Sort
  • 三向字符串快速排序 Three-way string quicksort

字符串排序算法要求大家先理解:基数排序和计数排序

排序算法最强总结及其代码实现

常用方法

预备知识:键索引计数法

首先我们需要了解一个预备知识:键索引计数法

键索引计数法作为三种字符串排序算法中两种的基础,本身也很适用于小整数键的简单排序。

键索引计数法主要分为四步:统计频率,将频率转换为索引,数据分类,回写。

原理图:

举例说明:

比如数组a={1,2,3,4,2,3,4,2,1,3,4,2,3,4},它里面重复的数字比较多,不重复的只有1,2,3,4,这时就可以用此方法。

(例子来源:https://www.jianshu.com/p/be5b67139396

  1. 频率统计

统计各个数字出现的次数,

1出现了2次
2出现了4次
3出现了4次
4出现了4次

需要用一个5位的数组记录(比所需数字多一位),原因留给各位看官思考。

  1. 将频率转化为索引

前面我们记录了各自数字的次数,并用数组保存

a[0]=0,
a[1]=2,
a[2]=4,
a[3]=4,
a[4]=4

这里从1开始计数,而不是从0,并不是为了与排序的数字对应,而是为了计算索引的方便,任意键的起始索引均为所有较小键的频率之和,我们就可以a[i+1]+=a[i]递推得到,这样a[0]=0,a[1]=2,a[2]=6,a[3]=10,a[4]=14,这样第一个数字(即1)的起始位置为 0,第2个数字(即 2)的起始位置为2......

多一个位置的原因:好处已经体现出来了,第一个就是用来标记最开始的起始位置的

  1. 数据分类

得到各个数字的起始索引,接下来就是将原数组进行归类,将相同的数字放在一起,这里我们用一个临时的数组进行记录

  1. 回写回原数组

最后将临时数组中的值写会原数组

代码实现:

public class countSort {
    public static void  main(String[] args){
        int[] nums={2,3,4,1,2,4,3,1,2,2,1};
        countSort sort=new countSort();
        sort.indecCountIndex(nums);
    }

    public void indecCountIndex(int[] nums){
        int[] count=new int[6];
         //计算频率
        for(int i=0;i<nums.length;i++){
            count[nums[i]+1]++;
        }
       //将频率转化为索引
        for(int i=1;i<count.length;i++){
            count[i]=count[i]+count[i-1];
        }
      //数据分类
        int[] aux=new int[nums.length];
        for(int i=0;i<nums.length;i++){
            aux[count[nums[i]]++]=nums[i];
        }
      //回写数据(我这里是打印)
        for(int i=0;i<nums.length;i++){
            System.out.print(aux[i]+" ");
        }
    }
}

低位优先的字符串排序 LSD string sort

定义:

  • 待排序的字符串长度:W

适用范围:

低位优先排序在我们的生活中经常见到,比如银行卡号的排序、车牌的排序以及电话号码的排序等

原理:

从右向左以每个字符作为关键字,用键索引计数法将字符串排序W次。由于计数排序法是稳定的,所以低位优先的字符串排序能够稳定地将字符串排序。

轨迹图:

代码实现:JAVA

摘自:https://www.cnblogs.com/sun-haiyu/p/7877651.html

算法(第四版)也有实现

import java.util.Arrays;

public class LSD {
    public static void sort(String[] a, int W) {
        // 每位数字范围0~9,基为10
        int R = 256;
        int N = a.length;
        String[] aux = new String[N];
        int[] count = new int[R+1];

        // 共需要d轮计数排序, 从最后一位开始,符合从右到左的顺序
        for (int d = W - 1; d >= 0; d--) {
            // 1. 计算频率,在需要的数组长度上额外加1
            for (int i = 0; i < N; i++) {
                // 使用加1后的索引,有重复的该位置就自增
                count[a[i].charAt(d) + 1]++;
            }
            // 2. 频率 -> 元素的开始索引
            for (int r = 0; r < R; r++) {
                count[r + 1] += count[r];
            }

            // 3. 元素按照开始索引分类,用到一个和待排数组一样大临时数组存放数据
            for (int i = 0; i < N; i++) {
                // 填充一个数据后,自增,以便相同的数据可以填到下一个空位
                aux[count[a[i].charAt(d)]++] = a[i];
            }
            // 4. 数据回写
            for (int i = 0; i < N; i++) {
                a[i] = aux[i];
            }
            // 重置count[],以便下一轮统计使用
            for (int i = 0; i < count.length; i++) {
                count[i] = 0;
            }

        }
    }

    public static void main(String[] args) {
        String[] a = {"4PGC938", "2IYE230", "3CIO720", "1ICK750", "1OHV845", "4JZY524", "1ICK750", "3CIO720",
        "1OHV845", "1OHV845","2RLA629", "2RLA629", "3ATW723"};
        LSD.sort(a, 7);
        System.out.println(Arrays.toString(a));
    }
}

上面程序将打印如下内容

[1ICK750, 1ICK750, 1OHV845, 1OHV845, 1OHV845, 2IYE230, 2RLA629, 2RLA629, 3ATW723, 3CIO720, 3CIO720, 4JZY524, 4PGC938]

高位优先的字符串排序 MSD string Sort

参考:

https://blog.csdn.net/weixin_41427400/article/details/79851043

适用范围:

MSD与LSD比较起来,拥有更强的普适性,它不需要字符串的长度相同即可对字符串数组进行排序;

在生活中的使用也比LSD更多一些,比如字典里的排序就是MSD的情况,当然还有很多,这里就不再举例了。

原理:

MSD的核心思想是分治算法,即将大问题分为小问题来解决,其思想与快速排序类似。

先对最高位的字符进行排序,将排序后的字符串进行分组——最高位相同的在一组;在对同一组的进行MSD排序,不过此时以第二位字符进行排序,直到排完最低位,算法结束。(如图3所示)

image.png

思想讲起来总是很简单,不过当中的一些细节确实我们需要注意的。一个显而易见的问题是怎么处理结尾字符的问题,因为MSD运行字符的长度不同,那么总会有字符串先结束,这是我们就需要对这些字符串进行处理。如果我们每个字符都去判断显然会很麻烦,因此我们选择一种巧妙的方式使用一个CharAt(string, int)函数来返回字符串对应下标的字符,当对应下标不存在的时候我们返回-1;


/* 转换函数:返回字符串中对于索引的字符
 * 参数:s:想要进行转换的字符串,i:字符索引
 * 返回值:对应索引的字符,若超出字符串长度返回-1
 */
char CharAt(string s, int i) {
    if (i < s.length())
        return s[i];
    else
        return -1;
    }

这样我们就可以把字符串结尾的情况同其余情况一起处理,同时保证了已结尾的字符串会在未结尾的字符串之前!

代码实现:

详见算法(第四版)第五章或者如下网址C++实现:

https://blog.csdn.net/weixin_41427400/article/details/79851043

提升性能:

在数值排序中提到过一次优化排序效率的方法:当待排序数组的长度较小时,使用插入排序。同样的,该方法也适应与高位优先字符串排序,而且这种优化一般情况下也是必须的,有专家做过实验,在数据量巨大时,将长度小于10的子数组排序切换到插入排序,可以将排序的效率提升十倍左右。

三向字符串快速排序 Three-way string quicksort

MSD对包含大量重复键的字符串进行排序时,效率十分低下。三向字符串快速排序可以很好的解决这个问题,其是MSD和快速排序的结合版。

适用范围:

非常适用于有共同前缀的字符串

预备知识:三向切分的快速排序(数字快速排序的改进算法)

参考:

https://www.jianshu.com/p/0966f989974d

要理解三向字符串快速排序,需要先理解好三向切分的快速排序。

传统快速排序中,可能出现大量重复元素,最特殊的情况:一个数组中所有元素都相同,此时无需继续排序了,但是普通的快速排序算法还是会对数组进行切分。基于此可以将数组切分成三部分,分别对应小于、等于、大于切分元素的数组元素。

我们来看这种被称为三向切分的快速排序。它从左到右遍历数组一次,维护一个指针lt使得a[low...lt-1]中的元素都小于v,一个指针gt使得a[gt + 1...high]中的元素都大于v,一个指针i使得a[lt..i-1]中的元素都等于v,a[i..gt]中的元素暂定。一开始i和low相等。随着循环,a[i...gt]越来越小,即gt-i不断减小,当i > gt时循环结束。循环中进行下面的操作:

  • 如果a[i]小于v,将a[i]和a[lt]交换,lt和i都加1;
  • 如果a[i]大于v,将a[i]和a[gt]交换,gt减1;
  • 如果a[i]等于v,将i加1

上面的这些操作保证了最后i > gt可以推出循环。

下面是三向切分快速排序的实现代码:

public class Quick3way {

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

    private static void sort(Comparable[] a, int low, int high) {
        if (high <= low) {
            return;
        }

        int lt = low;
        int gt = high;
        int i = low + 1;
        // 切分元素
        Comparable v = a[low];
        while (i <= gt) {
            int cmp = a[i].compareTo(v);
            if (cmp < 0) {
                swap(a, lt++, i++);
            } else if (cmp > 0) {
                swap(a, i, gt--);
            } else {
                i++;
            }
        }
        // 现在a[lo..lt-1] < v=a[lt..gt] < a[gt+1..high]成立
        // 切分元素相同的数组不会被递归算法访问到,对其左右的子数组递归排序
        sort(a, low, lt - 1);
        sort(a, gt + 1, high);
    }
}

对于存在大量重复元素的数组,这种方法比标准的快速排序要快。三向切分的最坏情况是所有元素各不相同,这时会比标准的快速排序要慢,因为比起标准的快速排序使用了更多的比较。

对于包含大量重复元素的数组,三向切分的快速排序算法将排序时间从线性对数级降低到线性级别,因此时间复杂度介于O(N)和O(Nlg N)之间,这依赖于输入数组中重复元素的数量。

三向字符串快速排序

我们可以利用上面学习的三向切分的数字快速排序思想,将字符串数组切分成三个子数组:

  • 一个含有所有首字母小于切分字符的子数组
  • 一个含有所有首字母等于切分字符的子数组
  • 一个含有所有首字母大于切分字符的子数组。

然后递归地对这三个数组排序,要注意对于所有首字母等于切分字符的子数组,在递归排序时应该忽略首字母(就像MSD中那样)。

递归调用轨迹:

image.png

代码实现:

在三向切分的数字快速排序的基础上稍加修改

import java.util.Arrays;

public class Quick3String {
    // 切换为插入排序的阈值
    private static int M = 15;

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

    private static void sort(String[] a, int low, int high, int d) {
        if (high <= low + M) {
            insertSort(a, low, high, d);
            return;
        }

        int lt = low;
        int gt = high;
        int i = low + 1;
        // 切分字符v是a[low]的第d个字符
        int v = charAt(a[low], d);
        while (i <= gt) {
            int t = charAt(a[i], d);
            if (t < v) {
                swap(a, lt++, i++);
            } else if (t > v) {
                swap(a, i, gt--);
            } else {
                i++;
            }
        }
        // 现在a[lo..lt-1] < v=a[lt..gt] < a[gt+1..high]成立
        // 切分元素相同的数组不会被递归算法访问到,对其左右的子数组递归排序
        sort(a, low, lt - 1, d);
        // 所有首字母与切分字符相等的子数组,递归排序,像MSD那样要忽略都相同的首字母
        if (v >= 0) {
            sort(a, lt, gt, d+ 1);
        }
        sort(a, gt + 1, high, d);
    }

    private static void swap(String[] a, int p, int q) {
        String temp = a[p];
        a[p] = a[q];
        a[q] = temp;
    }

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

    private static void insertSort(String[] a, int low, int high, int d) {
        for (int i = low + 1; i <= high; i++) {
            // 当前索引如果比它前一个元素要大,不用插入;否则需要插入
            if (less(a[i], a[i - 1], d)) {
                // 待插入的元素先保存
                String temp = a[i];
                // 元素右移
                int j;
                for (j = i; j > low && less(temp, a[j - 1], d); j--) {
                    a[j] = a[j - 1];
                }
                // 插入
                a[j] = temp;
            }
        }
    }

    private static boolean less(String v, String w, int d) {
        return v.substring(d).compareTo(w.substring(d)) < 0;
    }

    public static void main(String[] args) {
        String[] a = {"she", "sells", "seashells", "by", "the", "sea", "shore", "the",
                "shells", "she", "sells", "are", "surely", "seashells"};
        Quick3String.sort(a);
        System.out.println(Arrays.toString(a));
    }
}

三向切分的快速排序使用子数组的第一个元素作为切分点,三向切分的字符串快速排序使用子数组的第一个字符串的第d个字符作为切分字符。

在递归对子数组排序时,相比三向切分的快速排序,三向切分的字符串快速排序多了这么一个判断,这句的意思是只要还没到字符串的末尾(v = -1说明到达,其余均未到达),所有首字母与切分字符相等的子数组也需要递归排序,不过要像MSD那样,忽略掉相同的首字母,处理下一个字符。

总结

字符串排序算法选择:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,252评论 6 516
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,886评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,814评论 0 361
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,869评论 1 299
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,888评论 6 398
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,475评论 1 312
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,010评论 3 422
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,924评论 0 277
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,469评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,552评论 3 342
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,680评论 1 353
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,362评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,037评论 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,519评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,621评论 1 274
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,099评论 3 378
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,691评论 2 361

推荐阅读更多精彩内容

  • 数据结构与算法——字符串排序 对于许多排序应用,决定顺序的键都是字符串。下面将学习专门针对字符串类型的排序方法,这...
    sunhaiyu阅读 3,020评论 1 4
  • 数据结构与算法——快速排序 快速排序,顾名思义,它速度很快,针对一般应用中各种不同的输入都要比其他排序算法快很多,...
    sunhaiyu阅读 3,287评论 0 3
  • 1 初级排序算法 排序算法关注的主要是重新排列数组元素,其中每个元素都有一个主键。排序算法是将所有元素主键按某种方...
    深度沉迷学习阅读 1,415评论 0 1
  • 一、字符串排序算法比较 本文介绍的排序算法与传统的基于比较的通用排序算法不同,本文主要介绍LSD string s...
    null12阅读 6,035评论 0 0
  • 期盼已久的元旦假期如期而至,早已买好了车票,迫不及待的等着出发。 两个小时的公交车,五个小时的火车也就注定了今天将...
    田田拾光阅读 1,503评论 0 3