2019年10月26日
桶排序
1,算法思想
- 根据场景设置桶子的个数。
- 寻访序列,并且把元素一个一个放到对应的桶子去。
- 对每个不是空的桶子进行排序。
- 从不是空的桶子里的元素再拼接到一起放回原来的序列中。
2,算法图解
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,算法图解
由图中可以看出,当从下标0开始填充时,若执行顺序从前往后的话,中的 将会插入到中,而中的将会插入到中,因此排序后,中的相同的元素位置被颠倒了,使得算法不稳定。
此外,还可以将从下标1开始填充,这时执行顺序从前往后就可以保证稳定性了
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,算法图解
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);
}
}
由上图可以看出,每排序趟数为奇数次时,完全排好的是指向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));
}
该算法图解如下:
变长字符串排序②
以下使用一种高位优先的字符串排序,即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]
。
算法图解如下:
关于she,shells,she
的排序过程涉及到字符到达字符串的末尾,其排序图解为:
变长字符串排序③
当待排序的字符串数组存在大量的相同字符串或较长的公共前缀,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));
}
}
字符串排序总结
算法 | 稳定性 | 时间复杂度 | 空间复杂度 | 适用领域 |
---|---|---|---|---|
字符串插入排序 | 稳定 | ~ | 小数组或者已经有序的数组 | |
低位优先的字符串排序(LSD ) |
稳定 | 较短的定长字符串 | ||
高位优先的字符串排序(MSD ) |
稳定 | ~ | 随机字符串 | |
三向字符从快速排序 | 不稳定 | ~ | 通用字符串排序算法,特别适用于含有较长公共前缀的字符串 |
总结
算法 | 稳定性 | 时间复杂度 | 空间复杂度 |
---|---|---|---|
桶排序 | 取决于桶内排序算法 | ~ | |
计数排序 | 稳定 | ||
基数排序 | 稳定 | ~ |