测试函数:
@Test
public void sort() {
int[] arr = {25, 14, 1, 13, 89, 200, 130};
//bubbleSort(arr);
//insertSort(arr);
//shellSort(arr);
//quickSort(arr,0,arr.length-1);
//selectSort(arr);
//heapSort(arr);
//radixSort(arr);
mergeSort(arr);
System.out.println(Arrays.toString(arr));
}
public void swap(int[] arr, int x, int y) {
if (x == y) {
return;
}
int temp = arr[x];
arr[x] = arr[y];
arr[y] = temp;
}
private void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
boolean flag = false;
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
flag = true;
swap(arr, j + 1, j);
}
}
if (!flag)
return;
}
}
private void insertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
for (int j = i - 1; j >= 0 && arr[j + 1] < arr[j]; j--) {
swap(arr, j, j + 1);
}
}
}
private void shellSort(int[] arr) {
int gap = arr.length / 2;
while (gap > 0) {
for (int i = gap; i < arr.length; i = i + gap) {
for (int j = i - gap; j >= 0 && arr[j + gap] < arr[j]; j = j - gap) {
swap(arr, j + gap, j);
}
}
gap /= 2;
}
}
private void quickSort(int[] arr) {
// quickSortR(arr, 0, arr.length - 1);
quickSortR1(arr, 0, arr.length - 1);
}
//经典快排,每次以最左边的数为基准比较,每次只能确定一个数的位置,如果有多个重复的数会有多次无效排序
private void quickSortR(int[] arr, int left, int right) {
if (left < right) {
int baseIndex = division(arr, left, right);
quickSortR(arr, left, baseIndex - 1);
quickSortR(arr, baseIndex + 1, right);
}
}
private int division(int[] arr, int left, int right) {
int base = arr[left];
while (left < right) {
if (arr[right] >= base) {
right--;
}
arr[left] = arr[right];
if (arr[left] <= base) {
left++;
}
arr[right] = arr[left];
}
arr[left] = base;
return left;
}
//优化后的快排,partition()方法返回的是一个数组,每次排序能确定所有重复的数的位置
private void quickSortR1(int[] arr, int l, int r) {
if (l < r) {
//随机快排,不以数组两端的数为基准,每次从数组中随机取一个值作为基准
// swap(arr, l + (int) (Math.random() * (r - l + 1)), r);
int[] p = partition(arr, l, r);
quickSortR1(arr, l, p[0] - 1);
quickSortR1(arr, p[1] + 1, r);
}
}
private int[] partition(int[] arr, int l, int r) {
int less = l - 1;
int more = r;
while (l < more) {
if (arr[l] < arr[r]) {
swap(arr, ++less, l++);
} else if (arr[l] > arr[r]) {
swap(arr, --more, l);
} else {
l++;
}
}
swap(arr, more, r);
return new int[]{less + 1, more};
}
快排最坏情况:数据有序,会退化成冒泡排序,时间复杂度O(n^2)
数据有序时,以第一个关键字为基准分成两个子序列,前一个序列为空,(需要拆分n次)此时效率最差
这里提及一下在partition()扣边界的过程中碰到的一点问题:
swap方法中more --和-- more在实现上的细微差异
more – 和 – more的差别主要体现在swap()实际交换的位置
如果是more --,考虑到while(l < more),交换的就是与l比较的数,那么一次交换完毕后,more --,more实际指向的数其实还是一个未比较的状态,这个数的位置还没有确定
如果是-- more,交换的实际上是与l比较的数 --之后的结果,那么一次交换完毕之后,more实际指向的数已经完成比较并确定位置
private int[] partition(int[] arr, int l, int r) {
int less = l - 1;
int more = r;
while (l < more) {
if (arr[l] < arr[r]) {
swap(arr, ++less, l++);
} else if (arr[l] > arr[r]) {
swap(arr, --more, l);
} else {
l++;
}
}
swap(arr, more, r);
return new int[]{less + 1, more};
}
private int[] partition1(int[] arr, int l, int r) {
int less = l - 1;
int more = r;
while (l <= more) {
if (arr[l] < arr[r]) {
swap(arr, ++less, l++);
} else if (arr[l] > arr[r]) {
swap(arr, more --, l);
} else {
l++;
}
}
return new int[]{less + 1, more};
}
private void selectSort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
int index = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[index]) {
index = j;
}
}
swap(arr, index, i);
}
}
private void heapSort(int[] arr) {
if(arr == null || arr.length < 2){
return;
}
// for (int i = arr.length / 2; i >= 0; i--) {
// heapAdjust(arr, i, arr.length);
// }
for(int i = 0; i = 0; heapSize--) {
swap(arr,0,heapSize);
heapAdjust(arr, 0, heapSize);
}
}
private void heapInsert(int[] arr,int index){
while(arr[index] > arr[(index - 1) / 2]){
swap(arr,index,(index - 1) / 2);
index = (index - 1) / 2;
}
}
private void heapAdjust(int[] arr, int parent, int length) {
int child = 2 * parent + 1;
while (child < length) {
if (child + 1 < length && arr[child] < arr[child + 1]) {
child++;
}
if (arr[parent] >= arr[child]) {
break;
}
swap(arr,parent,child);
parent = child;
child = 2 * child + 1;
}
}
private void radixSort(int[] arr) {
int radix = 10;
int[] count = new int[radix];
int[] bucket = new int[arr.length];
int i = 0, j = 0;
for (int d = 0; d < 3; d++) {
for (i = 0; i < radix; i++)
count[i] = 0;
for (i = 0; i < arr.length; i++) {
j = getDigit(arr[i], d);
count[j]++;
}
for (i = 1; i < radix; i++) {
count[i] = count[i] + count[i - 1];
}
//从arr.length-1到0的原因,就是低位排序完成后低位较大的肯定在后面,如果高位数字相同,从前往后按照count[]数组中存储的位置,低位较小的数组就会排在低位较大的数字后面
//比如完成个位排序后 13,23,54,14
//如果是0到arr.length-1结果 14,13,23,54
//如果是arr.length-1到0结果 13,14,23,54
for (i = arr.length - 1; i >= 0; i--) {
j = getDigit(arr[i], d);
bucket[count[j] - 1] = arr[i];
count[j]--;
}
for (i = 0; i < arr.length; i++) {
arr[i] = bucket[i];
}
}
}
private int getDigit(int x, int d) {
int[] a = {1, 10, 100};
return (x / a[d]) % 10;
}
private void mergeSort(int[] arr) {
mergeSortR(arr, 0, arr.length - 1);
}
private void mergeSortR(int[] arr, int left, int right) {
if (left < right) {
int mid = left + ((right - left) >> 1);
mergeSortR(arr, left, mid);
mergeSortR(arr, mid + 1, right);
merge(arr, left, mid, right);
}
}
private void merge(int[] arr, int left, int mid, int right) {
int[] temp = new int[right - left + 1];
int p1 = left;
int p2 = mid + 1;
int t = 0;
while (p1 <= mid && p2 <= right) {
temp[t++] = arr[p1] > arr[p2] ? arr[p2++] : arr[p1++];
}
while (p1 <= mid) {
temp[t++] = arr[p1++];
}
while (p2 <= right) {
temp[t++] = arr[p2++];
}
t = 0;
while (left <= right) {
arr[left++] = temp[t++];
}
}
首先,排序算法的稳定性大家应该都知道,通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai = Aj, Ai原来在位置前,排序后Ai还是要在Aj位置前。
其次,说一下稳定性的好处。排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排序算法稳定,对基于比较的排序算法而言,元素交换的次数可能会少一些(个人感觉,没有证实)。
回到主题,现在分析一下常见的排序算法的稳定性,每个都给出简单的理由。
例:
姓名 | 班级 | 成绩 |
---|---|---|
李 | 2 | 12 |
王 | 1 | 50 |
赵 | 1 | 60 |
首先按照成绩大小排序,然后按照班级排序。稳定的排序可以保证每一个班级的成绩是按照从小到大的顺序排列好的
(1)冒泡排序
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无 聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改 变,所以冒泡排序是一种稳定排序算法。
(2)选择排序
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个 元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么 交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9, 我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。
(3)插入排序
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开 始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相 等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳 定的。(4)快速排序
快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] > a[center_index]。如果i和j都走不动了,i <= j, 交换a[i]和a[j],重复上面的过程,直到i>j。 交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为 5 3 3 4 3 8 9 10 11, 现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻。(5)归并排序
归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有 序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定 性。那么,在短的有序序列合并的过程中,稳定是是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结 果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。(6)基数排序
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优 先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。(7)希尔排序(shell)
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小, 插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元 素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。(8)堆排序
我们知道堆的结构是节点i的孩子为2i和2i+1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n /2-1, n/2-2, …1这些个父节点选择元素时,就会破坏稳定性。有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没 有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。
综上,得出结论: 选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法,而冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法。
master公式计算递归行为的时间复杂度