排序(下_常数阶)

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<List<Integer>>();
        for (int i = 0; i < bucketNum; i++) {
            buckList.add(new ArrayList<Integer>());
        }
        for (int value : arr) {
            int index = indexFor(value, min, step);
            ((ArrayList<Integer>) buckList.get(index)).add(value);
        }
        ArrayList<Integer> bucket = null;
        int index = 0;
        for (int i = 0; i < bucketNum; i++) {
            bucket = (ArrayList<Integer>) 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,复杂度分析

假设待排序的数据有N个,桶的个数为M个,那么每个桶平均有K=N/M个数据,每个桶内部使用对数阶排序算法如快排。每个桶内部的时间复杂度为O(KlogK),那么M个桶排序的时间复杂度就为:O(M*KlogK),又因为K=N/M,该时间复杂度又为:O(Nlog\frac{N}{M}),当M \rightarrow N,这时桶排序的时间复杂度就接近O(N)。因此:

  • 最好时间复杂度:O(N)
  • 最坏时间复杂度:O(NlogN)
  • 空间复杂度:O(N)

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

计数排序

1,算法思想

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

2,算法图解

2

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

此外,还可以将count从下标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,复杂度分析

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

而且该算法是稳定的。

基数排序

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<String>[] 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<String> 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,复杂度分析

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

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<String>[] wordsByLength = new ArrayList[maxLen + 1];
        ArrayList<String>[] 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<String> 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<String> 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

字符串排序总结

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

总结

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

推荐阅读更多精彩内容