排序(下_常数阶)

2019年10月26日

桶排序

1,算法思想

  1. 根据场景设置桶子的个数。
  2. 寻访序列,并且把元素一个一个放到对应的桶子去。
  3. 对每个不是空的桶子进行排序。
  4. 从不是空的桶子里的元素再拼接到一起放回原来的序列中。

2,算法图解

1

3,算法实现

public class Bucket {
    public static void bucketSort(int[] arr, int step) {
        int max = arr[0];
        int min = arr[0];
        for (int a : arr) {
            if (max < a) {
                max = a;
            }
            if (min > a) {
                min = a;
            }
        }
        int bucketNum = max / step - min / step + 1;
        List buckList = new ArrayList>();
        for (int i = 0; i < bucketNum; i++) {
            buckList.add(new ArrayList());
        }
        for (int value : arr) {
            int index = indexFor(value, min, step);
            ((ArrayList) buckList.get(index)).add(value);
        }
        ArrayList bucket = null;
        int index = 0;
        for (int i = 0; i < bucketNum; i++) {
            bucket = (ArrayList) buckList.get(i);
            Collections.sort(bucket);
            for (int k : bucket) {
                arr[index++] = k;
            }
        }
    }

    private static int indexFor(int a, int min, int step) {
        return (a - min) / step;
    }

    public static void main(String[] args) {
        Random randomInt = new Random();
        int[] a = new int[20];
        for (int i = 0; i < a.length; i++) {
            a[i] = randomInt.nextInt(100);
        }
        System.out.println(Arrays.toString(a));
        bucketSort(a, 10);
        System.out.println(Arrays.toString(a));
    }
}

4,复杂度分析

假设待排序的数据有个,桶的个数为个,那么每个桶平均有个数据,每个桶内部使用对数阶排序算法如快排。每个桶内部的时间复杂度为,那么个桶排序的时间复杂度就为:,又因为,该时间复杂度又为:,当,这时桶排序的时间复杂度就接近。因此:

  • 最好时间复杂度:
  • 最坏时间复杂度:
  • 空间复杂度:

其稳定性取决于桶内排序算法。

计数排序

1,算法思想

  1. 找出待排序的数组中最大和最小的元素
  2. 统计数组中每个值为的元素出现的次数,存入数组的第项
  3. 对所有的计数累加(从中的第一个元素开始,每一项和前一项相加)
  4. 反向填充目标数组:将每个元素放在新数组的第项,每放一个元素就将减去1

2,算法图解

2

由图中可以看出,当从下标0开始填充时,若执行顺序从前往后的话,中的 将会插入到中,而中的将会插入到中,因此排序后,中的相同的元素位置被颠倒了,使得算法不稳定。

此外,还可以将从下标1开始填充,这时执行顺序从前往后就可以保证稳定性了

3

3,算法实现

反向填充目标数组的实现

public class Count {
    public static void countSort(int[] arr) {
        int[] temp = new int[arr.length];
        int max = arr[0], min = arr[0];
        for (int i : arr) {
            if (i > max) {
                max = i;
            }
            if (i < min) {
                min = i;
            }
        }
        int k = max - min + 1;
        int[] count = new int[k];
        for (int value : arr) {
            count[value - min]++;
        }
        for (int i = 1; i < count.length; ++i) {
            count[i] = count[i] + count[i - 1];
        }

        for (int i = arr.length - 1; i >= 0; --i) {
            int index = count[arr[i] - min] - 1;
            temp[index] = arr[i];
            count[arr[i] - min]--;
        }
        System.arraycopy(temp, 0, arr, 0, arr.length);
    }

    public static void main(String[] args) {
        Random randomInt = new Random();
        int[] a = new int[20];
        for (int i = 0; i < a.length; i++) {
            a[i] = randomInt.nextInt(100);
        }
        System.out.println(Arrays.toString(a));
        countSort(a);
        System.out.println(Arrays.toString(a));
    }
}

正向填充目标数组的实现

public void countSort2(int[] arr) {
        int[] temp = new int[arr.length];
        int max = arr[0], min = arr[0];
        for (int i : arr) {
            if (i > max) {
                max = i;
            }
            if (i < min) {
                min = i;
            }
        }
        int k = max - min + 1;
        int[] count = new int[k + 1];
        for (int value : arr) {
            count[value - min + 1]++;
        }
        for (int i = 0; i < count.length - 1; ++i) {
            count[i + 1] += count[i];
        }

        for (int value : arr) {
            temp[count[value - min]++] = value;
        }
        System.arraycopy(temp, 0, arr, 0, arr.length);
    }

4,复杂度分析

从代码中可以看出,其最好,最坏,平均时间复杂度都为:,空间复杂度为:。

而且该算法是稳定的。

基数排序

1,算法思想

基数排序(英语:Radix sort)是一种非比较型==整数==排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

它是这样实现的:将所有待比较数值(正整数)统一为同样的数字长度,数字较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。

2,算法图解

4

3,算法实现

public class Radix {
    public static void radixSort(String[] arr, int stringLen) {
        final int len = 256;

        ArrayList[] buckets = new ArrayList[len];

        for (int i = 0; i < len; i++) {
            buckets[i] = new ArrayList<>();
        }

        for (int pos = stringLen - 1; pos >= 0; pos--) {
            for (String s : arr) {
                buckets[s.charAt(pos)].add(s);
            }

            int idx = 0;
            for (ArrayList thisBucket : buckets) {
                for (String s : thisBucket) {
                    arr[idx++] = s;
                }
                /**
                 * 每排完一次序,就将已排好的数据从buckets中清空,
                 * 否则外层再次循环时,第13行会将数据重复存入buckets中,
                 * 这样到最后buckets中会有pos*arr.length个数据,即所有元素都
                 * 重复存入了pos个,会造成arr的ArrayIndexOutOfBoundsException
                 */
                thisBucket.clear();
            }
        }
    }

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

4,复杂度分析

若排序的数据长度为,则需要进行次排序,而内部排序的时间复杂度为,因此总的时间复杂度为,当不大时,时间复杂度近似于;空间复杂度为;是稳定的排序算法。

5,应用

对定长字符串排序

利用基数+计数排序可以对字符串进行排序(LSD):

反向填充:

public static void radixCountStrSort(String[] arr, int strLength) {
        final int bucket = 256;
        String[] temp = new String[arr.length];
        for (int d = strLength - 1; d >= 0; d--) {
            int[] count = new int[bucket];
            //count下标对应的字母中填的值为该字母的最大位次
            for (String s : arr) {
                count[s.charAt(d)]++;
            }
            for (int r = 1; r < bucket; r++) {
                count[r] += count[r - 1];
            }
            for (int i = arr.length - 1; i >= 0; i--) {
                temp[count[arr[i].charAt(d)] - 1] = arr[i];
                count[arr[i].charAt(d)]--;
            }
            System.arraycopy(temp, 0, arr, 0, arr.length);
        }
    }

也可以正向填充,同时为了避免数组间的频繁复制,可以进一步优化为:

public static void radixCountStrSort2(String[] arr, int strLength) {
        final int bucket = 256;
        String[] buffer = new String[arr.length];
        String[] in = arr;
        String[] out = buffer;
        for (int d = strLength - 1; d >= 0; d--) {
            int[] count = new int[bucket + 1];
            //count下标对应的字母中填的值为该字母的起始位次
            for (String s : in) {
                count[s.charAt(d) + 1]++;
            }
            for (int r = 0; r < count.length - 1; r++) {
                count[r + 1] += count[r];
            }
            for (String s : in) {
                out[count[s.charAt(d)]++] = s;
            }
            String[] temp = in;
            in = out;
            out = temp;
        }
        //将in中的数据复制到out中
        if (strLength % 2 == 1) {
            System.arraycopy(in, 0, out, 0, arr.length);
        }
    }
5

由上图可以看出,每排序趟数为奇数次时,完全排好的是指向buffer内存块的数组,因此要将buffer内存块中的数据复制到arr内存块,以保证arr内存块中的数据是排好序的。

变长字符串排序①

    public static void changeStringSort(String[] arr, int maxLen) {
        final int bucket = 256;

        ArrayList[] wordsByLength = new ArrayList[maxLen + 1];
        ArrayList[] buckets = new ArrayList[bucket];

        for (int i = 0; i < wordsByLength.length; i++) {
            wordsByLength[i] = new ArrayList<>();
        }

        for (int i = 0; i < bucket; i++) {
            buckets[i] = new ArrayList<>();
        }

        for (String s : arr) {
            wordsByLength[s.length()].add(s);
        }

        int index = 0;
        for (ArrayList wordList : wordsByLength) {
            for (String s : wordList) {
                arr[index++] = s;
            }
        }

        int startingIndex = arr.length;
        for (int pos = maxLen - 1; pos >= 0; pos--) {
            startingIndex -= wordsByLength[pos + 1].size();

            for (int i = startingIndex; i < arr.length; i++) {
                buckets[arr[i].charAt(pos)].add(arr[i]);
            }

            index = startingIndex;
            for (ArrayList thisBucket : buckets) {
                for (String s : thisBucket) {
                    arr[index++] = s;
                }

                thisBucket.clear();
            }
        }
    }

    public static void main(String[] args) {
        String[] arr = {"1PGCI", "3IY", "3CIO", "4O", "1I", "4JZYE", "2NL", "2ATW"};
        System.out.println(Arrays.toString(arr));
        changeStringSort(arr, 5);
        System.out.println(Arrays.toString(arr));
    }

该算法图解如下:

6

变长字符串排序②

以下使用一种高位优先的字符串排序,即MSD方式,从左向右遍历所有字符。

public class StringSortMsd {
    private static final int BUCKET = 256;
    private static final int CUTOFF = 15;
    private static String[] temp;

    public static void sort(String[] arr) {
        int n = arr.length;
        temp = new String[n];
        sort(arr, 0, n - 1, 0);
    }

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

    private static void sort(String[] arr, int lo, int hi, int d) {
        if (hi <= lo + CUTOFF) {
            insertion(arr, lo, hi, d);
            return;
        }

        int[] count = new int[BUCKET + 2];
        for (int i = lo; i <= hi; i++) {
            int c = charAt(arr[i], d);
            count[c + 2]++;
        }

        for (int r = 0; r < BUCKET + 1; r++) {
            count[r + 1] += count[r];
        }

        for (int i = lo; i <= hi; i++) {
            int c = charAt(arr[i], d);
            temp[count[c + 1]++] = arr[i];
        }

        for (int i = lo; i <= hi; i++) {
            arr[i] = temp[i - lo];
        }

        for (int r = 0; r < BUCKET; r++) {
            sort(arr, lo + count[r], lo + count[r + 1] - 1, d + 1);
        }
    }

    private static void insertion(String[] arr, int lo, int hi, int d) {
        for (int i = lo; i <= hi; i++) {
            for (int j = i; j > lo && less(arr[j], arr[j - 1], d); j--) {
                exchange(arr, j, j - 1);
            }
        }
    }

    private static void exchange(String[] arr, int i, int j) {
        String temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    private static boolean less(String v, String w, int d) {
        for (int i = d; i < Math.min(v.length(), w.length()); i++) {
            if (v.charAt(i) < w.charAt(i)) {
                return true;
            }
            if (v.charAt(i) > w.charAt(i)) {
                return false;
            }
        }
        return v.length() < w.length();
    }

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

上面的代码其实使用了两种排序算法,当待排序数组的长度在CUTOFF以内,则采用的是插入排序方式,否则,采用的是MSD方式。这时因为MSD方式要对大量的长度为256的数组进行处理,所以当数组长度较小时,使用插入排序来提高性能。

由于MSD是逐步递归处理首字符相同的子数组,因此当字符串中的字符超过其长度的边界时,需要设定一个标志,让其不在进入下一轮的递归排序;本算法中,将该标志设为0,将count[0]作为保留位,而字符又有256种情况,因此,count需要最多需要存入BUCKET+1个数,所以count = new int[BUCKET + 2]

算法图解如下:

7
8

关于she,shells,she的排序过程涉及到字符到达字符串的末尾,其排序图解为:

9

变长字符串排序③

当待排序的字符串数组存在大量的相同字符串或较长的公共前缀,MSD字符串排序会检查其所有的字符,会创建大量的子数组,因此MSD适用于随机字符串排序;而三向字符串快速排序可以很好的解决该问题:

public class StringSortQuick {
    private static final int CUTOFF = 15;

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

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

    private static void sort(String[] arr, int lo, int hi, int d) {
        if (hi <= lo + CUTOFF) {
            insertion(arr, lo, hi, d);
            return;
        }

        int lt = lo, gt = hi;
        int v = charAt(arr[lo], d);
        int i = lo + 1;
        while (i <= gt) {
            int t = charAt(arr[i], d);
            if (t < v) {
                exchange(arr, lt++, i++);
            } else if (t > v) {
                exchange(arr, i, gt--);
            } else {
                i++;
            }
        }

        sort(arr, lo, lt - 1, d);
        if (v >= 0) {
            sort(arr, lt, gt, d + 1);
        }
        sort(arr, gt + 1, hi, d);
    }

    private static void insertion(String[] a, int lo, int hi, int d) {
        for (int i = lo; i <= hi; i++) {
            for (int j = i; j > lo && less(a[j], a[j - 1], d); j--) {
                exchange(a, j, j - 1);
            }
        }
    }

    private static void exchange(String[] a, int i, int j) {
        String temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

    private static boolean less(String v, String w, int d) {
        for (int i = d; i < Math.min(v.length(), w.length()); i++) {
            if (v.charAt(i) < w.charAt(i)) {
                return true;
            }
            if (v.charAt(i) > w.charAt(i)) {
                return false;
            }
        }
        return v.length() < w.length();
    }

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

10
11

字符串排序总结

算法 稳定性 时间复杂度 空间复杂度 适用领域
字符串插入排序 稳定 ~ 小数组或者已经有序的数组
低位优先的字符串排序(LSD) 稳定 较短的定长字符串
高位优先的字符串排序(MSD) 稳定 ~ 随机字符串
三向字符从快速排序 不稳定 ~ 通用字符串排序算法,特别适用于含有较长公共前缀的字符串

总结

算法 稳定性 时间复杂度 空间复杂度
桶排序 取决于桶内排序算法 ~
计数排序 稳定
基数排序 稳定 ~

你可能感兴趣的:(排序(下_常数阶))