最近笔试经常会考排序算法的稳定性,这篇文章就来汇总一下常见排序算法的实现,并基于实现来分析对应排序算法的稳定性。
参考链接
稳定性是什么
排序算法的稳定性就是经过排序之后原始序列中相同元素的相对位置是否可能发生改变,如果可能发生改变则是不稳定的,不会发生改变则是稳定的。比如原始序列为【5,3,1,3,2,6】(通过数字的粗细来表示不同的3),经过排序如果结果是【1,2,3,3,5,6】则代表是稳定的排序,如果结果是【1,2,3,3,5,6】则代表是不稳定的排序。
各大排序算法的稳定性分析
主要分析冒泡排序,选择排序,插入排序,堆排序,归并排序,快速排序,计数排序,桶排序和基数排序
冒泡排序 稳定
冒泡排序的思想就是每轮将当前最大的数字移动到最后来完成排序。
void bubbleSort(int[] nums){
int len = nums.length;
for(int i = 0; i < len; i++){ // i表示当前最右侧有序的元素个数
for(int j = 0; j < len - i - 1; j++)
if(nums[j] > nums[j+1]){
swap(nums, j, j + 1);
}
}
}
}
我们需要关注的是swap(nums, j, j + 1);这个操作是否会导致相同值的元素相对位置发生改变。根据 if(nums[j] > nums[j+1])这个条件可知,只有前一个元素大于后一个元素时才会发生交换,因此不会改变相同元素的相对顺序。
选择排序 不稳定
选择排序的思想是每轮选择当前最小的元素放到前面。
void selectSort(int[] nums){
int len = nums.length;
for(int i = 0; i < len; i++){
int minIndex = i;
for(int j = i+1; j < len; j++){
if(nums[j] < nums[minIndex]){
minIndex = j;
}
}
if(minIndex != i){
swap(nums, minIndex, i);
}
}
}
我们需要关注的是swap(nums, minIndex, i);这个操作是否会导致相同值的元素相对位置发生改变。判断条件是if(minIndex != i),在索引为i的元素与minIndex进行交换时,我们无法保证与索引i的元素值相同的元素与minIndex的元素之间的相对位置,因此就是不稳定的,比如【5,3,1,3,2,6】->【1,3,5,3,2,6】->【1,2,5,3,3,6】这样就破坏了稳定性。
插入排序 稳定
插入排序的思想是每轮将无序区的元素插入到有序区中合适的位置。
void insertSort(int[] nums){
int len = nums.length;
for(int i = 1; i < len; i++){
for(int j = i; j >= 1; j--){
if(nums[j] < nums[j-1]){
swap(nums, j, j-1);
}else{
break;
}
}
}
}
我们需要关注的是swap(nums, j, j-1);这个操作是否会导致相同值的元素相对位置发生改变。判断条件是if(nums[j] < nums[j-1]),相邻的元素如果值相同的话并不会发生交换,因此不会影响其相对位置。
堆排序 不稳定
建立最大堆,每次取堆顶元素放最后。是选择排序的一种。 不稳定。
void heapSort(int[] nums){
buildMaxHeap(nums);
int lastIndex = nums.length - 1;
for(int i = 0; i < nums.length; i++){
swap(nums, 0, lastIndex--);
heapify(nums, 0, lastIndex);
}
}
void buildMaxHeap(int[] nums){
int lastBranchNodeIndex = (nums.length - 1 - 1) >> 1;
for(int i = lastBranchNodeIndex; i>=0;i--){
heapify(nums, i, nums.length-1);
}
}
void heapify(int[] nums, int index, int lastIndex){
while ((index << 1) + 1 <= lastIndex) {
int maxIndex = index;
int leftChild = (index << 1) + 1;
int rightChild = (index << 1) + 2;
if(nums[leftChild] > nums[maxIndex]){
maxIndex = leftChild;
}
if(rightChild <= lastIndex && nums[rightChild] > nums[maxIndex]){
maxIndex = rightChild;
}
if(maxIndex != index){
swap(nums, index, maxIndex); // 关键行
index = maxIndex;
}else{
break;
}
}
}
归并排序 稳定
归并排序的算法就是基于分治策略,将一个大的待排序数组拆分成两个小数组,逐步拆分直到数组中的元素为1,然后再逐步开始合并。
int[] tmp = new int[nums.length];;
void mergeSort(int[] nums, int left, int right){
if(left >= right)
return;
int mid = ((right - left) >> 1) + left;
mergeSort(nums, left, mid);
mergeSort(nums, mid+1, right);
int index = left;
int i = left, j = mid + 1;
while(i <= mid && j <= right){
if(nums[i] <= nums[j]){ // 关键行
tmp[index++] = nums[i++];
}else{
tmp[index++] = nums[j++];
}
}
while(i <= mid){
tmp[index++] = nums[i++];
}
while(j <= right){
tmp[index++] = nums[j++];
}
for(i = left; i<= right;i++){
nums[i] = tmp[i];
}
}
关键代码是合并的时候是否保证了值相同的元素相对位置不发生改变,由判断条件if(nums[i] <= nums[j])可知,当待合并的两个数组中出现相同元素时,会将本来在左侧的元素仍然放在左侧,因此会保证其相对位置。
快速排序 不稳定
快速排序使用了分治法(Divide and conquer)策略,通过pivot将数组分成两部分,并分别进行排序。
快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
void quickSort(int[] nums, int left, int right){
if(left >= right){
return;
}
int pivotIndex = new Random().nextInt(right - left + 1) + left;
swap(nums, pivotIndex, left);
int partitionIndex = partition(nums, left, right);
quickSort(nums, left, partitionIndex - 1);
quickSort(nums, partitionIndex + 1, right);
}
int partition(int[] nums, int left, int right){
int pivotValue = nums[left];
while(left < right){
while(left < right && nums[right] >= pivotValue){
right--;
}
swap(nums, left, right);
while(left< right && nums[left] <= pivotValue){
left++;
}
swap(nums, left, right);
}
return left;
}
希尔排序 不稳定
void shellSort(int[] arr) {
int len = arr.length;
int tmp;
for (int step = len >> 1; step >= 1; step = setp >> 1) {
for (int i = step; i < len; i++) { // 对各个分组进行插入排序
tmp = arr[i];
int j = i - step;
while (j >= 0 && arr[j] > tmp) {
arr[j + step] = arr[j];
j -= step;
}
arr[j + step] = tmp;
}
}
}