冒泡排序
冒泡排序是最基础,最经典的排序算法。联想一下可乐,最大的气泡总是会最快的上升到最上面。所以,冒泡的原理就是,对于一个数组, 每一次循环都让最大的元素冒泡到最后面 。
核心规则:
- 从数组的第一个元素开始,比较相邻两个元素的大小;
- 如果前者比后者大,就交换位置;
- 如果后者比前者大,位置不改变;
- 依次后移,每次循环都让最大的元素排到最后面。
代码如下:
// 冒泡排序public static void bubbleSort(int[] array) {// 1. 每次循环,都能冒泡出剩余元素中最大的元素,因此需要循环 array.length 次for (int i = 0; i < array.length; i++) {// 2. 每次遍历,只需要遍历 0 到 array.length - i - 1中元素,因为之后的元素都已经是最大的了for (int j = 0; j < array.length - i - 1; j++) {//3. 交换元素if (array[j] > array[j + 1]) {int temp = array[j + 1];array[j + 1] = array[j];array[j] = temp;}}}}
时间复杂度分析
比较的次数为:
N + (N - 1) + (N - 2) + ... + 2 + 1 = (N^2 + N)/2
最坏情况下,每次比较都要交换,所以交换的次数为 (N^2 + N)/2
所以时间复杂度为 O(N^2) 。
选择排序
选择排序的重点在于 选择 ,每次从剩余数组中, 选择最大或者最小 的一个,放到数组的一端。
核心规则:
- 维护两个变量,一个用来存储当前最大值,另一个用来存储最大值的索引
- 将当前最大值依次与后面的元素进行比较,如果比当前最大值大,就更新最大值及其索引
- 遍历结束,将最大值放到数组的最右边,即将最右边的元素位置和最大值的位置进行交换
- 重复上面的步骤,直到完成排序
代码如下:
// 选择排序public static void selectSort(int[] array) {// 遍历N次,每次选择数组剩余元素中最大的元素for (int i = 0; i < array.length; i++) {// 暂时把第一个元素当做最大元素,并且记录最大元素的索引int maxIndex = 0;int max = array[0];// 注意 j 从 索引1 开始,到 array.length - i 截止for (int j = 1; j < array.length - i; j++) {// 如果元素大于当前最大值,则替换 max,maxIndexif (array[j] > max) {max = array[j];maxIndex = j;}}// 交换最大值元素 和 数组末尾元素int temp = array[maxIndex];array[maxIndex] = array[array.length - i - 1];array[array.length - i - 1] = temp;}}
时间复杂度分析
比较次数:
N + (N - 1) + (N - 2) + ... + 2 + 1 = (N^2 + N)/2
冒泡 vs 选择
和冒泡排序一样,时间复杂度为 O(N^2) ,但是明显 选择排序要快一点 ,因为冒泡排序需要频繁的交换相邻的两个元素,而选择排序每次遍历只需要交换一次,所以 选择排序真实情况速度要比冒泡快一倍 。
插入排序
插入排序的重点是 插入 ,每次抽离一个元素当作临时元素,将临时元素依次和后面的元素比较,通过比较的大小移动后面元素的位置,找到临时元素最合适的位置插入。
核心规则:
- 第一轮,先将倒数第二个元素抽离作为临时元素
- 将临时元素与后面的元素进行比较,如果后面的元素小于临时元素,则后面的元素左移
- 如果后面的元素大于临时元素,或者已经到达数组末尾,就将临时元素插入当前空隙位置
- 重复上面步骤,完成排序
插入排序已经不是选择最大元素,而是通过每次迭代,将部分元素排好序,以达到全部排序的效果。
这里用图解来解释以上步骤:
- 第一次迭代
- 第二次迭代
- 第三次迭代
可以看到,每次迭代都将 末尾元素排好序 。
代码如下:
一定要记得处理到达尾部的情况。
// 插入排序public static void insertSort(int[] array) {// 从倒数第二位开始,遍历到底0位,遍历 N-1 次for (int i = array.length - 2; i >= 0; i--) {// 存储当前抽离的元素int temp = array[i];// 从抽离元素的右侧开始遍历int j = i + 1;while (j <= array.length - 1) {// 如果某个元素,小于临时元素,则这个元素左移if (array[j] < temp) {array[j - 1] = array[j];} else {// 如果大于,则直接将临时元素插入,然后退出循环。array[j - 1] = temp;break;}j++;}// 处理到达尾部的情况if (j == array.length) {array[j - 1] = temp;}}}
也可以抽离第二个元素,比较其和前面的有序数组的大小来找到合适的位置。代码比较简洁:
/*** 插入排序* 每次在排好序的数组中找到合适的位置插入* 插入排序的大部分时间都浪费在了移动元素上面* 升序-具体算法:* 1.将第一个元素作为有序数组,抽离后面一个元素* 2.从后向前扫描有序数组* 3.如果元素大于抽离的元素,就向后移动一位* 4.直到找到小于或者等于抽离元素的元素,将抽离元素插入到它的后面* @param nums*/public static void insertSorted(int[] nums) {if (nums == null || nums.length < 2) {return;}for (int i = 1; i < nums.length; i++) {int temp = nums[i];int j = i - 1;while (j >= 0 && nums[j] > temp) {nums[j + 1] = nums[j];j--;}nums[j + 1] = temp;}}
时间复杂度分析
- 最好情况下,数组本身就是升序的,每次迭代不需要进行移动,那么时间复杂度就是
O(N) - 最坏情况下,数组为降序排列,每次迭代每次比较都需要进行移动,那么比较次数为
(N^2 + N)/2,
移动次数为 (N^2 + N)/2 ,时间复杂度为 O(N^2)
冒泡 vs 选择 vs 插入
冒泡相比较而言肯定是最差的。
选择和插入就要分情况而定了,如果数组本身有很多元素就已经按照希望的顺序排好序了,那就使用插入排序,否则就使用选择排序。
插入排序的进阶-二分插入排序
插入排序的核心逻辑是找到临时元素的插入位置,而且是在 有序 数组中查找临时元素需要插入的位置,这就立刻可以想到 二分查找 。
所以可以写一个二分查找函数,用来查找元素的插入位置:
// 查找应该插入的索引位置public static int searchIndex(int[] array, int left, int right, int aim) {// 循环查找节点位置while (left < right) {int middle = (left + right) / 2;if (array[middle] < aim) {left = middle + 1;} else {right = middle - 1;}}// 根据当前元素和目标元素进行对比,判断是否需要插入到当前元素之前if(array[left] > aim){return left -1;}// 否则就是当前位置return left;}
修改插入排序的代码:
// 插入排序public static void insertSort(int[] array) {// 从倒数第二位开始,遍历到底0位,遍历 N-1 次for (int i = array.length - 2; i >= 0; i--) {// 存储当前抽离的元素int temp = array[i];int index = searchIndex(array, i + 1, array.length - 1, temp);// #1. 根据插入的索引位置,进行数组的移动和插入int j = i + 1;while (j <= index) {array[j - 1] = array[j];j++;}array[j - 1] = temp;}}
注释 #1 处,已经找到了抽离元素的插入位置 index ,就需要让小于抽离元素的元素左移,然后插入 temp 。
归并排序-分治思想
分治就是将一个大问题分解为几个子问题进行求解,最后再合并几个子问题的解得到原问题的解。分治常常和递归一起使用。
生活中就有很多这样的例子,在疫情期间学校就有统计任务,学校会将任务分配给各个学院的院长,再由院长分配给各个年纪辅导员,辅导员再分配给各个班级的班长….
而利用分治的思想就实现了归并排序~
归并排序的核心思想就是将要排序的数组分解为小数组,将小数组排好序进行合并就得到排序数组。
public static void main(String[] args) {int[] nums = {2,45,6,8,43,24,0};mergeSort(nums,0, nums.length - 1);for (int i = 0; i < nums.length; i++) {System.out.print(nums[i] + " ");}}/*** 归并排序 - 分治* 归并排序是先将数组拆分然后分别排好序再合并* 重点在于将两个排好序的数组进行合并* @param arr* @param left* @param right right = nums.length - 1,注意下标越界问题*/public static void mergeSort(int[] arr, int left, int right) {if (right <= left) return;int mid = (left + right) >> 1;//右移一位相当于除以2mergeSort(arr, left, mid);mergeSort(arr, mid + 1, right);merge(arr, left, mid, right);}/*** 将两个排好序的数组进行合并,并且保证合并之后的数组是排好序的* 具体算法:* 首先需要维护一个中间数组temp,用来放合并的元素* 循环遍历两个子数组,放入temp中* 记得将最后剩余的元素放入temp* 再复制给原数组* @param arr* @param left* @param mid* @param right*/private static void merge(int[] arr, int left, int mid, int right) {int[] temp = new int[right - left + 1];//i表示左数组的第一个元素的下标,j表示右数组的第一个元素下标,k用来记录放入temp中的元素个数int i = left,j = mid + 1,k = 0;while (i <= mid && j <= right) {temp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];}while (i <= mid) temp[k++] = arr[i++];while (j <= right) temp[k++] = arr[j++];for (int l = 0; l < temp.length; l++) {arr[left + l] = temp[l];}//System.arraycopy(temp, 0, arr, left, temp.length);}
快速排序 - 分治
快排也用到了分治的思想,但是和归并排序刚好是反过来的。
归并是先排序左右子数组,然后将有序子数组进行合并排序;快排是先调配出左右子数组,然后对两个子数组进行排序。
所以,快排的重点是如何调配出左右子数组。
- 我们可以通过找到一个基准元素(这个基准元素可以随便选)
- 将数组元素分别与基准元素比较
- 比基准元素大的就放右边,比基准元素小的就放左边
- 然后对分出的两个子数组进行排序
- 达到全部排好序
```java
/**
- 快速排序
- 快排是通过一个基准元素,将数组分为小于基准元素和大于基准元素的两个子数组
- 然后分别排序以达到全排好序
- @param arr
- @param begin
- @param end / public static void quickSorted(int[] arr, int begin, int end) { if (end <= begin) return; int pivot = partition(arr, begin, end); quickSorted(arr, begin, pivot - 1); quickSorted(arr, pivot + 1, end); } /*
- 这个方法的作用就是返回基准元素pivot的下标
- 且保证pivot左侧的元素都小于pivot,右侧的元素都大于pivot
- 方法巧妙的地方在于维护一个变量counter记录小于pivot的数量,counter初始化为begin,
- 只要有小于pivot的元素就和counter交换位置,最后counter就是基准元素的下标位置
- @param arr
- @param begin
- @param end
- @return
*/
private static int partition(int[] arr, int begin, int end) {
//基准元素随便选
int pivot = end, counter = begin;
for (int i = begin; i < end; i++) {
} int temp = arr[pivot]; arr[pivot] = arr[counter]; arr[counter] = temp; return counter; }if (arr[i] < arr[pivot]) {int temp = arr[counter];arr[counter] = arr[i];arr[i] = temp;counter++;}
```
