【Day47】912. 排序数组【6 种排序】

文章目录

912.排序数组

题目:

给你一个整数数组 nums,请你将该数组升序排列

你必须在 不使用任何内置函数 的情况下解决问题,时间复杂度为 O(nlog(n)),并且空间复杂度尽可能小。

示例 1
输入 :nums = [5,2,3,1]
输出 :[1,2,3,5]
解释:数组排序后,某些数字的位置没有改变(例如,2 和 3),而其他数字的位置发生了改变(例如,1 和 5)。

示例 2
输入 :nums = [5,1,1,2,0,0]
输出 :[0,0,1,1,2,5]
解释:请注意,nums 的值不一定唯一。

提示

1 <= nums.length <= 5 * 10^4

-5 * 10^4 <= nums[i] <= 5 * 10^4


优先使用快速排序(随机 pivot)

  • 平均时间复杂度 O(n log n)
  • 空间复杂度 O(log n)
  • 原地排序,实际性能最好

需要稳定排序 → 归并排序

需要时间复杂度严格稳定(无最坏情况)→ 堆排序

插冒归 ------ 稳定
快堆选 ------ 不稳定


1. 冒泡排序

  1. 从头开始,两两相邻比较
  2. 前面比后面大,就交换(大的往后挪)
  3. 每一轮走完,最大的数会像气泡一样"冒"到最后面
  4. 重复这个过程,直到整个数组有序

荐(2)


go 复制代码
package main

import "fmt"

// 冒泡排序:对数组进行升序排序
func bubbleSort(nums []int) []int {
	n := len(nums)

	// 外层循环:控制排序的总轮数;n 个数字,只需要排 n-1 轮就够了
	for i := 0; i < n-1; i++ {
		// 内层循环:每一轮两两比较,把大的元素往后移;每一轮结束,最后面 i 个数字已经排好序了,不用再比
		for j := 0; j < n-i-1; j++ {
			// 升序:前一个元素 > 后一个元素 则交换
			// 降序:前一个元素 < 后一个元素 则交换
			if nums[j] > nums[j+1] {
				// 交换两个元素的位置
				nums[j], nums[j+1] = nums[j+1], nums[j]
			}
		}
	}

	// 返回排序完成的数组
	return nums
}

func main() {
	arr := []int{4, 2, 5, 1, 3}
	fmt.Println(bubbleSort(arr)) // 输出:[1 2 3 4 5]
}

时间复杂度:O(n²)

  • 两层循环
  • 最好、最坏、平均都是 O(n²)

空间复杂度:O(1)

  • 没有开辟新数组/切片
  • 只使用了临时变量交换
  • 原地排序

稳定性:稳定

稳定性 = 相等的元素,在排序后,前后顺序保持不变
冒泡排序可以通过增加一个标志位 判断某一轮是否发生交换 ,如果没有交换说明数组已经有序,可以提前结束

这个优化不会改变最坏时间复杂度,仍然是 O(n²),但可以将最好情况优化到 O(n),空间复杂度仍然是 O(1)。

go 复制代码
package main

import "fmt"

// bubbleSort 冒泡排序 优化版本
// 增加了交换标记,数组提前有序时可以直接退出,提升效率
func bubbleSort(nums []int) []int {
	n := len(nums)

	// 外层循环:控制排序轮数,最多执行 n-1 轮
	for i := 0; i < n-1; i++ {
		swapped := false // 标记本轮是否发生过交换

		// 内层循环:两两比较,将大元素向后交换
		// 每轮结束后,最后 i 个元素已确定位置,无需再比较
		for j := 0; j < n-i-1; j++ {
			// 升序排列:前一个 > 后一个 则交换
			if nums[j] > nums[j+1] {
				nums[j], nums[j+1] = nums[j+1], nums[j]
				swapped = true // 发生了交换,标记为 true
			}
		}

		// 如果本轮一次交换都没发生 → 数组已经完全有序
		// 直接跳出循环,不用继续排序
		if !swapped {
			break
		}
	}

	return nums
}

func main() {
	arr := []int{4, 2, 5, 1, 3}
	fmt.Println(bubbleSort(arr)) // 输出:[1 2 3 4 5]
}

2. 简单选择排序

  1. 一开始,整个数组都是未排序的
  2. 从第一个位置开始,认为当前位置是要放"最小值"的位置
  3. 在当前位置后面的所有元素中,找到最小值
  4. 把这个最小值交换到当前位置
  5. 然后移动到下一个位置重复这个过程
  6. 直到所有位置都处理完,数组就有序了

go 复制代码
package main

import "fmt"

// 选择排序:对数组进行升序排序
func selectionSort(nums []int) []int {
	n := len(nums)

	// 外层循环:控制每一轮要放"最小值"的位置
	// 外层循环:i 走到 n-1 就够了,最后一个元素自动有序
	for i := 0; i < n - 1; i++ {
		// 先假设当前位置是最小值下标
		minIndex := i

		// 内层循环:在未排序区找到真正的最小值
		for j := i + 1; j < n; j++ {
			// 升序:找到更小的值就更新最小值下标
			// 降序:找到更大的值就更新最大值下标
			if nums[j] < nums[minIndex] {
				minIndex = j
			}
		}

		// 找到最小值后,与当前位置交换
		if minIndex != i { // 小优化:避免自己和自己交换
			nums[i], nums[minIndex] = nums[minIndex], nums[i]
		}
	}

	// 返回排序完成的数组
	return nums
}

func main() {
	arr := []int{4, 2, 5, 1, 3}
	fmt.Println(selectionSort(arr)) // 输出:[1 2 3 4 5]
}

时间复杂度:O(n²)

  • 两层循环
  • 最好、最坏、平均都是 O(n²)

空间复杂度:O(1)

  • 没有开辟新数组/切片
  • 只使用了临时变量交换
  • 原地排序

稳定性:不稳定


3. 插入排序

  1. 把数组分成左边有序区右边无序区
  2. 最开始第一个元素自己就是有序区,剩下的都是无序区
  3. 每次从无序区拿第一个元素,当成要插入的数
  4. 把这个数从后往前和有序区的元素比较
  5. 如果有序区的元素更大(升序),就把它往后挪一位
  6. 直到找到比它小的元素 ,或走到有序区开头
  7. 通过元素后移腾出位置,把要插入的数放到空出来的位置
  8. 重复直到无序区为空,数组就有序了

go 复制代码
package main

import "fmt"

// 插入排序:对数组进行升序排序
func insertionSort(nums []int) []int {
	n := len(nums)

	// 外层循环:从第二个元素开始(第一个默认有序)
	for i := 1; i < n; i++ {
		// 保存当前要插入的元素
		cur := nums[i]
		// 从 i 的前一个位置开始往前比较
		j := i - 1

		// 内层循环:往前遍历有序区,比 cur 大的元素往后挪
		// 升序:nums[j] > cur
		// 降序:nums[j] < cur
		for j >= 0 && nums[j] > cur {
			nums[j+1] = nums[j] // 元素后移
			j--                 // 继续往前比较
		}

		// 循环结束,j+1 就是插入的正确位置
		nums[j+1] = cur
	}
	
	return nums
}

func main() {
	arr := []int{4, 2, 5, 1, 3}
	fmt.Println(insertionSort(arr)) // 输出:[1 2 3 4 5]
}

时间复杂度:O(n²)

  • 两层循环
  • 最坏/平均:O(n²)
  • 最好(数组已经有序):O(n)

空间复杂度:O(1)

  • 原地排序
  • 只使用了临时变量
  • 没有开辟新数组

稳定性:稳定


4. 快速排序

  1. 选定一个基准值 pivot(固定选区间第一个元素)
  2. 使用左右双指针在当前区间内遍历
  3. 右指针向左找:找到小于基准的元素停下
  4. 左指针向右找:找到大于基准的元素停下
  5. 交换左右指针指向的元素,让小的靠左、大的靠右
  6. 双指针相遇时,将基准值交换到指针位置(基准永久就位)
  7. 递归处理基准左侧区间右侧区间
  8. 递归终止:区间长度 ≤1 时天然有序
  9. 随机基准:随机选一个元素当 pivot,避免有序数组退化

go 复制代码
package main

import (
	"fmt
	// "math/rand"  // 随机基准需要打开:生成随机数
	// "time"      // 随机基准需要打开:设置随机种子
)

// 格式:func quickSort(nums []int) []int
// 原地排序 + 双指针交换法 + 固定基准(默认)
func quickSort(nums []int) []int {

	// ==================== 关键 Go 语法讲解 ====================
	// 第一步:先声明函数变量 sort
	// 作用:告诉编译器"有一个叫 sort 的函数",为递归做准备
	// 因为 Go 中函数不能直接在定义时调用自己,必须先声明再赋值
	// ==========================================================
	var sort func(arr []int, left int, right int)

	// ==================== 关键 Go 语法讲解 ====================
	// 第二步:给 sort 变量赋值,真正实现函数逻辑
	// 此时函数已经声明过了,内部可以安全递归调用 sort
	// ==========================================================
	sort = func(arr []int, left int, right int) {
		// 递归终止条件:区间长度 <= 1,天然有序,直接返回
		if left >= right {
			return
		}

		// ===================== 基准选择 =====================
		// 版本1:固定基准
		pivot := arr[left]

		// 版本2:随机基准(加分项,避免有序数组退化)
		// 打开注释即可使用,记得同时打开上方 import 包
		/*
			rand.Seed(time.Now().UnixNano())                // 初始化随机种子
			randomIdx := left + rand.Intn(right-left+1)     // 在 [left, right] 随机选下标
			arr[left], arr[randomIdx] = arr[randomIdx], arr[left] // 交换到最左边
			pivot = arr[left]                              // 更新基准
		*/
		// ====================================================

		// 初始化左右双指针
		i, j := left, right

		// 双指针分区核心逻辑
		for i < j {
			// 右指针向左找:第一个 小于 pivot 的元素
			for i < j && arr[j] >= pivot {
				j--
			}
			// 左指针向右找:第一个 大于 pivot 的元素
			for i < j && arr[i] <= pivot {
				i++
			}
			// 指针未相遇 → 交换
			if i < j {
				arr[i], arr[j] = arr[j], arr[i]
			}
		}

		// 指针相遇,将基准放到最终正确位置
		// 循环结束:i 和 j 已经相遇(i == j)
		// 相遇位置 i 就是基准 pivot 的最终正确位置
		// 因为基准取自 arr[left],所以将基准与相遇位置交换
		arr[left], arr[i] = arr[i], arr[left]

		// 递归排序左区间
		sort(arr, left, i-1)
		// 递归排序右区间
		sort(arr, i+1, right)
	}

	// 启动递归,对整个数组排序
	sort(nums, 0, len(nums)-1)

	// 返回排序后的数组
	return nums
}

func main() {
	// 测试用例 1
	nums1 := []int{5, 2, 3, 1}
	fmt.Println(quickSort(nums1)) // 输出: [1 2 3 5]

	// 测试用例 2
	nums2 := []int{5, 1, 1, 2, 0, 0}
	fmt.Println(quickSort(nums2)) // 输出: [0 0 1 1 2 5]
}

时间复杂度 O(n log n)

固定基准

  • 平均:O(n log n)
    每次划分均匀,效率最高
  • 最坏:O(n²)
    数组完全有序/逆序,划分极度不平衡
  • 最好:O(n log n)
    每次划分完美均匀

随机基准

  • 平均:O(n log n)
  • 最坏:O(n log n)
    最坏时间复杂度仍然是 O(n²),但通过随机选择 pivot,使这种情况出现的概率极低,因此期望时间复杂度为 O(n log n) 【降低"出现最坏情况的概率"】
  • 最好:O(n log n)

空间复杂度 O(log n)

  • 平均:O(log n)
    递归调用栈深度,平衡划分
  • 最坏:O(n)
    划分极度不平衡,递归退化为链表
  • 原地排序,无额外数组空间开销

稳定性:不稳定


5. 归并排序

  1. 采用分治法,把数组从中间分成左右两半
  2. 递归地把左右两半分别排好序
  3. 两个已经有序的子数组合并成一个大的有序数组
  4. 重复直到整个数组有序
  5. 归并排序永远是 O(n log n),不会退化
  6. 缺点是需要额外辅助空间
go 复制代码
package main

import "fmt"

// mergeSort 归并排序
func mergeSort(nums []int) []int {
	// 递归终止条件
	// 数组长度 <= 1,天然有序,直接返回
	if len(nums) <= 1 {
		return nums
	}

	// 1. 拆分:从中间分成左右两半
	mid := len(nums) / 2                  // 找到中间位置
	left := mergeSort(nums[:mid])         // 递归处理左半边
	right := mergeSort(nums[mid:])        // 递归处理右半边

	// 2. 合并:将两个有序的数组合并成一个有序数组
	return merge(left, right)
}

// merge 合并两个【已经有序】的数组 → 返回新的有序数组
func merge(left, right []int) []int {
	// 创建一个空的结果切片
    // 长度 0,容量预先分配为 左+右 总长度(避免自动扩容,更高效)
	result := make([]int, 0, len(left)+len(right))
	
	// 双指针:i 遍历 left,j 遍历 right
	i, j := 0, 0

	// 3. 双指针同时遍历,谁小就先把谁放进结果
	for i < len(left) && j < len(right) {
		if left[i] <= right[j] {
			result = append(result, left[i])
			i++ // 左指针后移
		} else {
			result = append(result, right[j])
			j++ // 右指针后移
		}
	}

	// 4. 处理 left 剩下的元素(如果有)
	// 此时 left 剩下的一定都比结果里的大
	for i < len(left) {
		result = append(result, left[i])
		i++
	}

	// 5. 处理 right 剩下的元素(如果有)
	// 此时 right 剩下的一定都比结果里的大
	for j < len(right) {
		result = append(result, right[j])
		j++
	}

	// 返回最终合并好的有序数组
	return result
}

func main() {
	nums := []int{5, 2, 3, 1}
	fmt.Println(mergeSort(nums)) // 输出: [1 2 3 5]
	
	nums2 := []int{5, 1, 1, 2, 0, 0}
	fmt.Println(mergeSort(nums2)) // 输出: [0 0 1 1 2 5]
}

时间复杂度 O(n log n)

  • 最好:O(n log n)
  • 最坏:O(n log n)
  • 平均:O(n log n)
  • 不会像快排那样退化,永远稳定高效

空间复杂度 O(n)

  • O(n)
  • 需要额外数组存储合并结果
  • 递归调用栈:O(log n)
  • 总空间:O(n)

稳定性:稳定


6. 堆排序

  1. 把数组变成一个大顶堆(根节点最大)
  2. 堆顶最大值 和数组最后一位交换 → 最大值就位
  3. 把剩下的元素重新调整成堆
  4. 重复以上步骤,直到整个数组有序
  5. 时间复杂度永远 O(n log n)原地排序,不需要额外空间
go 复制代码
package main

import "fmt"

// sortArray 堆排序对外接口
func sortArray(nums []int) []int {
    n := len(nums)

    // 1. 构建大顶堆:从最后一个非叶子节点向上调整
    for i := n/2 - 1; i >= 0; i-- {
        heapify(nums, n, i)
    }

    // 2. 一个个交换堆顶到末尾,并调整剩余元素
    for i := n - 1; i > 0; i-- {
        // 将堆顶最大值 与 当前数组末尾交换
        nums[0], nums[i] = nums[i], nums[0]
        // 交换后,剩余元素重新调整为大顶堆
        heapify(nums, i, 0)
    }

    return nums
}

// heapify 调整堆:让以 i 为根的子树变成大顶堆
// n:堆的长度,i:当前要调整的根节点
func heapify(nums []int, n int, i int) {
    largest := i     // 最大值初始化为根节点
    left := 2*i + 1  // 左孩子节点下标
    right := 2*i + 2 // 右孩子节点下标

    // 如果左孩子更大,更新最大值下标
    if left < n && nums[left] > nums[largest] {
        largest = left
    }

    // 如果右孩子更大,更新最大值下标
    if right < n && nums[right] > nums[largest] {
        largest = right
    }

    // 如果最大值不是根节点,说明需要交换
    if largest != i {
        nums[i], nums[largest] = nums[largest], nums[i]
        // 交换后,递归调整受影响的子树
        heapify(nums, n, largest)
    }
}

func main() {
    // 测试用例 1
    nums1 := []int{5, 2, 3, 1}
    fmt.Println(sortArray(nums1)) // 输出: [1 2 3 5]

    // 测试用例 2
    nums2 := []int{5, 1, 1, 2, 0, 0}
    fmt.Println(sortArray(nums2)) // 输出: [0 0 1 1 2 5]
}

时间复杂度 O(n log n)

建堆 O(n) + 每次调整堆 O(log n)

初始化建堆的时间复杂度为 O(n),建完堆以后需要进行 n−1 次调整,一次调整的时间复杂度为 O(logn),那么 n−1 次调整即需要 O(nlogn) 的时间复杂度。因此,总时间复杂度为 O(n+nlogn)=O(nlogn)。

空间复杂度 O(1)

原地排序,没有额外开辟数组(只需要常数的空间存放若干变量)

稳定性:不稳定


小顶堆

特点 :堆顶是整个堆里最小 的值
核心逻辑:快速获取当前堆的最小值

1. 海量数据找 最大的 K 个数

用途 :数据太大无法全加载内存,找最大 K 个
为什么用小顶堆

  • 我们要保留大的,淘汰小的
  • 小顶堆能O(1) 拿到堆里最小的那个
  • 只要新数 > 堆顶,就替换堆顶

我们要留大的,扔小的

那我就把当前最小的放堆顶

新来一个数,只要比最小的大,就把最小的踢走

最后剩下的,自然就是最大的 K 个

原理

  1. 维护一个大小固定为 K 的小顶堆
  2. 遍历所有数据
  3. 新数 > 堆顶 → 删堆顶,插入新数
  4. 最终堆里就是全局最大 K 个数

2. 最小优先队列

用途 :每次要取最小元素
原理

  • 小顶堆保证堆顶最小
  • 每次出队都是最小值,时间 O(logn)

想拿最小值 → 直接拿堆顶

拿走后,堆会自动调整,新堆顶又是新的最小值

永远能最快拿到最小

这就是 "最小优先队列"。


大顶堆

特点 :堆顶是整个堆里最大 的值
核心逻辑:快速获取当前堆的最大值

海量数据找 最小的 K 个数

用途 :有限内存,找最小 K 个
原理

  1. 维护大小为 K 的大顶堆
  2. 新数 < 堆顶 → 替换
  3. 最终堆里就是全局最小 K 个数
    为什么用大顶堆:快速拿到堆里最大的,方便淘汰大的,保留小的

相关推荐
We་ct1 小时前
LeetCode 33. 搜索旋转排序数组:O(log n)二分查找
前端·算法·leetcode·typescript·个人开发·二分·数组
重生之我是Java开发战士1 小时前
【优选算法】优先级队列:最后一块石头的重量,数据流中的第K大元素,前K个高频单词,数据流中的中位数
数据结构·算法·leetcode
无敌昊哥战神10 小时前
【LeetCode 257】二叉树的所有路径(回溯法/深度优先遍历)- Python/C/C++详细题解
c语言·c++·python·leetcode·深度优先
x_xbx11 小时前
LeetCode:148. 排序链表
算法·leetcode·链表
炽烈小老头12 小时前
【 每天学习一点算法 2026/03/23】数组中的第K个最大元素
学习·算法·排序算法
木井巳13 小时前
【递归算法】子集
java·算法·leetcode·决策树·深度优先
lightqjx13 小时前
【算法】二分算法
c++·算法·leetcode·二分算法·二分模板
灰色小旋风14 小时前
力扣21 合并两个有序链表(C++)
c++·leetcode·链表
JCGKS15 小时前
海量文档单词计数算法方案分析
golang·数据结构与算法·海量数据·搜索引起·倒排查找