遵从百度百科解释,算法稳定性定义如下:
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
之前面试前没了解过,胡乱一说算法稳定性就是保证算法计算一次和计算多次结果都是一样的# #,现在想来这种东西不会可不要瞎说。
后来私下查找了一些资料,开始学习了算法稳定性的知识,可参考之前的文章:《算法的稳定性》。
紧跟着,又一波面试袭来,其中有一个面试官就问到了算法稳定性的问题。
在我胸有成竹地回答出算法稳定性的概念之后,紧接着又问哪些算法是稳定的哪些不稳定?
还好这些也都复习过,自然难不倒我
堆排序、快速排序、希尔排序、直接选择排序是不稳定的排序算法,而基数排序、冒泡排序、直接插入排序、折半插入排序、归并排序是稳定的排序算法。
甩给他一连串的概念之后,权当这类问题已经OK了,可惜我高兴的太早
面试的最后一题是现场编写基数排序的代码,还好也有准备,勉强写完,调试几次确认输出结果正确,以为万事大吉。
万万没想到,这时候面试官点题一样的问了一句,那基数排序这段代码里,哪行代码能保证它是稳定排序?
就这样,虽然是视频面试,我就一边调试,一边凌乱,碎碎念都掩盖不住现场的尴尬。以下是我的代码:
private static void radixSort(int[] data, int radix, int d) {
int[] tem = new int[data.length];
int[] buckets = new int[radix];
int rate = 1;
for (int i = 0; i < d; i++) {
Arrays.fill(buckets, 0);
System.arraycopy(data, 0, tem, 0, data.length);
for (int j = 0; j < data.length; j++) {
int subkey = (tem[j] / rate) % radix;
buckets[subkey]++;
}
for (int j = 1; j < radix; j++) {
buckets[j] = buckets[j] + buckets[j - 1];
}
for (int m = data.length - 1; m >= 0; m--) {
int subkey = (tem[m] / rate) % radix;
data[--buckets[subkey]] = tem[m];
}
rate *= radix;
}
}
就是这一段代码,我盯着它一遍一遍调试,尤其在最后一个for循环中第17行,每次执行的时候都明确会改变数组的顺序了,怯怯的回了一句:那这种实现方式可能不是稳定的排序吧……
还好面试官心地善良,指明了第15、17行确定了它是一种稳定排序,因为最后的回写到原数组中是使用从后往前写入,不会改变原有的顺序。
后面仔细钻研了一遍代码后发现确实如此,因为第2个for循环中,会把基数排序中每一个桶里面的元素数量进行相加,buckets数组记录的结果就是第n个桶以及之前的桶的所有元素个数一共有多少,如下图,十位遍历完成后,buckets[0]就应该为3,buckets[1]也是3,buckets[2]就是4,因为2这个桶里有一个值,以此类推。
原数组:{21,1100,99,102,347,5,496} | ||||
个位遍历 | 十位遍历 | 百位遍历 | 千位遍历 | |
0 | 1100 | 1100,102,5 | 5,21,99 | 5,21,99,102,347,496 |
1 | 21 | 1100,102 | 1100 | |
2 | 102 | 21 | ||
3 | 347 | |||
4 | 347 | 496 | ||
5 | 5 | |||
6 | 496 | |||
7 | 347 | |||
8 | ||||
9 | 99 | 496,99 |
因为buckets里面记录的是当前桶和之前桶里的所有元素的数量,所以我们在回写元素的时候,要每次回写一个数都把buckets的值-1,确保当前的元素已被回写并且记录。所以当我们执行第17行的逻辑时,要--bucket[subkey],也就是元素的指针向前推进,为了和这一步保持相同逻辑,所以在第15行遍历的时候,是从后向前进行遍历,确保在当前位数的遍历时,只针对当前位数把元素放在对应的桶中,元素放置的顺序不会进行改变。也就保证即使有两个1100,也会是哪个先读取到,就把哪个元素在对应位置回写回去,相同元素的下标不会因为进行了一次遍历就发生变化。这样也就保证了基数排序算法的稳定性。
同理,其他的排序算法也可以找到对应确保算法稳定性的代码行
冒泡排序算法
private static void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
for (int j = i; j < arr.length; j++) {
if (arr[i] > arr[j]) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
}
第4行代码,由于arr[i]>arr[j]的时候才会进行比较并交换,所以当arr[i]=arr[j]并不会有元素交换的动作,因此冒泡排序是稳定的算法。当然,也免不了总有刁民就想把条件设置成arr[i]>=arr[j],多一次比较和操作,提高一下复杂度,预留一些优化空间,这谁又能管得了呢~
快速排序算法
private static void quickSort(int[] arr, int low, int high) {
// 指定递归退出条件
if (low < high) {
// 获取排序后基准值的位置
int pivot = partition(arr, low, high);
// 对小于基准值的元素再次进行排序
quickSort(arr, low, pivot - 1);
// 对大于基准值的元素再次进行排序
quickSort(arr, pivot + 1, high);
}
}
private static int partition(int[] arr, int low, int high) {
int pivotKey = arr[low];
while (low < high) {
// 当尾部元素大于基准值时high-1
while (low < high & arr[high] >= pivotKey) {
high--;
}
// 否则将尾部元素赋值给首部位置
arr[low] = arr[high];
// 当首部元素小于基准值时low+1
while (low < high && arr[low] <= pivotKey) {
low++;
}
// 否则将首部元素赋值给尾部位置
arr[high] = arr[low];
}
// 将基准值插入到首部和尾部中间位置
arr[low] = pivotKey;
// 返回基准值的位置
return low;
}
第16行和第22行中,判断的条件都是<=或>=,这也就表明了,当存在相同值的元素时,low和high的值依然会做自增(减)处理,对应arr[high]或arr[low]也就有可能对应位置产生变化。所以快速排序不是一个稳定的排序算法。