如果您发现内容有误,请在下面留言告诉我,我会改正的~~
(除非特殊说明,以下排序默认是指从小到大排序)
计数排序
计数排序是分配排序的一种,它和另两种分配排序:基数排序、桶排序都是线性时间排序算法,即时间复杂度是O(n)。
计数排序的基本思路:额外申请一个足够大的数组,遍历一遍待排序的数组,每访问一个数,就在额外数组里相应的位置上加一。遍历完成后即得到一组统计数据,统计每个数有多少个。最后再按统计结果按顺序重新输出即可。
有待排序数组A={5,6,2,2,4,1,5},那么先申请一个长度为六的数组B,然后开始遍历(先不考虑下标从零开始这一事实。。。): B[5]++,B[6]++,B[2]++,B[2]++,B[4]++,B[1]++,B[5]++。
然后现在B里面就是这样的:{1,2,0,1,2,1},而他们的下标是1,2,3,4,5,6,表示有一个1,两个2,零个3,一个4,两个5,一个6。然后就这样把它们按顺序输出到原数组里即可。
很容易看出来只要遍历有限次数的原数组和额外的数组就能完成排序,所以时间复杂度是O(n)。
计数算法是一个典型的以空间换时间的算法,通过使用额外空间来降低时间复杂度。
从上面的栗子中,大家可以看出来计数排序是有一定约束的,即待排序的数一定得是整数、大于零、数的范围不能太大,不能太分散。这是制约计数排序最大的问题,决定了它不具有快排、插入一样的普遍性和适用性。可以说这是一个只在一定范围内有效的线性时间排序算法。
下面是java实现:
public static int[] countSort(int array[]) {
if (array.length < 2)
return array;
int max = array[0], min = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] < min) {
min = array[i];
continue;
}
if (array[i] > max) {
max = array[i];
continue;
}
}
int count[] = new int[max - min + 1];
for (int i = 0; i < count.length; i++) {
count[i] = 0;
}
for (int i = 0; i < array.length; i++) {
count[array[i] - min] += 1;
}
for (int i = 1; i < count.length; i++) {
count[i] += count[i - 1];
}
int[] sorted = new int[array.length];
for (int i = 0; i < sorted.length; i++) {
//array[i]-min即count的下标
//count[array[i]-min]即排好序时的位置
sorted[count[array[i] - min] - 1] = array[i];
count[array[i] - min]--;
}
return sorted;
}
第一个循环找到数组的最大最小值,第二个循环初始化count数组,第三个循环计数,第四个循环为第五个循环做预处理,处理完后count最后一位存的是原数组长度,最后一个循环将数组按排序输出。
最后一个循环有点难理解,下面这样写可能会好理解点:
array值: 2 0 2 4 3 5 0 1 2
count下标: 0 1 2 3 4 5
count值: 2 3 6 7 8 9
排序后位置: 1 2 3 4 5 6 7 8 9
排序后的值: 0 0 1 2 2 2 3 4 5
根据计数排序的思路,我把它稍微改了改,成为找第k大数的选择算法,暂且叫它计数选择算法吧。
计数选择算法
借用计数排序的思路,最后不输出排序后的结果,利用计数的特点直接在count计数数组里找第k大的数。是线性时间选择算法。
下面是实现:
public static int countSelect(int array[], int k) {
if (array.length == 0) {
throw new IllegalArgumentException("数组长度不能为零!");
}
int max = array[0], min = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] < min) {
min = array[i];
continue;
}
if (array[i] > max) {
max = array[i];
continue;
}
}
int count[] = new int[max - min + 1];
for (int i = 0; i < count.length; i++) {
count[i] = 0;
}
for (int i = 0; i < array.length; i++) {
count[array[i] - min] += 1;
}
int tempK = k;
int result = max;
for (int i = 0; i < count.length; i++) {
tempK -= count[i];
if (tempK <= 0) {
result = i + min;
break;
}
}
return result;
}
最后一个循环也可以写成从左向右累加,等于或大于k的时候返回。
随机选择
随机选择是一种适用性比计数选择更广泛的线性时间选择算法,它的平均时间复杂度是O(n)。不过它不是一种严格意义上的线性时间选择算法,因为它的最差时间复杂度是O(n2)。
随机选择和快速排序的思路非常相似:选择一个轴值,按左小右大划分开,如果k比轴值的位置小,则在左边的子数组上递归调用随机选择;反之,如果比轴值大,则在右边的子数组上递归调用;如果等于轴值的位置,直接返回轴值。
较好的情况:每次都能把数组分成左右等长的 子数组,这样每次递归需要操作的数组长度都会减半。
最差的情况:每次都选到最大/最小值,每次递归只减少一个数。
下面是代码实现:
public static int randomSelect(int arr[], int begin, int end, int k) {
int length = end - begin;
if (length < 2)
return arr[k - 1];
Random random = new Random(System.currentTimeMillis());
// 随机生成轴元素下标
int pivot = random.nextInt(length) + begin;
// 得到轴元素新的位置(下标)
int pivotNew = partition(arr, begin, end, pivot);
if (k == pivotNew + 1)
return arr[pivotNew];
// 递归
if (k > pivotNew)
return randomSelect(arr, pivotNew + 1, end, k);
else
return randomSelect(arr, begin, pivotNew, k);
}
private static int partition(int[] arr, int begin, int end, int pivot) {
// 因为需要包含左下标位,所以从begin-1开始
int left = begin - 1;
int right = end;
while (true) {
while ((++left) < right && arr[left] <= arr[pivot])
;
while ((--right) >= left && arr[right] >= arr[pivot])
;
// 相当于左右指针交换了位置且一定是相邻的
if (left > right)
break;
swap(arr, left, right);
}
// 轴元素和哪个指针交换取决于轴元素在指针的左右
int newpivot = pivot >= left ? left : right;
swap(arr, pivot, newpivot);
return newpivot;
}
可以看到,随机选择和快排非常相似,只是快排每次对两边递归,随机每次只对一边递归。
算法导论第九章第三节给出了一个由几个数学家发明的优化算法:Selection in worst-case linear time
最差线性时间选择
这个算法是在随机选择的基础上优化了一下轴值的选择部分,避免了最差情况(即时间复杂度为O(n2)的情况)出现,此算法最差时间复杂度仍然是线性的O(n)。
此算法最重要最精妙的地方就在于轴值的选择上,通过划分组,找到中位数的中位数来确保每次迭代最少有(3/10)n - 6个数能被排除出去来保证最差也是线性时间。此算法的原理和证明就不再赘述。
下面是实现:
public static int quikCelect(int array[], int begin, int end, int k) {
// 下标相减
int length = end - begin;
if (length <= divide) {
if (length < 2)
return array[begin];
insertSort(array, begin, end);
return array[k - 1];
}
int grp = length / divide;
// 将分组排序并将中值放在数组头
for (int i = 0, left = begin; i < grp && left < end; i++) {
swap(array, begin + i, insertSort(array, left, left += divide));
}
// 对最后剩下的小于五个元素的组进行排序
// if (length % divide > 0)
// swap(array, begin + grp,
// insertSort(array, begin + grp * divide, end));
// 找出中位数的中位数并放在数组头
quikCelect(array, begin, begin + grp, begin + grp >> 1);
// pivot是中位数的下标
int pivot = partition(array, begin, end, begin);
if (k == pivot + 1)
return array[pivot];
if (k <= pivot)
return quikCelect(array, begin, pivot, k);
else
return quikCelect(array, pivot + 1, end, k);
}
private static int partition(int array[], int left, int right, int pivot) {
int pl = left;
int pr = right;
while (true) {
while ((++pl) < right && array[pl] <= array[pivot])
;
while ((--pr) > left && array[pr] >= array[pivot])
;
if (pl > pr)
break;
swap(array, pl, pr);
}
swap(array, pr, pivot);
return pr;
}
* 其中用到了插入排序算法以及和快排一样的partition函数。
这个算法的代码稍微复杂一点,不太容易理解。我看了四篇博客,没有一个代码是没问题的,搞了三天才弄明白并实现了这个算法。代码已经经过几千万次循环随机测试,结果应该是没问题的,代码过程我猜应该也是没问题的~~ (如果真的还有错请留言告诉我~ )