排序算法 | 最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂的 | 空间复杂度 | 排序方式 | 稳定性 |
---|---|---|---|---|---|---|
快速排序 | O ( n log n ) O(n\log n) O(nlogn) | O ( n 2 ) O(n^2) O(n2) | O ( n log n ) O(n\log n) O(nlogn) | O ( log n ) O(\log n) O(logn) | 交换 | 不稳定 |
计数排序 | O ( k + n ) O(k + n) O(k+n) | O ( k + n ) O(k + n) O(k+n) | O ( k + n ) O(k + n) O(k+n) | O ( k + n ) O(k + n) O(k+n) | \ | 稳定 |
堆排序 | O ( n log n ) O(n\log n) O(nlogn) | O ( n log n ) O(n\log n) O(nlogn) | O ( n log n ) O(n\log n) O(nlogn) | O ( 1 ) O(1) O(1) | 选择 | 不稳定 |
归并排序 | O ( n log n ) O(n\log n) O(nlogn) | O ( n log n ) O(n\log n) O(nlogn) | O ( n log n ) O(n\log n) O(nlogn) | O ( n ) O(n) O(n) | \ | 稳定 |
希尔排序 | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( n 1.3 ) O(n^{1.3}) O(n1.3) | O ( 1 ) O(1) O(1) | 插入 | 不稳定 |
插入排序 | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 插入 | 稳定 |
冒泡排序 | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 交换 | 稳定 |
选择排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 选择 | 稳定 |
private static void swap(int nums, int i, int j){
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
原理:选择一个轴元素key,通过 i j ij ij指针定位元素,将待排序数组分为左侧小于等于key和右边大于等于key两组,递归操作。
时间复杂度:二分思想,故平均复杂度 O ( n log n ) O(n \log n) O(nlogn);但如果每次二分选择的轴都极不平均则更高,极端举例:只把元素分为1和 n − 1 n-1 n−1个,则出现需要划分n次,而每次都要遍历数组,因此最坏复杂度为 O ( n 2 ) O(n^2) O(n2)。例如完全正序,完全逆序都将出现最坏情况
空间复杂度:只有轴需要消耗空间,而二分法,每个部分都需要一个轴,因此空间为 O ( log n ) O(\log n) O(logn)
稳定性:根据图示可知,当 i j ij ij都定位到轴元素key时,将发生位置交换,因此不具有稳定性
参考代码
public static void quickSort(int[] nums, int l, int r){
if(l >= r) return;
int i = l - 1, j = r + 1; //先定位到边界两侧
int key = nums[l];
while(i < j){
while(nums[++i] < key); //先移动再与关键字判断
while(nums[--j] > key); //先移动在与关键字判断
if(i < j)
swap(nums, i, j); //交换两侧值
}
quickSort(nums, l, j);
quickSort(nums, j + 1, r);
}
public static void heapSort(int[] nums){
for (int i = (nums.length >>> 1) - 1; i >= 0; i--){
//建堆
siftDown(nums, i, nums.length);
}
for(int i = nums.length - 1; i > 0; i--){
//排序
swap(nums, 0, i);
siftDown(nums, 0, i);
}
}
private static void siftDown(int[] heap, int i, int len){
int curNum = heap[i];
int half = len >>> 1;
while (i < half){
//直到到没有子结点
int lcIdx = (i << 1) + 1; //左子结点索引
int rcIdx = lcIdx + 1; //右子结点索引
int temp = heap[lcIdx]; //选取左子结点作为临时值
if(rcIdx < len && temp < heap[rcIdx]){
temp = heap[lcIdx = rcIdx];
}
if (curNum >= temp)
break;
heap[i] = temp;
i = lcIdx; //下一层检测
}
heap[i] = curNum;
}
原理:对于已排好序的两个数组合并,只需要通过两个数组各自的指针 i j ij ij进行遍历,将比较结果放入新数组中,之后移动指针即可。归并排序通过上述理论,首先将数组两两划分,直到划分到长为1(有序),之后两两合并完成操作。
时间复杂度:典型分治,时间复杂度 O ( n log n ) O(n \log n) O(nlogn)
空间复杂度:合并时开辟一个存放合并结果的辅助数组,数组长度只需要和原数组长度一致即可,因此为 O ( n ) O(n) O(n)
稳定性:合并时不会影响次序,具有稳定性
参考代码
public void mergeSort(int nums, int l, int r){
if(l >= r) return;
int mid = (l + r) >> 1;
mergeSort(nums, l, mid);
mergeSort(nums. mid + 1, r);
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r){
if (nums[i] <= nums[j])
temp[k++] = nums[i++];
else
temp[k++] = nums[j++];
}
//temp是外部开好的数组
while (i <= mid) temp[k++] = nums[i++];
while (j <= r) temp[k++] = nums[j++];
for (i = l, j = 0; i <= r; i++, j++)
nums[i] = temp[j];
}
原理:一轮遍历可以找到一个数组的最小值,选择排序的方式就是每一轮都找到一个数组的最小值,是最容易理解的排序方法。
时间复杂度:需要 n / 2 n/2 n/2次遍历,每次遍历平均需要搜索 n / 2 n/2 n/2个元素,因此,时间复杂度 O ( n ) O(n) O(n)
空间复杂度:需要常数辅助空间作为比较元素的临时遍历
稳定性:每次选择都是从前往后选,因此可以是稳定的
参考代码
public static void selectSort(int nums){
for(int i = 0; i < nums.length; i++){
int idx = i;
for(int j = i; j < nums.length; j++){
if(nums[j] < nums[i])
idx = j;
}
swap(nums, i, idx);
}
}
原理:在排好顺中的序列中,可以通过遍历找到某个待插入正确位置使得新数组是有序的,因此,插入排序从前往后,依次将元素插入已排好序的数组中即可。(一个元素的数组可以看作有序数组)
时间复杂度:查找元素和插入平均都需要 n / 2 n/2 n/2次操作,因此,时间复杂度 O ( n 2 ) O(n^2) O(n2);若数组已经升序,则只需要进行 n − 1 n-1 n−1次比较,因此最好时间复杂度为 O ( n ) O(n) O(n),二分查找优化可降到 O ( log n ) O(\log n) O(logn)
空间复杂度:比较后插入时需要常数个辅助空间存放临时变量
稳定性:可以是稳定的,设置好比较方法使得相同时插入在后面即可
参考代码
public static void insertSort(int nums){
for(int i = 1; i < nums.length; i++){
int temp = nums[i];
for(int j = i; j > 0 && nums[j - 1] > temp; j--){
nums[j] = nums[j - 1];
}
nums[j] = temp;
}
}
public static void insertSort(int nums){
for(int i = 1; i < nums.length; i++){
int temp = nums[i];
int l = 0, r = i - 1;
while(l <= r){
int mid = (l + r)/2;
if(nums[mid] > temp) r = mid - 1;
else l = mid + 1;
}
for(int j = i; j > low; j--){
nums[j] = nums[j - 1];
}
nums[j] = temp;
}
}
原理:插入排序的改进办法,通过缩小增量的插入排序来降低时间复杂度,gap开始为数组的一半,每次减半,每轮都完成一次跨越gap的插入排序。
时间复杂度: O ( n 1.3 ) O(n^{1.3}) O(n1.3),较难证明,可以写作 O ( n log n ) O(n\log n) O(nlogn)
空间复杂度:同时间复杂度
稳定性:由于分组的存在,不稳定
参考代码
public static void shellSort(int[] nums){
int gap = nums.length;
while(true){
gap /= 2;
for(int i = 0; i < gap; i++){
temp = nums[i];
for(int j = i; j > 0 && temp > nums[j]; j -= gap){
nums[j] = nums[j - gap];
}
nums[i] = temp;
}
if(gap == 1) break;
}
}
原理:”车轮式的比武,每次决出胜者将参与下一次比武,直到选出最强者;每次选出剩余角色的最强者都需要进行一轮比武"
时间复杂度:由原理易知为 O ( n 2 ) O(n^2) O(n2)
空间复杂度:常数
稳定性:稳定
参考代码
public static void bubbleSort(int nums){
for(int i = 1; i < nums.length; i++){
boolean changed = false;
for(int j = 0; j < nums.length - i;j++){
if(nums[j] > nums[j + 1]){
swap(nums, j, j + 1);
changed = true;
}
}
if(changed == false) break;
}
}
public static void bubbleSort(int nums){
int l = 0, r = nums.length - 1, shift = 1;
while(l < r){
boolean changed = false;
for(int i = l; i < r; i++){
if(nums[i] > nums[i + 1]){
swap(nums, i, i + 1);
shift = i;
}
}
r = shift;
for(int i = r - 1; i >= l; i--){
if(nums[i] > nums[i + 1]){
swap(nums, i, i + 1);
shift = i + 1;
}
}
l = shift;
}
}
public static void countingSort(int nums){
int[] counter = new int[65535];
//此处的counter数组需要随情况变化
for(int i = 0; i < nums.length; i++){
counter[nums[i]]++;
}
int idx = 0;
for(int i = 0; i < counter.length; i++){
while(counter[i] > 0)
nums[idx++] = counter[i];
}
}
class Solution {
public void sortColors(int[] nums) {
int[] counter = new int[3];
for(int i = 0; i < nums.length; i++){
counter[nums[i]]++;
}
int idx = 0;
for(int i = 0; i < 3; i++){
while(counter[i] > 0){
nums[idx++] = i;
counter[i]--;
}
}
}
}
class Solution {
public String rankTeams(String[] votes) {
int len = votes[0].length();
int[][] map = new int[26][len + 1]; // 多1用于存放团队信息
for(int i = 0; i < 26; i++) map[i][len] = i;
for(int i = 0; i < votes.length; i++){
//投票统计
String s = votes[i];
for(int j = 0; j < len; j++){
map[s.charAt(j) - 'A'][j]++;
}
}
Arrays.sort(map, (a, b) ->{
//投票结果排序
for(int i = 0; i < len; i++){
if(a[i] < b[i]) return 1;
if(a[i] > b[i]) return -1;
}
return 0;
});
StringBuilder sb = new StringBuilder();
for(int i = 0; i < len; i++){
//获取结果对应团队
sb.append((char)('A' + map[i][len]));
}
return sb.toString();
}
}
class Solution {
public int carFleet(int target, int[] position, int[] speed) {
int n = position.length, res = 0;
double[][] cars = new double[n][2];
//分别记录开始位置和到达时间
for(int i = 0; i < n; i++){
cars[i][0] = position[i];
cars[i][1] = (double)(target - position[i])/speed[i];
}
//开始位置排序
Arrays.sort(cars, (a, b) -> Double.compare(a[0], b[0]));
double cur = 0;
for(int i = n - 1; i >= 0 ; i--){
//能否追上前车
if(cars[i][1] > cur){
cur = cars[i][1];
res++;
}
}
return res;
}
}