对比指标
-
时间复杂度
-
空间复杂度
-
稳定性
排序后,元素的相对位置有没有改变
-
是否原地排序
排序过程中不需要额外的辅助空间,只需要常数级的空间
排序方法 | 平均时间复杂度 | 最坏时间复杂度 | 最好时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
冒泡排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
快速排序 | O(n log n) | O(n^2) | O(n log n) | O(log n) | 不稳定 |
归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 |
堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 |
计数排序 | O(n + k) | O(n + k) | O(n + k) | O(k) | 稳定 |
桶排序 | O(n + k) | O(n^2) | O(n) | O(n + k) | 不稳定 |
基数排序 | O(n * k) | O(n * k) | O(n * k) | O(n + k) | 稳定 |
1.选择排序
- **排序思路:**正向遍历,在未排序的元素中子循环遍历寻找最小值,与未排序的第一个元素交换位置,直到排序完成结束遍历。(改进思路:同时寻找最大值,两边一起排序,但是时间复杂度仍然不变)
- 时间复杂度:O(n2)
- 空间复杂度:O(1)
- **稳定性:**不稳定,交换元素会导致原有数组元素位置变化。
- **是否原地排序:**无借助其他数组,是原地排序。
- 代码实现:
java
//选择排序
public void selectSort(int[] nums) {
//遍历元素
for (int i = 0; i < nums.length - 1; i++) {
//记录最小值
int min = i;
//遍历寻找未排序元素中最小值
for (int j = i + 1; j < nums.length; j++) {
if (nums[j] < nums[min]) {
min = j;
}
}
//交换最小值
int temp = nums[i];
nums[i] = nums[min];
nums[min] = temp;
}
}
2.冒泡排序
-
**排序思路:**逆向遍历,每次遍历都把逆序对(前元素大于后元素)交换位置,直到排序完成。
-
时间复杂度:O(n2)
-
空间复杂度:O(1)
-
**稳定性:**稳定,每次交换位置都只交换逆序对,不影响相等元素的相对位置。
-
**是否原地排序:**无借助其他数组,是原地排序。
-
代码实现:
java
//冒泡排序
void bubbleSort(int[] nums) {
//冒泡排序次数判断,一共需要排n-1次
int sorted = 0;
while(sorted < nums.length-1) {
//count用来记本次冒泡排序是否有数据交换,如果没有数据交换说明已经排序完成,直接返回就可以
int count = 0;
//开始冒泡,交换逆序列
for(int j = nums.length - 2; j >= 0; j--) {
if(nums[j] > nums[j+1]) {
int temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = temp;
count++;
}
}
if(count == 0) {
break;
}
sorted++;
}
}
3.插入排序
-
**排序思路:**在待排序的元素中,假设前n-1个元素已经排列完,将第n个元素插入到已经拍好的序列中,使得前n个元素有序。
-
时间复杂度:O(n2)
-
空间复杂度:O(1)
-
**稳定性:**稳定,每次排序交换都是整体移动,不影响元素相对位置。
-
**是否原地排序:**无借助其他数组,是原地排序。
-
代码实现:
java
//插入排序
void insertSort(int[] nums) {
//初始化认为第0号元素已经有序,所以从1开始遍历
for(int i = 1; i < nums.length; i++) {
//超前遍历,找到他的位置,整体后移
for(int j = i; j > 0; j--) {
if(nums[j] < nums[j-1]) {
int temp = nums[j];
nums[j] = nums[j-1];
nums[j-1] = temp;
}
else {
break;
}
}
}
}
4.希尔排序
- **排序思路:**插入排序的优化版,先选取一个gap进行划分元素,对每组元素分别进行插入排序,然后gap递减,使得元素变得相对有序,再进行插入排序。
- 时间复杂度:小于O(n2),但是具体不好算
- 空间复杂度:O(1)
- **稳定性:**不稳定,分组进行交换的时候有可能对数组元素的相对位置进行打乱。
- **是否原地排序:**无借助其他数组,是原地排序。
- 代码实现:
java
//希尔排序
void shellSort(int[] nums) {
//初始化gap,在gap为1之前都在分组排序
int gap = nums.length/4;
while(gap > 0) {
//与插入排序类似,分组排序
for(int i = 1; i < nums.length; i++) {
for(int j = i; j > 0; j=j-gap) {
if(j>=gap) {
if(nums[j] < nums[j-gap]) {
int temp = nums[j];
nums[j] = nums[j-gap];
nums[j-gap] = temp;
}
}
}
}
//逐渐减小gap
gap=gap/2;
}
}
5.快速排序
-
**排序思路:**先排序好一个节点,再递归的排序剩下的节点,使每个节点左边都是小于他的数,右边都是大于他的数。
-
时间复杂度:O(nlogn)
-
空间复杂度:O(n)
-
**稳定性:**不稳定,分组进行交换的时候有可能对数组元素的相对位置进行打乱。
-
**是否原地排序:**借助二叉树排序,不是原地排序。
-
代码实现:
-
快慢指针法
-
选取最左元素left作为待排序元素
- 利用fast指针遍历数组,如果该元素大于初始元素,就让slow+1,再交换slow指针对应元素和fast指针对应元素
- fast遍历完成数组后,slow元素位置就是该待排序位置的排序位置。
- 递归遍历该元素左右区间。
java//快速排序--快慢指针法 void quickSort(int[] nums, int left, int right) { //如果left==right 说明只包含1个元素,不需要排序 if(left >= right) { return; } //每次确定最左边的那个数 int key = nums[left]; int slow = left; int fast = left; //遍历数组,如果fast指针找到比key小的数字,就让slow+1,交换slow与fast的元素 while(fast <= right) { if(nums[fast] < key) { slow++; int temp = nums[slow]; nums[slow] = nums[fast]; nums[fast] = temp; } fast++; } //将初始数放在他的位置 int temp = nums[left]; nums[left] = nums[slow]; nums[slow] = temp; //递归遍历左区间和右区间 quickSort(nums, left, slow-1); quickSort(nums, slow+1, right); }
-
-
左右指针法
- 选取最左元素left为待排序元素。
- 设置begin和end来遍历元素,begin从左向右走,end从右向左走。
- end朝左走,若遇到比left小的元素,begin开始向右走,直到begin的元素比left大,交换begin元素值和end元素值。
- begin与end相交处就是该待排序元素的排序位置。
- 递归遍历该元素的左右区间
java//快速排序--左右指针法 void quickSort(int[] nums, int left, int right) { //如果left==right 说明只包含1个元素,不需要排序 if(left >= right) { return; } //每次确定最左边的那个数 int key = nums[left]; int begin = left; int end = right; //遍历左右指针 while(begin < end) { while(begin < end && nums[end] >= key) { end--; } while(begin < end && nums[begin] <= key) { begin++; } int temp = nums[begin]; nums[begin] = nums[end]; nums[end] = temp; } //将begin位置即初始元素排序后位置 int temp = nums[begin]; nums[begin] = nums[left]; nums[left] = temp; //递归遍历左区间和右区间 quickSort(nums, left, begin-1); quickSort(nums, begin+1, right); }
-
挖坑法
- 选取最左元素为待排序元素,存储在变量key中,最左元素为初始化坑位。
- 设置begin和end指针,分别指向最左元素和最右元素。
- end指针向左走,直到所指元素小于key,将end指针元素放坑位中,end指针处形成新坑位。
- begin指针开始向右走,直到所指元素大于key,将begin指针元素放到现在坑位中,begin指针处形成新坑位。
- 不断循环3,4,直到begin与end相遇,此时将key的值填入即可,左侧均小于key,右侧均大于key。
java//快速排序--挖坑法 void quickSort(int[] nums, int left, int right) { //如果left==right 说明只包含1个元素,不需要排序 if(left >= right) { return; } //每次确定最左边的那个数 int key = nums[left]; int begin = left; int end = right; //挖坑法确定key的位置 while(begin < end) { //end指针左移,第一个小于key的值为新坑 while(end > begin && nums[end] >= key) { end--; } //旧坑位填入值 nums[begin] = nums[end]; //begin指针右移,第一个大于key的值为新坑 while(begin < end && nums[begin] <= key) { begin++; } //旧坑位填入值 nums[end] = nums[begin]; } //当前坑位就是key的位置 nums[begin] = key; //递归遍历左区间和右区间 quickSort(nums, left, begin-1); quickSort(nums, begin+1, right); }
-
6.堆排序
-
堆的定义 :完全二叉树 + 数组顺序存储 (下表从1开始:父节点i:左孩子2i,右孩子2i+1)
大顶堆 :父节点大于等于子节点 小顶堆:父节点小于等于子节点
-
**排序思路:**选择排序的一种,升序用大顶堆,降序用小顶堆。首先将待排序的数组构造成一个大顶堆,此时整个数组的最大值就是根节点元素。将根节点与末尾交换,此时末尾为最大值,剩余待排序元素为n-1。将剩余n-1个元素再构造成大顶堆,再与第n-1的数交换,不断循环,直到排序完成。
-
时间复杂度:O(n+nlogn)
-
空间复杂度:O(1)
-
**稳定性:**不稳定,交换堆顶元素会导致原有数组元素位置变化。
-
**是否原地排序:**无借助其他数组,是原地排序。
-
代码实现:
-
建堆:倒着将每个节点为根的子树调整成堆,从最后一个非叶节点开始依次向下调整。如果父节点小于子节点,交换父子节点,继续遍历。
-
排序:每轮的堆顶都放在最后,向下调整新的堆顶。(n-1轮)
java//堆排序--大顶堆 public void heapSort(int[] nums) { int len = nums.length; //构建大顶堆 for(int i = len/2 - 1; i >= 0; i--) { heapAdjust(nums, i, len-1); } //调整排序 for(int i = 0; i < len-1; i++) { //每次都将根结点放在最后,调整堆 int temp = nums[0]; nums[0] = nums[len-i-1]; nums[len-i-1] = temp; heapAdjust(nums, 0, len-1-i-1); } } //向下调整堆 public void heapAdjust(int[] nums,int begin, int end) { int temp = nums[begin]; for(int i = 2*begin+1; i <= end; i = 2 * i +1) { //存在右孩子且右孩子大于左孩子,将i+1指向右孩子,保证i始终指向的是左右孩子中较大的那个 if(i<end && nums[i] <nums[i+1]) { i++; } //如果孩子节点大于父亲节点,交换孩子节点与父亲节点 if(nums[i] > temp) { nums[(i-1)/2] = nums[i]; nums[i] = temp; } } }
-
7.归并排序
-
**排序思路:**将数组拆分为长度为1的多个小数组,递归的将相邻两个数组组合,直至归并为已排序数组。
-
时间复杂度:O(nlogn)
-
空间复杂度:O(n)
-
**稳定性:**稳定,拆分数组以及数组合并都不改变相等元素的相对位置。
-
**是否原地排序:**借助其他数组,不是原地排序。
-
代码实现:
- 拆分数组为多个长度为1的子数组。
- 将相邻的子数组合并排序。
java//归并排序 public void mergeSort(int[] nums, int left, int right) { //将数组递归拆分并且合并 if (left < right) { //递归拆分数组 int mid = left + (right - left) / 2; mergeSort(nums, left, mid); mergeSort(nums, mid + 1, right); merge(nums, left, mid, right); } } //合并排序数组 public void merge(int[] nums, int left, int mid, int right) { //借助新数组进行帮助排序 int[] temp = new int[right - left + 1]; //i为左数组的起点,j为右数组的起点,k为新数组temp的起点 int i = left; int j = mid + 1; int k = 0; //比较左数组和右数组,谁小谁加入temp while (i <= mid && j <= right) { if (nums[i] <= nums[j]) { temp[k] = nums[i]; k++; i++; } else { temp[k] = nums[j]; k++; j++; } } //将左数组和右数组的剩余部分全部加入temp数组 while (i <= mid) { temp[k] = nums[i]; k++; i++; } while (j <= right) { temp[k] = nums[j]; k++; j++; } //将temp数组全部还原到原数组中 for (i = left, j = 0; i <= right; i++, j++) { nums[i] = temp[j]; } }
8.计数排序
-
**排序思路:**通过计数而不是比较进行排序,记录每个元素出现的次数,再依次填入
-
时间复杂度:O(n+k)
-
空间复杂度:O(n+k)
-
**稳定性:**不稳定,会打乱相同元素的相对位置。
-
**是否原地排序:**非原地排序
-
代码实现:
- 遍历数组,找到最大元素max,根据max的值创建计数数组来储存各个元素的值。
- 遍历数组,将该元素的次数填入计数数组。
- 根据计数数组,统计累计数组。
- 按照累计数组进行排序。
java//计数数组 public void countSort(int[] nums) { //遍历寻找最大值,确定计数数组的范围 int max = nums[0]; for(int i = 1; i < nums.length; i++) { if(max < nums[i]) { max = nums[i]; } } int[] count = new int[max+1]; //开始计数 for(int i = 0; i < nums.length; i++) { count[nums[i]]++; } //根据计数数组,计算累计数组 for(int i = 1; i < count.length; i++) { count[i] = count[i-1]+count[i]; } //根据累计数组进行计数排序,倒序遍历保证排序稳定性 int[] result = new int[nums.length]; for(int i = nums.length - 1; i >= 0; i--) { result[count[nums[i]]-1]=nums[i]; count[nums[i]]--; } //将排序后数组复制到原数组中 for(int i = 0; i < result.length; i++) { nums[i] = result[i]; } }
9.桶排序
- **排序思路:**将整个数组分为n个相等大小的子区间(桶),对每个桶内的数进行排序,然后直接遍历桶。
- 时间复杂度:O(n+k)
- 空间复杂度:O(n+k)
- **稳定性:**稳定,按顺序对相等元素进行操作,不改变相对位置。
- **是否原地排序:**否,借助多个子区间进行排序
- 代码实现:
- 设置定量数组作为桶的容量大小。
- 遍历数组,将数据放入对应的桶中。
- 对非空桶进行排序。
- 合并桶中数据。
java
//桶排序
public void bucketSort(int[] nums) {
//设置定量数组作为桶的容量大小。
int bucketSize = 10;
List<List<Integer>> buckets = new ArrayList<List<Integer>>(bucketSize);
for (int i = 0; i < bucketSize; i++) {
buckets.add(new ArrayList<>());
}
//遍历数组,将数据放入对应的桶中。
for (int i = 0; i < nums.length; i++) {
int index = nums[i] / bucketSize;
buckets.get(index).add(nums[i]);
}
//对非空桶进行排序。
for (int i = 0; i < bucketSize; i++) {
Collections.sort(buckets.get(i));
}
//按照桶的顺序进行赋值。
List<Integer> result = new ArrayList<>();
for (int i = 0; i < bucketSize; i++) {
for (int j = 0; j < buckets.get(i).size(); j++) {
result.add(buckets.get(i).get(j));
}
}
//复制List到int[]
for (int i = 0; i < result.size(); i++) {
nums[i] = result.get(i);
}
}
10.基数排序
- **排序思路:**通过逐步处理数字来进行排序,通过位运算比较每个数字的位值来排序。
- 时间复杂度:O(d(n + k))
- 空间复杂度:O(n+k)
- **稳定性:**稳定,因为每次排序都是按顺序进行位运算,不影响想等元素的相对位置。
- **是否原地排序:**利用额外数组存储位信息,不是原地排序。
- 代码实现:
- 计算最大位的长度,判断循环比较次数d。
- 从最低位到最高位依次对每一位进行计数排序。
java
//基数排序
public void radixSort(int[] nums) {
//计算最大值max,max的位数就是最高位
int max = nums[0];
for (int i = 1; i < nums.length; i++) {
if (max < nums[i]) {
max = nums[i];
}
}
//循环处理最高位数字,n/1%10
for (int exp = 1; max / exp > 0; exp = exp * 10) {
//计数排序
int[] count = new int[10];
for (int i = 0; i < nums.length; i++) {
count[nums[i] / exp % 10]++;
}
//计算累计数组
for (int i = 1; i < count.length; i++) {
count[i] = count[i - 1] + count[i];
}
//根据累计数组进行排序,注意原数组与位数字的对应关系
int[] result = new int[nums.length];
for (int i = result.length - 1; i >= 0; i--) {
result[count[nums[i] / exp % 10] - 1] = nums[i];
count[nums[i] / exp % 10]--;
}
//复制result到原数组中
for (int i = 0; i < result.length; i++) {
nums[i] = result[i];
}
}
}