算法-22-字符串的排序算法(四种排序)

目录

1、字符串

1.1、属性

1.2、字母表

2、字符串排序方法

3、键索引计数法

3.1、第一步:频率统计

3.2、第二步:将频率转换为索引

3.3、第三步:数据分类排序

3.4、第四步:回写排序好的数组

4、低位优先的字符串排序

5、高位优先的字符串排序

5.1、优化-插入排序

6、三向字符串快速排序

7、所有排序算法性能对比


1、字符串

String 是由一系列字符组成的。字符的类型是 char,可能有 2的16次方个值。数十年以来, 程序员的注意力都局限于 7 位 ASCII 码(请见表 5.5.4)或是 8 位扩展 ASCII 码表示的字符,但许 多现代的应用程序都已经需要使用 16 位 Unicode 编码了。

1.1、属性

字符串String拥有以下属性:

  • 不可变性:String 对象是不可变的,因此可以将它们用于赋值语句、作为函数的参数或是返回值,而不用担心它们的值会发生变化。
  • 索引:我们最常完成的操作就是从某个字符串中提取一个特定的字符,即 Java 的 String 类的 charAt() 方法。
  • 子字符串:Java 的 substring() 方法实现了提取特定的子字符串的操作。
  • 字符串的连接:在 Java 中通过将一个字符串追加到另一个字符串的末尾创建一个新字符串的操 作是一个内置的操作(使用“+”运算符),所需的时间与结果字符串的长度成正比。

1.2、字母表

当我们在做一些关于字符串相关的算法时,常常会对字符串中字符的数量和内容做一些限制,只使用特定的字符来降低算法的难度,就比如BASE64 算法中限定的字符数量就是64个,ASCII字母表是128个。下图就是常用字母表的搜集,其中 R是字符的数量lgR则是表示一个索引所需要的比特数( 也就是需要用几位二进制数来表示字母的的排序索引)。

2、字符串排序方法

我们生活中,常常会需要给字符串排序的情况,就比如通讯录里边的名字排序等。接下来我们将要学习两类高效的排序方法。

第一类方法会从右到左检查键中的字符。这种方法一般被称为低位优先(Least-Signi cant-Digit First,LSD)的字符串排序。如果将一个字符串看作一个 256 进制的数字,那么从右向左检查字符串就等 价于先检查数字的最低位。这种方法最适合用于键的长度都相同的字符串排序应用。

第二类方法会从左到右检查键中的字符,首先查看的是最高位的字符。这些方法通常称为高位 优先(MSD)的字符串排序。高位优先的字符串排序的吸引人之处 在于,它们不一定需要检查所有的输入就能够完成排序。高位优先的字符串排序和快速排序类似, 因为它们都会将需要排序的数组切分为独立的部分并递归地用相同的方法处理子数组来完成排序。

3、键索引计数法

键索引计数法其实就是通过统计每个字符出现的频率,然后通过频率之和来转化成排序后的索引。

简单来说就是 比如a出现了3次,b出现了4次,c出现了5次,那么b字母开头的字符串排序后的索引,肯定是从第四个索引开始的,c开头的字符串排序后的索引肯定是从第八个索引(a+b=7)开始的。这样我们将字符串数组中的每个字符串中的字母都按照这样的方式排序的话,那么整个数组就都排序好了。

现在我们有20个学生,每个学生都有自己的分组编号(只有1、2、3、4 组),但是这些学生的名字的排序并不是按照组号的顺序排列的,所以我们需要将这20个学生的名字按照 组号的顺序排序好。

下面的代码是我们学生的信息类。

 class Student{
        String name;
        int key; //组号
        public int key() {
            return key;
        }
    }

3.1、第一步:频率统计

这里我们创建一个大小为6的int数组 count,然后遍历每个学生,然后将组号出现的频率统计到count数组中。注意,我们这里没用组号作为索引,而是组号+1的形式作为索引。这里为什么要用组号+1的索引来统计,后面你就知道了。

3.2、第二步:将频率转换为索引

看上面的数据,我们发现1组的人有3个,2组的人有5个,3组的人有6个。那么,1组人的索引从0开始,2组人的索引从3开始,3组人的索引从8开始。我们发现每个组的开始索引都是前面组的人数的和。利用这点我们可以对count数组中的统计数据进行转换。

3.3、第三步:数据分类排序

看上面的数据我们可以发现,count数组中count[1]=0 对应着1组人员开始的索引,count[2]=3 对应着2组人员开始的索引,count[3]=8 对应着3组人员开始的索引。然后我们只需要一个辅助数组aux[],将原来数组的数据按照count中对应的索引,开始标记转移到aux[]数组中,并对相同的元素进行++,就可以得到组号的排序数据了。

3.4、第四步:回写排序好的数组

在进行完上面的三步之后,aux[]数组中的数据已经是排序好的数据了,我们只需要将这些数组按照排序好的顺序复制回原来的数组,这样我们就算排序完成了。

命题 A:键索引计数法排序 N 个键为 0 到 R-1 之间的整数的元素需要访问数组 11N + 4R+1 次。

证明:根据代码可得,初始化数组会访问数组 N+R+1 次。在第一次循环中,N 个元素均会使 计数器的值加 1(访问数组 2N 次);第二次循环会进行 R 次加法(访问数组 2R 次);第三 次循环会使计数器的值增大 N 次并移动 N 次数据(访问数组 3N 次);第四次循环会移动数 据 N 次(访问数组 2N 次)。所有的移动操作都维护了等键元素的相对顺序。

public class GroupSort {

    static class Student {
        String name;
        int key; // 组号
        public Student(String name, int key) {
            this.name = name;
            this.key = key;
        }
        public int key() {
            return key;
        }
    }

    public static void sort(Student[] s) {

        int N = s.length;
        int R = 5;
        int[] count = new int[R + 1];
        Student[] aux = new Student[N];
        //第一步:频率统计
        for (int i = 0; i < N; i++) {
            count[s[i].key + 1]++;
        }
        //第二步:频率转换成索引
        for (int r = 0; r < R; r++) {
            count[r + 1] += count[r];
        }
        //第三步:数据分组排序
        for (int i = 0; i < N; i++) {
            aux[count[s[i].key]++] = s[i];
        }
        //第四步:回写排序好的数据
        for (int i = 0; i < N; i++) {
            s[i] = aux[i];
        }
    }

    public static void main(String[] args) {

        Student[] a = new Student[30];
        for (int i = 0; i < 30; i++) {
            a[i] = new Student("小子" + i, (int) (Math.random() * 4+1));
        }
        for (Student s : a) {
            System.out.println("name:" + s.name + "-key:" + s.key);
        }
        System.out.println("----------------");
        sort(a);
        for (Student s : a) {
            System.out.println("name:" + s.name + "-key:" + s.key);
        }
    }
}

4、低位优先的字符串排序

低位优先意思就是从字符串的最右边的字符开始对字符串数据进行排序。

该算法的思想就是利用了上面键索引计数法,上面将的键索引计数法是通过一个字符对数据进行排序的,我们这里的低位优先的排序方法是对于多个字符进行排序的,就比如有一个用拍照的字符串组成的一个字符串数组,如果我们想要将它们排序的话,就得将每个字符都进行排序,最后得到一个整体的排序数组。

这里你有可能会不明白一点,为啥每个字符都排序后,整个字符串就排序好了呢?

这是因为假设这有四个字符串:  ACD、BAD、CAB、ABC 

我们先讲最后一个字符排序得到的顺序是:CAB、ABC、ACD、BAD

然后我们将第二个字符排序得到的顺序是:CAB、BAD、ABC、ACD

然后我们将第三个字符排序得到的顺序是:ABC、ACD、BAD、CAB

我们可以看到刚开始ABC是在ACD的后面,然后在对倒数第一个和倒数第二个字符排序的时候ABC移动到了ACD的前面。然后他们在对第一个字符排序的时候,他们前面排序好的相对位置是不变的,这样在我们对所有字符排序完之后,整个字符串数组就已经排序好了。

对于基于 R 个字符的字母表的 N 个以长为 W 的字符串 为键的元素,低位优先的字符串排序需要访问 ~7WN + 3WR 次数组,使 用的额外空间与 N + R 成正比。

从理论上说,低位优先的字符串排序的意义重大,因为它是一种适用 于一般应用的线性时间排序算法。无论 N 有多大,它都只遍历 W 次数据。 具体描述如上。但是它是有限制的,它相对于每个字符串都是有相同的长度来说都是相同的。当然也能进行改进,但是这样就失去了该算法的优势。

public class LSD {

    public static void sort(String[] s,int W) {

        int N = s.length;
        int R = 128;  //字母和数字的字符最大的数值为127
        String[] aux = new String[N];

        for (int d = W - 1; d >= 0; d--) {
            int[] count = new int[R + 1];
            //第一步:频率统计
            for (int i = 0; i < N; i++) {
                count[s[i].charAt(d) + 1]++;
            }
            //第二步:频率转换成索引
            for (int r = 0; r < R; r++) {
                count[r + 1] += count[r];
            }
            //第三步:数据分组排序
            for (int i = 0; i < N; i++) {
                aux[count[s[i].charAt(d)]++] = s[i];
            }
            //第四步:回写排序好的数据
            for (int i = 0; i < N; i++) {
                s[i] = aux[i];
            }
        }
    }

    public static void main(String[] args) {

        String[] a = new String[20];
        for (int i = 0; i < 20; i++) {
            a[i]=getCharAndNumr(5);
        }
        for (String s : a) {
            System.out.println(" "+s);
        }
        sort(a,5);
        System.out.println("----------------");
        for (String s : a) {
            System.out.println(" "+s);
        }
    }
    /**
     * 随机生成指定长的字符串
     */
    public static String getCharAndNumr(int length) {

        Random random = new Random();
        StringBuilder valSb = new StringBuilder();
//        String charStr = "0123456789abcdefghijklmnopqrstuvwxyz";
        String charStr = "ABCDE";
        int charLength = charStr.length();

        for (int i = 0; i < length; i++) {
            int index = random.nextInt(charLength);
            valSb.append(charStr.charAt(index));
        }
        return valSb.toString();
    }
}

上面代码中的R为什么是128呢?这是因为我们使用的字母+数字组成的字符串中,每个字符的数值最大不超过127。

5、高位优先的字符串排序

上面讲的是对于字符串长度相同的数组进行排序的方法,但是大多数情况下,我们需要排序的字符串数组中每个字符串的长度是不一样的,就比如对名字的排序。这时候我们就需要一个更加通用的排序方法。

高位优先的字符串排序算法也是建立在键索引计数法的基础上完成的。但是不同的是它选择了递归的方式来不断对后面的字符进行比较,简单来说就是:

假设有:ABC、BCBD、BC、BCC、BA、CAD 六个字符串,在第一轮利用键索引计数法将该数据进行排序后,还是如下顺序,第二步递归调用本身来对相同首字母的子字符串数组进行第二轮的键索引计数法,也就是会开头字母为A、B、C的字符串分成三组,然后再分别对各个子字符串数组进行键索引计数法排序,这样不断地递归下去,知道所有的字符串都排序完成为止。

这里需要注意:因为每个字符串的长度不一样,如上述:BCBD、BC的前两个字符是相同,我们需要对第三个字符进行比较,但是第一个字符串只有两个字符,在第三轮比较中会超出自身的长度范围,这时候我们就需要一个方法来将BC排序BCBD的前面。

这时候我们需要将count[0]的位置空出来给没有后序字符的字符串,来对这样的字符串进行统计频率,然后在频率转换为索引的时候因为它们在count数组的最前面,那么它们肯定排在有后序字符的字符串前面了。

因为我们count数组本身就需要需要额外一个位置来统计最后一个字符的数量,这时候又得把0的位置空出来给没有字符的字符串,所以它初始化的时候为 new int[R+2]。

public class MSD {

    private static int R = 127; //基数,字符的最大数值
    private static String aux[]; //辅助数组

    public static void sort(String[] a) {
        int N = a.length;
        aux = new String[N];
        sort(a, 0, N - 1, 0);
    }

    private static void sort(String[] a, int lo, int hi, int d) {

        if (hi <= lo)
            return;
        int[] count = new int[R + 2];
        // 第一步:频率统计
        for (int i = lo; i <= hi; i++) {
            count[charAt(a[i], d) + 2]++;
        }
        // 第二步:频率转换成索引
        for (int r = 0; r < R + 1; r++) {
            count[r + 1] += count[r];
        }
        // 第三步:数据分组排序,前面count+2,这里count+1
        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 r = 0; r < R; r++) {
            sort(a, lo + count[r], lo + count[r + 1] - 1, d + 1);
        }
    }
    /**
     * 如果该字符串的第d个字符为null,那么我们就需要将
     */
    private static int charAt(String s, int d) {
        if (d < s.length()) {
            return s.charAt(d);
        } else {
            return -1;
        }
    }

    public static void main(String[] args) {

        String[] a = new String[20];
        for (int i = 0; i < 20; i++) {
            a[i] = getCharAndNumr((int) (6 * Math.random() + 1));
        }
        for (String s : a) {
            System.out.println(" " + s);
        }
        sort(a);
        System.out.println("----------------");
        for (String s : a) {
            System.out.println(" " + s);
        }
    }
    /**
     * 随机生成指定长的字符串
     */
    public static String getCharAndNumr(int length) {
        Random random = new Random();
        StringBuilder valSb = new StringBuilder();
        String charStr = "ABCDE";
        int charLength = charStr.length();
        for (int i = 0; i < length; i++) {
            int index = random.nextInt(charLength);
            valSb.append(charStr.charAt(index));
        }
        return valSb.toString();
    }
}

该方法还是有一些弊端的:

  1. 子字符串数组:在递归的过程我们发现我们会将每个字符进行递归,如果这个数据量庞大,但是每个子字符串数组有很多,那么我们在时间和空间上都会有很大的折扣,这时候我们可以将子数组进行插入排序来提高效率。
  2. 等值键:如果我们有大量的相同的字符,就比如名字都相同,而且还很多,这样又切回不了插入排序,还必须得把每个字符都比较一遍,这也会让这个算法在那白浪费时间来排序一样的东西。
  3. 额外空间:该算法中我们用了两个辅助数组aux[]、 count[],aux[]只会创建一遍,但是count[]因为在递归中,创建所造成的空间浪费也是很大的。

要将基于大小为 R 的字母表的 N 个字符串排序,高位优先的字符串排序算法平均需要 检查 NlogRN 个字符。

要将基于大小为 R 的字母表的 N 个字符串排序,高位优先的字符串排序算法访问数 组的次数在 8N + 3R 到 ~7wN+3wR 之间,其中 w 是字符串的平均长度。

5.1、优化-插入排序

红框内的是相对于原始代码的改进部分,其他部分没改动。

6、三向字符串快速排序

对于高位优先的字符串排序,我们也能进行改进对其进行三向切分,但是高位优先的字符串排序可能会创建大量(空)子数组,而三向字符串快速排序的切 分总是只有三个。因此三向字符串快速排序能够很好处理等值键、有较长公共前缀的键、取值范围 较小的键和小数组——所有高位优先的字符串排序算法不善长的各种情况。

特别重 要的一点是,这种切分方法能够适应键的不同部分的不同结构。和快速排序一样,三向字符串快速 排序也不需要额外的空间(递归所需的隐式栈除外),这是它相比高位优先的字符串排序的一大优点, 后者在统计频率和使用辅助数组时都需要空间。

三向字符串排 序背后的核心思想是对首字母相同的键采取特殊的策略(也就是对首字母相同的键,进行)。

算法-9-快速排序

public class Quick3string {

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

    private static void sort(String[] a, int lo, int hi, int d) {

        if (hi<=lo) return;
        int left = lo, right = hi;
        int i = lo+1;
        int v = chartAt(a[lo], d);

        while (i <= right) {

            int w = chartAt(a[i], d);
            if (w > v) {
                exch(a, i, right--);
            } else if (w < v) {
                exch(a, i++, left++);
            } else {
                i++;
            }
        }

        sort(a, lo, left - 1, d);
        if (v >= 0) {
            sort(a, left, right, d + 1);
        }
        sort(a, right + 1, hi, d);

    }

    private static void exch(String[] a, int v, int w) {
        String temp = a[v];
        a[v] = a[w];
        a[w] = temp;
    }

    private static int chartAt(String s, int d) {
        return d < s.length() ? s.charAt(d) : -1;
    }

    public static void main(String[] args) {

        String[] a = new String[20];
        for (int i = 0; i < 20; i++) {
            a[i] = getCharAndNumr((int) (6 * Math.random() + 1));
        }
        for (String s : a) {
            System.out.println(" " + s);
        }
        sort(a);
        System.out.println("----------------");
        for (String s : a) {
            System.out.println(" " + s);
        }
    }
    /**
     * 随机生成指定长的字符串
     */
    public static String getCharAndNumr(int length) {
        Random random = new Random();
        StringBuilder valSb = new StringBuilder();
        String charStr = "ABCDE";
        int charLength = charStr.length();
        for (int i = 0; i < length; i++) {
            int index = random.nextInt(charLength);
            valSb.append(charStr.charAt(index));
        }
        return valSb.toString();
    }
}

7、所有排序算法性能对比

 

 

  • 12
    点赞
  • 97
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值