冒泡排序
1.算法思路
从后往前,两两进行比较,将较小的元素移动到数组靠前的位置,循环这个过程,直到所有的元素按从小到大的顺序排列
2.代码实现
public class Sort {
//冒泡排序,从后往前,将较小值一个一个往前面冒
private void bubbleSort2(int[] nums) {
int n = nums.length;
for (int i = 0; i < n - 1; i++) {
for (int j = n - 1; j >= i + 1; j--) {
if (nums[j] < nums[j - 1]) {
swap(nums, j, j - 1);
}
}
}
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
3.复杂度分析
- 时间复杂度:需要两层循环,所以时间复杂度为O(n2)。
- 空间复杂度:不需要额外的内存空间,所以空间复杂度为O(1)。
简单选择排序
1.算法思路
由于冒泡排序每次比较,满足条件就会执行一次交换操作,选择排序为了减少交换次数,将每一次的最小值记录下来,最后再与当前元素交换。
2.代码实现
public class Sort {
//选择排序,首先定义一个min下标,用来记录当前最小值,当某个值小于他时,跟新
//最后将最小值与当前游标所在值交换
private void chooseSort(int[] nums) {
int n = nums.length;
for (int i = 0; i < n - 1; i++) {
int min = i;
for (int j = i + 1; j < n; j++) {
if (nums[min] > nums[j]) {
min = j;
}
}
swap(nums, min, i);
}
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
3.复杂度分析
- 时间复杂度:需要两层循环,所以时间复杂度为O(n2)。
- 空间复杂度:需要额外常数级别的内存空间,所以空间复杂度为O(1)。
直接插入排序
1.算法思路
直接插入排序的思想是,维护一个已经排好序的列表,当遍历到列表后未排序的当前元素时,找到元素在之前列表中对应的位置,然后插入到这个位置,重复这个过程,直到所有的元素有序
2.代码实现
public class Sort {
//记录当前游标和对应的值,然后把他插入到正确的位置,(游标前的元素都是排好序的)
private void insertSort(int[] nums) {
int n = nums.length;
for (int i = 1; i < n; i++) {
int j = i;
int temp = nums[i];
while (j > 0 && temp < nums[j - 1]) {
nums[j] = nums[j - 1];
j--;
}
nums[j] = temp;
}
}
}
3.复杂度分析
- 时间复杂度:需要两层循环,所以时间复杂度为O(n2)。
- 空间复杂度:需要额外常数级别的内存空间,所以空间复杂度为O(1)。
希尔排序
1.算法思路
希尔排序是插入排序的改进版,也是最先发现的高效排序算法。
与插入排序相比,希尔排序多了一个间隔的设置,间隔的设置可以自行决定。比如下面的实现,将初始间隔设置为数组长度一半,每次减半,直到间隔为1。然后将间隔处的几个元素进行插入排序(比如数组长度为8,间隔为4时,比较的是索引为0,4;1,5;2,6;3,7处的元素),排好之后,再缩小间隔,重复这个过程,直到间隔为1,所有元素排好为止。
2.代码实现
public class Sort {
//设置一个排序间隔,假设数组总长度为8,不妨设初始间隔为4,然后每隔4个元素
//进行一次插入排序,所有元素都排好后,再缩小间隔为2,重复上一步的操作,直到
//间隔为1,则完成整个数组的排序
private void shellSort(int[] nums) {
int n = nums.length;
for(int gap=n/2;gap>0;gap/=2){
for (int i = gap; i < n; i++) {
int j = i;
int temp = nums[i];
while (j >=gap && temp < nums[j - gap]) {
nums[j] = nums[j - gap];
j-=gap;
}
nums[j] = temp;
}
}
}
}
3.复杂度分析
- 时间复杂度:希尔排序的时间复杂度与gap的选取有关,平均时间复杂度为O(nlogn)~O(n2)。
- 空间复杂度:需要额外常数级别的内存空间,所以空间复杂度为O(1)。
堆排序
1.算法思路
堆排序是简单选择排序的改进版。
按从小到大排序,需要借助大顶堆。首先遍历整个数组,构建一个大顶堆。然后再进行遍历,交换堆顶元素与数组末尾元素(由于堆顶元素总是最大的,所以最大的都被移动到数组末尾),然后重新调整成大顶堆,循环遍历,直到所有元素有序。
adjust方法用于构造大顶堆,其思路是比较当前节点,左孩子,右孩子三者的大小,将最大的放到当前节点,然后重复这个过程,直到根节点是最大节点。
2.代码实现
迭代:
public class Sort {
//堆排序
private void heapsort(int[] nums) {
int n = nums.length;
for (int i = n / 2 - 1; i >= 0; i--) {
adjust(nums, i, n - 1);
}
for (int i = n - 1; i >= 1; i--) {
swap(nums, i, 0);
adjust(nums, 0, i - 1);
}
}
private void adjust(int[] nums, int s, int m) {
int j, temp = nums[s];
for (j = 2 * s + 1; j <= m; j = j * 2 + 1) {
while (j < m && nums[j] < nums[j + 1]) {
j++;
}
if (temp >= nums[j]) break;
nums[s] = nums[j];
s = j;
}
nums[s] = temp;
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
递归:
public class Sort {
private void heapsort(int[] nums) {
int n=nums.length;
for(int i=n/2-1;i>=0;i--){
adjust(nums,i,n-1);
}
for(int i=n-1;i>=1;i--){
swap(nums,0,i);
adjust(nums,0,i-1);
}
}
private void adjust(int[] nums,int s,int m){
if(s>=m) return;
int left=2*s+1;
int right=2*s+2;
int maxIdx=s;
if(left<=m&&nums[left]>nums[maxIdx]) maxIdx=left;
if(right<=m&&nums[right]>nums[maxIdx]) maxIdx=right;
if(maxIdx!=s){
swap(nums,maxIdx,s);
adjust(nums,maxIdx,m);
}
}
private void swap(int[] nums,int i,int j){
int temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
}
3.复杂度分析
- 时间复杂度:adjust建堆的时间复杂度为O(logn),最多需要循环n次,所以平均时间复杂度为O(nlogn)。
- 空间复杂度:需要额外常数级别的内存空间,所以空间复杂度为O(1)。
归并排序
1.算法思路
归并排序是几个高效排序算法中唯一稳定的排序算法。
归并排序的思路是先将数组两两分开,每部分排好序后,再两两进行合并。排序合并的过程中,需要借助一个辅助数组,将排好序的元素先加入辅助数组,再转到原数组对应位置。
2.代码实现
public class Sort {
//归并排序
private void mergesort(int[] nums, int low, int high) {
if (low >= high) return;
int middle = low + (high - low) / 2;
mergesort(nums, low, middle);
mergesort(nums, middle + 1, high);
merge(nums, low, middle, high);
}
private void merge(int[] nums, int low, int middle, int high) {
int[] temp = new int[high - low + 1];
int index = high - low;
int i = middle, j = high;
while (i >= low && j >= middle + 1) {
if (nums[i] > nums[j]) {
temp[index--] = nums[i--];
} else {
temp[index--] = nums[j--];
}
}
while (i >= low) {
temp[index--] = nums[i--];
}
while (j >= middle + 1) {
temp[index--] = nums[j--];
}
for (int k = 0; k < temp.length; k++) {
nums[k + low] = temp[k];
}
}
}
3.复杂度分析
- 时间复杂度:merge的时间复杂度为O(n),需要归并logn次,所以平均时间复杂度为O(nlogn)。
- 空间复杂度:需要额外O(n)空间的辅助空间,所以空间复杂度为O(n)。
快速排序
1.算法思路
快速排序是冒泡排序的改进版,优化之后的快速排序是最快的排序算法。
快速排序的思想是选取一个枢纽值,让所有元素基本有序(前面的小于它,后面的大于它);然后再对前半部分以及后半部分进行同样的操作,让它们分别基本有序。直到所有的元素有序。
快速排序的优化,可以从以下四个地方考虑:
- 枢纽值得选取,三值取中
- 优化不必要得交换
- 数据量较小时,使用插入排序,较大时,再改为快排
- 优化递归操作,可以使用尾递归
2.代码实现
public class Sort {
//快速排序
private void qsort(int[] nums, int low, int high) {
if (low < high) {
int povit = partition(nums, low, high);
qsort(nums, low, povit - 1);
qsort(nums, povit + 1, high);
}
}
private int partition(int[] nums, int low, int high) {
int povitkey = nums[low];
while (low < high) {
while (low < high && povitkey <= nums[high]) high--;
swap(nums, low, high);
while (low < high && povitkey >= nums[low]) low++;
swap(nums, low, high);
}
return low;
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
3.复杂度分析
- 时间复杂度:partition的时间复杂度为O(n),qsort在理想状态下时间复杂度为O(logn),最坏情况下(取决于枢纽值得选取),递归深度为n,时间复杂度为O(n),所以综合下来,平均时间复杂度为O(nlogn)。
- 空间复杂度:额外空间占用取决于递归栈得深度,所以空间复杂度为O(logn)~O(n)。
动图展示
可以参考https://visualgo.net/zh/sorting
排序算法对比
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n2) | O(n) | O(n2) | O(1) | 稳定 |
简单选择排序 | O(n2) | O(n2) | O(n2) | O(1) | 稳定 |
直接插入排序 | O(n2) | O(n) | O(n2) | O(1) | 稳定 |
希尔排序 | O(nlogn)~O(n2) | O(n1.3) | O(n2) | O(1) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
快速排序 | O(nlogn) | O(nlogn) | O(n2) | O(logn)~O(n) | 不稳定 |