Java排序算法
稳定性:两个相同元素经过排序后是否保持原有的顺序,保持则稳定,否则不稳定
1. 冒泡排序
因为越大的元素会经过交换慢慢“浮”到数列的顶端,如同气泡最终会上浮到顶端一样
- 比较相邻元素,如果第一个元素比第二个元素大,交换这两个元素
- 经过第一趟排序后,最后一个元素被交换成整个数列中最大的元素
- 再依次比较除最大的元素之外的数
- 重复执行以上的步骤,直到没有元素可比较
-
关键点:每次做完一轮比较,最大数不再参与下一轮的比较
-
Java代码实现:
public static void bubbleSort(int[] arr) { int len = arr.length; for (int i = 0; i < len; i++) { for (int j = 0; j < len - i - 1; j++) { if (arr[j] > arr[j + 1]) { int tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; } } } }
-
时间复杂度: O(n²)
-
稳定性:稳定(只有更大的元素才会被交换到后面的位置,所以排序不会改变相同值的前后位置)
2. 选择排序
- 从待排序数列中找到最小的数
- 通过跟起始位置的数进行交换 将这个最小的数放到数列的起始位置
- 从剩下未排好序的数列中找到最小的数,通过交换放到第二个数的位置
- 重复执行找最小数并交换的操作,直到所有的数都排好序
-
关键点:第n次排序,那么前n-1个数已经是有序的,从剩下的数中找最小值,和第n个数进行交换
-
Java代码实现:
public static void selectionSort(int[] arr) { int len = arr.length; for (int i = 0; i < len; i++) { int minIndex = i; for (int j = i; j < len; j++) { if (arr[j] < arr[minIndex]) { minIndex = j; } } int tmp = arr[i]; arr[i] = arr[minIndex]; arr[minIndex] = tmp; } }
-
时间复杂度: O(n²)
-
稳定性:不稳定(如果一个比当前元素小的元素出现在了和当前元素相同的元素的后面,那么交换会破坏原有的顺序。举例说明:3 4 3 2 1,第一趟排序后数据变为1 4 3 2 3,第一个3被交换到了第二个3的后面)
3. 插入排序
将未排序的元素插入到已排序的数列适当位置上,来实现所有元素有序
- 将当前的元素n和当前元素前面n-1个元素从后至前比较
- 找到第一个小于或者等于当前元素的元素k
- 将k后面到n前面的元素依次后移
- 将n放到k的后面
- 依次执行以上步骤,直到所有元素都排完
-
关键点:从待排序元素开始从后向前扫描进行比较
-
Java代码实现:
public static void insertionSort(int[] arr) { int len = arr.length; for (int i = 0; i < len - 1; i++) { int current = arr[i + 1]; int j = i; while (j >= 0 && current < arr[j]) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = current; } }
-
时间复杂度: O(n²)
-
稳定性:稳定(如果两个元素相等,那么后一个元素会插入到前一个元素的后面)
4. 希尔排序
-
Java代码实现:
public static void shellSort(int[] arr) { int len = arr.length; int tmp, gap = len / 2; while (gap > 0) { for (int i = gap; i <len ; i++) { int preIndex = i - gap; tmp = arr[i]; while (preIndex >= 0 && arr[preIndex] > tmp) { arr[preIndex + gap] = arr[preIndex]; preIndex -= gap; } arr[preIndex + gap] = tmp; } gap /= 2; } }
5. 快速排序
- 选择一个元素当作基准值
- 设置两个指针,一个在队首、一个在队尾
- 队首指针寻找比基准值大的元素,队尾指针寻找比基准值小的元素
- 交换两个指针寻找到的元素,继续寻找,直到两指针相遇
- 将基准值和指针位置元素交换
- 递归把小于基准值的子数列和大于基准值的子数列分别进行排序
-
关键点:为什么队尾指针先开始走?原因:通常情况下我们以队首元素作为基准值,如果队首指针先开始走,那么到比基准值大的元素停止,完成一趟排序的时候,基准值要和指针位置元素交换,这时交换后比基准元素大的元素被交换到了左边,不符合基准元素左边的元素小于基准元素的要求。
-
Java代码实现:
public static void quickSort(int[] arr, int startIndex, int endIndex) { if (startIndex >= endIndex) { return ; } int pivotIndex = partitionPointer(arr, startIndex, endIndex); quickSort(arr, startIndex, pivotIndex - 1); quickSort(arr, pivotIndex + 1, endIndex); } private static int partitionPointer(int[] arr, int startIndex, int endIndex) { int pivot = arr[startIndex]; int left = startIndex, right = endIndex; while (left < right) { while (left < right && arr[right] >= pivot) { right--; } while (left < right && arr[left] <= pivot) { left++; } if (left < right) { int tmp = arr[left]; arr[left] = arr[right]; arr[right] = tmp; } } arr[startIndex] = arr[left]; arr[left] = pivot; return left; }
-
算法原理-2(挖坑法)
- 选择一个元素当作基准值
- 设置两个指针,一个在队首、一个在队尾
- 设置坑的位置,初始时等于基准值的位置
- 从队尾指针开始向前移动,找到比基准值小的元素,将这个元素放到坑的位置,同时这个元素原来的位置当作新的坑,队尾指针停止
- 队首指针开始向后移动,找到比基准值大的元素,将这个元素放到坑的位置,同时这个元素原来的位置当作新的坑,队首指针停止
- 重复4,5步骤,直到两指针相遇
- 将基准元素放到两指针相遇的位置
- 递归把小于基准值的子数列和大于基准值的子数列分别进行排序
-
关键点:指针找到要交换位置的元素后,放到坑里,这个元素的原始位置作为新的坑,指针停止,交给下一个指针执行。
-
Java代码实现:
public static void quickSort(int[] arr, int startIndex, int endIndex) { if (startIndex >= endIndex) { return ; } int pivotIndex = partitionDig(arr, startIndex, endIndex); quickSort(arr, startIndex, pivotIndex - 1); quickSort(arr, pivotIndex + 1, endIndex); } private static int partitionDig(int[] arr, int startIndex, int endIndex) { int pivot = arr[startIndex]; int left = startIndex, right = endIndex; int pitIndex = startIndex; while (left < right) { for (;left < right; right--) { if (arr[right] < pivot) { arr[pitIndex] = arr[right]; pitIndex = right; left++; break; } } for (;left < right; left++) { if (arr[left] > pivot) { arr[pitIndex] = arr[left]; pitIndex = left; right--; break; } } } arr[left] = pivot; return left; }
-
时间复杂度: O(n*logn)
-
稳定性:不稳定
6. 归并排序
- 将数列一分为二
- 递归的对两个子数列进行归并,递归的终止条件是子数列长度小于等于1
- 将两个有序的子数列归并成有序的大数列
-
关键点:“归并”:将两个有序数列合并为一个大的有序数列
-
Java代码实现-1:
public static int[] mergeSort(int[] arr) { int len; if ((len = arr.length) <= 1) { return arr; } int mid = len >> 1; int[] left = Arrays.copyOfRange(arr, 0, mid); int[] right = Arrays.copyOfRange(arr, mid, len); return merge(mergeSort(left), mergeSort(right)); } private static int[] merge(int[] left, int[] right) { int[] result = new int[left.length + right.length]; for (int index = 0, i = 0, j = 0; index < result.length ; index++) { if (i >= left.length) { result[index] = right[j++]; } else if (j >= right.length) { result[index] = left[i++]; } else if (left[i] <= right[j]) { result[index] = left[i++]; } else { result[index] = right[j++]; } } return result; }
-
Java代码实现-2:
public static void mergeSort(int[] arr) { sort(arr, 0, arr.length - 1); } private static void sort(int[] arr, int start, int end) { if (start >= end) { return ; } int mid = start + ((end - start) >> 1); sort(arr, start, mid); sort(arr, mid + 1, end); merge(arr, start, mid, end); } private static void merge(int[] arr, int start, int mid, int end) { int[] tmp = new int[end - start + 1]; int index = 0, left = start, right = mid + 1; while (left <= mid && right <= end) { tmp[index++] = arr[left] < arr[right] ? arr[left++] : arr[right++]; } while (left <= mid) { tmp[index++] = arr[left++]; } while (right <= end) { tmp[index++] = arr[right++]; } for (int i = 0; i < tmp.length; i++) { arr[start + i] = tmp[i]; } }
-
时间复杂度: O(n*logn)
-
稳定性:稳定(在归并两个子数列是,如果两个子数列的值相等,先从左面子数列取值,所以不会破坏相同值的前后顺序)
7. 堆排序
使用“堆”这种数据结构来实现排序。堆是一种具有特殊性质的完全二叉树。分为两种:
最大堆:父节点的值总是大于或者等于两个子节点的值
最小堆:父节点的值总是小于或者等于两个子节点的值
堆与数组的联系:如果一个元素在数组中的索引是n,那么这个元素的左子节点的索引是2n+1,右子节点的索引是2n+2;如果一个子节点的索引是n,那么它的父节点的索引是(n-1)/2
- 将数列构造成一个最大堆,此时堆顶元素就是整个数列最大的元素
- 将堆顶元素和最后一个元素进行交换,此时最后一个元素是整个数列最大的元素
- 将剩余的n-1个元素再次构建成一个最大堆,再将堆顶元素和第n-1个元素交换
- 重复执行构建最大堆和交换操作,直到所有元素都操作完
-
关键点:构建最大堆,将堆顶元素交换到队尾,继续将n-1个元素构建最大堆,再次交换…
-
Java代码实现:
public static void heapSort(int[] arr) { buildMaxHeap(arr); for (int i = arr.length - 1; i > 0; i--) { int tmp = arr[0]; arr[0] = arr[i]; arr[i] = tmp; downAdjust(arr, 0, i); } } private static void buildMaxHeap(int[] arr) { // 获取最后一个非叶子节点,从最后一个非叶子节点开始调整?因为要做下沉操作,叶子节点没有可下沉的空间了 for (int i = (arr.length - 2) / 2; i >= 0 ; i--) { downAdjust(arr, i, arr.length); } } /** * 堆元素下沉 * 1. 找到下沉节点的最大子节点 * 2. 如果该子节点比下沉节点大,那么下沉节点和该子节点交换 * 2. 直到到下沉的结束位置截止 * @param arr 数组 * @param adjustIndex 下沉节点的索引 * @param len 下沉的结束位置 */ private static void downAdjust(int[] arr, int adjustIndex, int len) { int tmp = arr[adjustIndex]; int childIndex = 2 * adjustIndex + 1; while (childIndex < len) { if (childIndex + 1 < len && arr[childIndex] < arr[childIndex + 1]) { childIndex++; } if (tmp >= arr[childIndex]) { break; } arr[adjustIndex] = arr[childIndex]; adjustIndex = childIndex; childIndex = 2 * childIndex + 1; } arr[adjustIndex] = tmp; }
-
时间复杂度: O(n*logn)
-
稳定性:稳定(堆的节点的值大于或者等于子节点,在下沉操作时不会和子节点交换)
8. 计数排序
计数排序适用于数列的数值的范围在较小的情况下。通过标记数值出现的次数来实现排序,不通过比较来实现排序
- 获取数列的取值范围,根据取值范围新建标记数组
- 遍历数列,在标记数组中标记每个元素出现的次数
- 遍历标记数组
例子1: 给定1-9之间的15个数,使用计数排序对数列排序
-
Java代码实现:
/** * 简单计数排序 * 1. 找到数组最大值 * 2. 创建长度为最大值的标记数组 * 3. 遍历原数组,填充标记数组 * 4. 遍历标记数组,填充到原数组 * @param arr */ public static void simple(int[] arr) { int len = arr.length; int max = arr[0]; for (int i = 0; i < len; i++) { if (arr[i] > max) { max = arr[i]; } } int[] countingArr = new int[max + 1]; for (int i = 0; i < len; i++) { countingArr[arr[i]]++; } int index = 0; for (int i = 0; i < countingArr.length; i++) { for (int j = 0; j < countingArr[i]; j++) { arr[index++] = i; } } }
例子2: 给定90-99之间的15个数,使用计数排序对数列排序
-
Java代码实现:
/** * 计数排序进阶,数列的值在一定的取值范围内,但是都是比较大的数字 * 1. 获取数列的最大值和最小值 * 2. 创建标记数组,数组长度是最大值-最小值+1 * 3. 遍历原数组,填充标记数组 * 4. 遍历标记数组,得到结果 * @param arr */ public static void progress(int[] arr) { int len = arr.length; int min = arr[0], max = min; for (int i = 0; i < len; i++) { if (arr[i] > max) { max = arr[i]; } if (arr[i] < min) { min = arr[i]; } } int diff = max - min + 1; int[] countingArr = new int[diff]; for (int i = 0; i < len; i++) { countingArr[arr[i] - min]++; } int index = 0; for (int i = 0; i < countingArr.length; i++) { for (int j = 0; j < countingArr[i]; j++) { arr[index++] = min + i; } } }
-
时间复杂度: O(n + k) n是数组长度,k是数列取值范围
-
稳定性:稳定