一、排序算法的时间复杂度和空间复杂度

排序算法 平均时间复杂度 最坏时间复杂度 最好时间复杂度 空间复杂度 稳定性 冒泡排序 O(n²) O(n²) O(n) O(1) ✅ 直接选择排序 O(n²) O(n²) O(n²) O(1) ❌ 直接插入排序 O(n²) O(n²) O(n) O(1) ✅ 快速排序 O(nlogn) O(n²) O(nlogn) O(nlogn) ❌ 堆排序 O(nlogn) O(nlogn) O(nlogn) O(1) ❌ 归并排序 O(nlogn) O(nlogn) O(nlogn) O(n) ✅ 希尔排序 O(nlogn) O(n²)O(nlogn) O(1) ❌ 计数排序 O(n+k) O(n+k) O(n+k) O(n+k) ✅ 基数排序 O(N*M)  O(N*M) O(N*M) O(M) ✅

注:

1 归并排序可以通过手摇算法将空间复杂度降到O(1),但是时间复杂度会提高。

2 基数排序时间复杂度为O(N*M),其中N为数据个数,M为数据位数。

1.1 复杂度辅助记忆

冒泡、选择、直接 排序需要两个for循环,每次只关注一个元素,平均时间复杂度为O(n²))(一遍找元素O(n),一遍找位置O(n))快速、归并、希尔、堆基于二分思想,log以2为底,平均时间复杂度为O(nlogn)(一遍找元素O(n),一遍找位置O(logn))

1.2 稳定性辅助记忆

稳定性记忆-“快希选堆”(快牺牲稳定性) 排序算法的稳定性:排序前后相同元素的相对位置不变,则称排序算法是稳定的;否则排序算法是不稳定的。

二、理解时间复杂度

2.1 常数阶O(1)

int i = 1;

int j = 2;

++i;

j++;

int m = i + j;

 2.2 对数阶O(logN)

int i = 1;

while(i

{

i = i * 2;

}

2.3 线性阶O(n)

for(i=0; i<=n; i++)

{

System.out.println("hello");

}

2.4 线性对数阶O(n)

for(m=1; m

{

i = 1;

while(i

{

i = i * 2;

}

}

2.5 平方阶O(n)

for(x=1; i<=n; x++)

{

for(i=1; i<=n; i++)

{

System.out.println("hello");

}

}

2.6 K次方阶O(n)

for(i=0; i<=n; i++)

{

for(j=0; i<=n; i++)

{

for(k=0; i<=n; i++)

{

System.out.println("hello");

}

}

}

// k = 3 , n ^ 3

上面从上至下依次的时间复杂度越来越大,执行的效率越来越低。

三、空间复杂度

3.1 常数阶O(1) —— 原地排序

只用到 temp 这么一个辅助空间

原地排序算法,就是空间复杂度为O(1)的算法,不牵涉额外得到其他空间~

private static void swap(int[] nums, int i, int j) {

int temp = nums[i];

nums[i] = nums[j];

nums[j] = temp;

}

2.2 对数阶O(logN)

2.3 线性阶O(n)

int[] newArray = new int[nums.length];

for (int i = 0; i < nums.length; i++) {

newArray[i] = nums[i];

}

四、排序算法

4.1 冒泡排序

(思路:大的往后放)

4.1.1 代码

private static void bubbleSort(int[] nums) {

for (int i = 0; i < nums.length; i++) {

for (int j = 0; j < nums.length - 1 - i; j++) {

if (nums[j] > nums[j + 1]) {

swap(nums, j, j + 1);

}

}

}

}

4.1.2 复杂度

时间复杂度: N^2

空间复杂度:1

最佳时间复杂度:N^2  (因为就算你内部循环只对比,不交换元素,也是一样是N)

稳定性:稳定的 (大于的才换,小于等于的不交换)

// { 0,1,2,3,4}

private static void bubbleSort(int[] nums) {

for (int i = 0; i < nums.length; i++) {

boolean isChange = false;

for (int j = 0; j < nums.length - 1 - i; j++) {

if (nums[j] > nums[j + 1]) {

swap(nums, j, j + 1);

isChange = true;

}

}

if(!isChange){

return;

}

}

}

改进后的代码,最佳时间复杂度: N  (因为假如第一轮对比就没有任何元素交换,那么可以直接退出,也就是只有一次外循环)

4.2 选择排序

(思路:最小的放最前)

4.2.1 代码

private static void selectSort(int[] nums) {

for (int i = 0; i < nums.length; i++) {

int minIndex = i;

for (int j = i + 1; j < nums.length; j++) {

if (nums[j] < nums[minIndex]) {

minIndex = j;

}

}

swap(nums,minIndex,i);

}

}

4.2.2 复杂度

时间复杂度: N^2

空间复杂度:1

最佳时间复杂度:N^2  

稳定性:不稳定的 

4.3 直接插入排序

(思路:往排序好的数组中,找到合适的位置插进去)

4.3.1 代码

private static void insertSort(int[] nums) {

for (int i = 1; i < nums.length; i++) {

int temp = nums[i];

int j = i - 1;

for (; j >= 0 && temp < nums[j]; j--) {

nums[j + 1] = nums[j];

}

nums[j + 1] = temp;

}

}

4.3.2 复杂度

时间复杂度: N^2

空间复杂度:1

最佳时间复杂度:N  (因为你不进入内部循环。 [1,2,3,4,5])

稳定性:稳定的 

4.4 快速排序

(思路:利用数字target,把数组切成两边,左边比 target大,后边比 target小)

4.4.1 代码

/**

* 快速排序算法

* @param nums 待排序的数组

* @param beginIndex 排序起始索引

* @param endIndex 排序结束索引

*/

private static void quickSort(int[] nums, int beginIndex, int endIndex) {

if (beginIndex >= endIndex) {

return; // 递归终止条件:当开始索引大于等于结束索引时,表示已经完成排序

}

int mid = getMid(nums, beginIndex, endIndex); // 获取中间索引,用于分割数组

quickSort(nums, beginIndex, mid - 1); // 对中间索引左侧的数组进行快速排序

quickSort(nums, mid + 1, endIndex); // 对中间索引右侧的数组进行快速排序

}

/**

* 获取分区中的中间元素的索引

* @param nums 待排序的数组

* @param beginIndex 分区的起始索引

* @param endIndex 分区的结束索引

* @return 中间元素的索引

*/

private static int getMid(int[] nums, int beginIndex, int endIndex) {

int target = nums[beginIndex]; // 以数组的起始元素作为基准值

int left = beginIndex;

int right = endIndex;

boolean right2left = true; // 标识位,表示当前从右往左搜索

while (right > left) {

if (right2left) {

while (right > left && nums[right] > target) {

right--;

}

if (right > left) {

nums[left] = nums[right]; // 当右侧元素较大时,将右侧元素移到插入位置

right2left = false; // 切换为从左往右搜索

}

} else {

while (right > left && nums[left] < target) {

left++;

}

if (right > left) {

nums[right] = nums[left]; // 当左侧元素较小时,将左侧元素移到插入位置

right2left = true; // 切换为从右往左搜索

}

}

}

nums[left] = target; // 将基准值放入插入位置,完成一轮交换

return left;

}

4.4.2 复杂度

时间复杂度: N Log N (每个元素找到中间位置的,需要 LogN 时间,N个元素就是NLogN)

空间复杂度:N Log N (递归调用,需要栈空间)

最差时间复杂度:N ^ 2  ( 比如正序数组 [1,2,3,4,5] )

稳定性:不稳定的 

4.4.3 相关面试题

1、奇偶分割

给你一个数组,奇数放左边,偶数放右边,不要求排序

public static void main(String[] args) {

// 题目,把奇数放左边,偶尔放右边

// 思路:快速排序,交换元素

int[] array = {13, 100, 17, 12, 25, 0,1,6};

quickSort(array, 0, array.length - 1);

System.out.println(JSON.toJSONString(array));

}

private static void quickSort(int[] array, int beginIndex, int endIndex) {

if (beginIndex >= endIndex) {

return;

}

int mid = getMid(array, beginIndex, endIndex);

}

private static int getMid(int[] array, int beginIndex, int endIndex) {

int left = beginIndex;

int right = endIndex;

int temp = array[beginIndex];

boolean right2Left = true;

while (left < right) {

if (right2Left) {

// 如果是偶数

if (array[right] % 2 == 0) {

right--;

} else {

array[left++] = array[right];

right2Left = false;

}

} else {

// 如果是奇数

if (array[left] % 2 != 0) {

left++;

} else {

array[right--] = array[left];

right2Left = true;

}

}

}

array[left] = temp;

return left;

}

这个方法的时间复杂度是O(N),空间复杂度是 O(1),不稳定

下面这个方法的时间复杂度是O(N),空间复杂度是 O(N),稳定

public static void main(String[] args) {

// 题目,把奇数放左边,偶尔放右边

// 思路:快速排序,交换元素

int[] array = {13, 100, 17, 12, 25, 0, 1, 6, 5, 5};

int[] arrayNew = new int[array.length];

int oddCount = 0;

for (int i = 0; i < array.length; i++) {

if (array[i] % 2 != 0) {

oddCount++;

}

}

System.out.println(oddCount);

// 奇数的下标

int oddIndex = 0;

// 偶数的下标

int evenIndex = oddCount;

for (int i = 0; i < array.length; i++) {

if (array[i] % 2 != 0) {

arrayNew[oddIndex++] = array[i];

}else {

arrayNew[evenIndex++] = array[i];

}

}

System.out.println(JSON.toJSONString(arrayNew));

}

4.5 堆排序

(思路:最大放上面,然后与最后元素交换,继续建堆)

4.5.1 代码

/**

* 堆排序算法

* @param nums 待排序的数组

* @param beginIndex 排序的起始索引

* @param endIndex 排序的结束索引

*/

private static void heapSort(int[] nums, int beginIndex, int endIndex) {

if (beginIndex >= endIndex) {

return; // 当开始索引大于等于结束索引时,排序完成

}

for (int i = endIndex; i >= beginIndex; i--) {

createHeap(nums, i); // 构建最大堆

swap(nums, 0, i); // 将最大元素移到数组末尾

}

}

/**

* 构建最大堆

* @param nums 待构建的数组

* @param endIndex 当前堆的结束索引

*/

private static void createHeap(int[] nums, int endIndex) {

int lastFatherIndex = (endIndex - 1) / 2;

for (int i = lastFatherIndex; i >= 0; i--) {

int biggestIndex = i;

int leftChildIndex = i * 2 + 1;

int rightChildIndex = i * 2 + 2;

if (leftChildIndex <= endIndex) {

biggestIndex = nums[biggestIndex] > nums[leftChildIndex] ? biggestIndex : leftChildIndex;

}

if (rightChildIndex <= endIndex) {

biggestIndex = nums[biggestIndex] > nums[rightChildIndex] ? biggestIndex : rightChildIndex;

}

swap(nums, biggestIndex, i); // 调整堆,确保最大元素位于堆顶

}

}

/**

* 交换数组中两个元素的位置

* @param nums 数组

* @param i 索引1

* @param j 索引2

*/

private static void swap(int[] nums, int i, int j) {

int temp = nums[i];

nums[i] = nums[j];

nums[j] = temp;

}

4.5.2 复杂度

时间复杂度: N Log N (每个元素都要构建1次堆,需要 LogN 时间,N个元素就是NLogN,任何情况下都一样)

空间复杂度:1 (原地排序)

最差时间复杂度:N ^ 2  ( 比如正序数组 [1,2,3,4,5] )

稳定性:不稳定的 

4.6 归并排序

递归思路,左右两边排序好了,就已经排序好了

4.6.1 代码

// 归并排序的主方法

private static void mergeSort(int[] nums, int beginIndex, int endIndex) {

// 如果起始索引大于等于结束索引,表示只有一个元素或没有元素,不需要排序

if (beginIndex >= endIndex) {

return;

}

// 计算数组的中间索引

int mid = beginIndex + (endIndex - beginIndex) / 2;

// 递归排序左半部分

mergeSort(nums, beginIndex, mid);

// 递归排序右半部分

mergeSort(nums, mid + 1, endIndex);

// 合并左右两部分

merge(nums, beginIndex, mid, endIndex);

}

// 合并函数,用于将左右两部分合并成一个有序的数组

private static void merge(int[] nums, int beginIndex, int mid, int endIndex) {

int left = beginIndex;

int right = mid + 1;

int[] newArrays = new int[endIndex - beginIndex + 1];

int newArraysIndex = 0;

// 比较左右两部分的元素,将较小的元素放入新数组

while (left <= mid && right <= endIndex) {

newArrays[newArraysIndex++] = nums[left] <= nums[right] ? nums[left++] : nums[right++];

}

// 将剩余的左半部分元素复制到新数组

while (left <= mid) {

newArrays[newArraysIndex++] = nums[left++];

}

// 将剩余的右半部分元素复制到新数组

while (right <= endIndex) {

newArrays[newArraysIndex++] = nums[right++];

}

// 将合并后的新数组复制回原数组

for (int i = 0; i < newArrays.length; i++) {

nums[beginIndex + i] = newArrays[i];

}

}

4.6.2 复杂度

时间复杂度: N Log N (每个元素都要递归,需要 LogN 时间,N个元素就是NLogN,任何情况下都一样)

空间复杂度:N

稳定性:稳定的 

 4.7 希尔排序

思路:直接插入排序的升级版(分段式插入排序)

4.7.1 代码

private static void quickSort(int[] nums) {

// int gap = nums.length / 2;

// while (gap > 0) {

for (int i = 1; i < nums.length; i++) {

int temp = nums[i];

int j;

for (j = i - 1; j >= 0 && temp < nums[j]; j--) {

nums[j + 1] = nums[j];

}

nums[j + 1] = temp;

}

// gap = gap / 2;

// }

}

// 把上面的快速排序改成shell排序,只需要把间隔1 改成gap

private static void shellSort(int[] nums) {

int gap = nums.length / 2;

while (gap > 0) {

for (int i = gap; i < nums.length; i++) {

int temp = nums[i];

int j;

for (j = i - gap; j >= 0 && temp < nums[j]; j = j - gap) {

nums[j + gap] = nums[j];// 如果当前元素比待插入元素大,将当前元素向后移动

}

nums[j + gap] = temp; // 因为上边 j=j-gap退出的时候,j已经被剪掉1次了,可能小于0了

}

gap = gap / 2;

}

}

4.7.2 复杂度

时间复杂度: N Log N 

空间复杂度:1

稳定性:不稳定的 

 4.8 计数排序

前提:

1、数组规模不大,比如统计中国各个年龄的人数(因为年龄顶多就是0-150岁)

4.8.2 代码

public static void main(String[] args) {

int[] nums = {10, 18, 18, 18, 18, 18, 18, 20, 20, 20, 20, 2, 8, 3, 4, 1, 2, 3, 4};

int[] ageCount = new int[200];

for (int i = 0; i < nums.length; i++) {

ageCount[nums[i]]++;

}

for (int i = 1; i < ageCount.length; i++) {

int count = ageCount[i];

for (int j = 0; j < count; j++) {

System.out.print(i + ",");

}

}

}

4.8.2 复杂度

时间复杂度: N  (N是数组长度)

空间复杂度:K

稳定性:稳定的 

 4.9 基数排序

4.9.1 代码

如何取到指定位置的数字?

public static void main(String[] args) {

int number = 1234;

// 1234 如何取到个位数 4,1234 % 10 = 4 ,4/1 = 4

// 1234 如何取到十位数 3,1234 % 100 = 34 ,34/10 = 3

// 1234 如何取到百位数 2,1234 % 1000 = 234 ,234/100 = 2

// 1234 如何取到千位数 1,1234 % 10000 = 1234 ,234/1000 = 1

for (int i = 0; i < getDigitLength(number); i++) {

int num = (int) (number % Math.pow(10, i + 1) / Math.pow(10, i));

System.out.println(num);

}

}

// 1 就是1位数

// 10 就是2位数

// 100 就是3位数

// 1000 就是4位数

public static int getDigitLength(int number) {

int length = 1;

while (number / 10 > 0) { // 100

length++;

number = number / 10;

}

return length;

}

基数排序算法:

public static void main(String[] args) {

int[] array = {13, 100, 17, 12, 25};

Map> bucketMap = new HashMap<>();

for (int i = 0; i < 10; i++) {

bucketMap.put(i, new ArrayList<>());

}

int maxDigitLength = 0;

for (int i = 0; i < array.length; i++) {

maxDigitLength = getDigitLength(array[i]) > maxDigitLength ? getDigitLength(array[i]) : maxDigitLength;

}

for (int i = 0; i < maxDigitLength; i++) { // 从后往前取数值

for (int j = 0; j < array.length; j++) {

int iValue = (int) (array[j] % Math.pow(10, i + 1) / Math.pow(10, i));

bucketMap.get(iValue).add(array[j]); // 入桶

}

int arrayIndex = 0;

for (int j = 0; j < 10; j++) {

List bucketList = bucketMap.get(j);

for (Integer num: bucketList) {

array[arrayIndex++] = num; //倒出来

}

bucketMap.put(j,new ArrayList<>()); // 清空桶

}

System.out.println(JSON.toJSONString(array));

}

}

// 1 就是1位数

// 10 就是2位数

// 100 就是3位数

// 1000 就是4位数

public static int getDigitLength(int number) {

int length = 1;

while (number / 10 > 0) { // 100

length++;

number = number / 10;

}

return length;

}

4.9.2 复杂度

时间复杂度: N * d  (N是数组长度,d是最大位数,对于整数排序的话,d = log N 级别,因此整体是 N Log N)

空间复杂度:N

稳定性:稳定的 

精彩文章

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。