build
This commit is contained in:
parent
be77ba7a70
commit
31820449f5
@ -1373,9 +1373,9 @@ $$
|
||||
/* 平方阶(冒泡排序) */
|
||||
int bubbleSort(int[] nums) {
|
||||
int count = 0; // 计数器
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for (int i = nums.length - 1; i > 0; i--) {
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
@ -1396,9 +1396,9 @@ $$
|
||||
/* 平方阶(冒泡排序) */
|
||||
int bubbleSort(vector<int> &nums) {
|
||||
int count = 0; // 计数器
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for (int i = nums.size() - 1; i > 0; i--) {
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
@ -1419,9 +1419,9 @@ $$
|
||||
def bubble_sort(nums: list[int]) -> int:
|
||||
"""平方阶(冒泡排序)"""
|
||||
count = 0 # 计数器
|
||||
# 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
# 外循环:未排序区间为 [0, i]
|
||||
for i in range(len(nums) - 1, 0, -1):
|
||||
# 内循环:冒泡操作
|
||||
# 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for j in range(i):
|
||||
if nums[j] > nums[j + 1]:
|
||||
# 交换 nums[j] 与 nums[j + 1]
|
||||
@ -1438,9 +1438,9 @@ $$
|
||||
/* 平方阶(冒泡排序) */
|
||||
func bubbleSort(nums []int) int {
|
||||
count := 0 // 计数器
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for i := len(nums) - 1; i > 0; i-- {
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for j := 0; j < i; j++ {
|
||||
if nums[j] > nums[j+1] {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
@ -1461,9 +1461,9 @@ $$
|
||||
/* 平方阶(冒泡排序) */
|
||||
function bubbleSort(nums) {
|
||||
let count = 0; // 计数器
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for (let i = nums.length - 1; i > 0; i--) {
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for (let j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
@ -1484,9 +1484,9 @@ $$
|
||||
/* 平方阶(冒泡排序) */
|
||||
function bubbleSort(nums: number[]): number {
|
||||
let count = 0; // 计数器
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for (let i = nums.length - 1; i > 0; i--) {
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for (let j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
@ -1507,9 +1507,9 @@ $$
|
||||
/* 平方阶(冒泡排序) */
|
||||
int bubbleSort(int *nums, int n) {
|
||||
int count = 0; // 计数器
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for (int i = n - 1; i > 0; i--) {
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
@ -1530,9 +1530,9 @@ $$
|
||||
/* 平方阶(冒泡排序) */
|
||||
int bubbleSort(int[] nums) {
|
||||
int count = 0; // 计数器
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for (int i = nums.Length - 1; i > 0; i--) {
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
@ -1553,9 +1553,9 @@ $$
|
||||
/* 平方阶(冒泡排序) */
|
||||
func bubbleSort(nums: inout [Int]) -> Int {
|
||||
var count = 0 // 计数器
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for i in stride(from: nums.count - 1, to: 0, by: -1) {
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for j in 0 ..< i {
|
||||
if nums[j] > nums[j + 1] {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
@ -1576,11 +1576,11 @@ $$
|
||||
// 平方阶(冒泡排序)
|
||||
fn bubbleSort(nums: []i32) i32 {
|
||||
var count: i32 = 0; // 计数器
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
var i: i32 = @intCast(i32, nums.len ) - 1;
|
||||
while (i > 0) : (i -= 1) {
|
||||
var j: usize = 0;
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
while (j < i) : (j += 1) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
|
@ -2,7 +2,7 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 10.4. 搜索算法
|
||||
# 10.4. 重识搜索算法
|
||||
|
||||
「搜索算法 Searching Algorithm」用于在数据结构(例如数组、链表、树或图)中搜索一个或一组满足特定条件的元素。
|
||||
|
||||
|
@ -2,16 +2,14 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.2. 冒泡排序
|
||||
# 11.3. 冒泡排序
|
||||
|
||||
「冒泡排序 Bubble Sort」的工作原理类似于泡泡在水中的浮动。在水中,较大的泡泡会最先浮到水面。
|
||||
「冒泡排序 Bubble Sort」通过连续地比较与交换相邻元素实现排序。这个过程就像气泡从底部升到顶部一样,因此得名冒泡排序。
|
||||
|
||||
「冒泡操作」利用元素交换操作模拟了上述过程,具体做法为:从数组最左端开始向右遍历,依次比较相邻元素大小,如果“左元素 > 右元素”就交换它俩。遍历完成后,最大的元素会被移动到数组的最右端。
|
||||
|
||||
**在完成一次冒泡操作后,数组的最大元素已位于正确位置,接下来只需对剩余 $n - 1$ 个元素进行排序**。
|
||||
我们可以利用元素交换操作模拟上述过程:从数组最左端开始向右遍历,依次比较相邻元素大小,如果“左元素 > 右元素”就交换它俩。遍历完成后,最大的元素会被移动到数组的最右端。
|
||||
|
||||
=== "<1>"
|
||||

|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
@ -31,13 +29,14 @@ comments: true
|
||||
=== "<7>"
|
||||

|
||||
|
||||
## 11.2.1. 算法流程
|
||||
## 11.3.1. 算法流程
|
||||
|
||||
设输入数组长度为 $n$ ,整个冒泡排序的步骤为:
|
||||
设输入数组长度为 $n$ ,冒泡排序的步骤为:
|
||||
|
||||
1. 完成第一轮「冒泡」后,数组的最大元素已位于正确位置,接下来只需对剩余 $n - 1$ 个元素进行排序;
|
||||
2. 对剩余 $n - 1$ 个元素执行冒泡操作,可将第二大元素交换至正确位置,因而待排序元素只剩 $n - 2$ 个;
|
||||
3. 如此类推,经过 $n - 1$ 轮冒泡操作,整个数组便完成排序;
|
||||
1. 首先,对 $n$ 个元素执行“冒泡”,**将数组的最大元素交换至正确位置**,
|
||||
2. 接下来,对剩余 $n - 1$ 个元素执行“冒泡”,**将第二大元素交换至正确位置**。
|
||||
3. 以此类推,经过 $n - 1$ 轮“冒泡”后,**前 $n - 1$ 大的元素都被交换至正确位置**。
|
||||
4. 仅剩的一个元素必定是最小元素,无需排序,因此数组排序完成。
|
||||
|
||||

|
||||
|
||||
@ -48,9 +47,9 @@ comments: true
|
||||
```java title="bubble_sort.java"
|
||||
/* 冒泡排序 */
|
||||
void bubbleSort(int[] nums) {
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for (int i = nums.length - 1; i > 0; i--) {
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
@ -68,9 +67,9 @@ comments: true
|
||||
```cpp title="bubble_sort.cpp"
|
||||
/* 冒泡排序 */
|
||||
void bubbleSort(vector<int> &nums) {
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for (int i = nums.size() - 1; i > 0; i--) {
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
@ -88,9 +87,9 @@ comments: true
|
||||
def bubble_sort(nums: list[int]) -> None:
|
||||
"""冒泡排序"""
|
||||
n = len(nums)
|
||||
# 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
# 外循环:未排序区间为 [0, i]
|
||||
for i in range(n - 1, 0, -1):
|
||||
# 内循环:冒泡操作
|
||||
# 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for j in range(i):
|
||||
if nums[j] > nums[j + 1]:
|
||||
# 交换 nums[j] 与 nums[j + 1]
|
||||
@ -102,9 +101,9 @@ comments: true
|
||||
```go title="bubble_sort.go"
|
||||
/* 冒泡排序 */
|
||||
func bubbleSort(nums []int) {
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for i := len(nums) - 1; i > 0; i-- {
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for j := 0; j < i; j++ {
|
||||
if nums[j] > nums[j+1] {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
@ -120,9 +119,9 @@ comments: true
|
||||
```javascript title="bubble_sort.js"
|
||||
/* 冒泡排序 */
|
||||
function bubbleSort(nums) {
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for (let i = nums.length - 1; i > 0; i--) {
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for (let j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
@ -140,9 +139,9 @@ comments: true
|
||||
```typescript title="bubble_sort.ts"
|
||||
/* 冒泡排序 */
|
||||
function bubbleSort(nums: number[]): void {
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for (let i = nums.length - 1; i > 0; i--) {
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for (let j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
@ -160,9 +159,9 @@ comments: true
|
||||
```c title="bubble_sort.c"
|
||||
/* 冒泡排序 */
|
||||
void bubbleSort(int nums[], int size) {
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for (int i = 0; i < size - 1; i++) {
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for (int j = 0; j < size - 1 - i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
int temp = nums[j];
|
||||
@ -179,9 +178,9 @@ comments: true
|
||||
```csharp title="bubble_sort.cs"
|
||||
/* 冒泡排序 */
|
||||
void bubbleSort(int[] nums) {
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for (int i = nums.Length - 1; i > 0; i--) {
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
@ -199,9 +198,9 @@ comments: true
|
||||
```swift title="bubble_sort.swift"
|
||||
/* 冒泡排序 */
|
||||
func bubbleSort(nums: inout [Int]) {
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for i in stride(from: nums.count - 1, to: 0, by: -1) {
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for j in stride(from: 0, to: i, by: 1) {
|
||||
if nums[j] > nums[j + 1] {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
@ -219,11 +218,11 @@ comments: true
|
||||
```zig title="bubble_sort.zig"
|
||||
// 冒泡排序
|
||||
fn bubbleSort(nums: []i32) void {
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
var i: usize = nums.len - 1;
|
||||
while (i > 0) : (i -= 1) {
|
||||
var j: usize = 0;
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
while (j < i) : (j += 1) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
@ -236,17 +235,9 @@ comments: true
|
||||
}
|
||||
```
|
||||
|
||||
## 11.2.2. 算法特性
|
||||
## 11.3.2. 效率优化
|
||||
|
||||
**时间复杂度 $O(n^2)$** :各轮冒泡遍历的数组长度依次为 $n - 1$ , $n - 2$ , $\cdots$ , $2$ , $1$ ,总和为 $\frac{(n - 1) n}{2}$ ,因此使用 $O(n^2)$ 时间。在引入下文的 `flag` 优化后,最佳时间复杂度可达到 $O(n)$ ,所以它是“自适应排序”。
|
||||
|
||||
**空间复杂度 $O(1)$** :指针 $i$ , $j$ 使用常数大小的额外空间,因此是“原地排序”。
|
||||
|
||||
由于冒泡操作中遇到相等元素不交换,因此冒泡排序是“稳定排序”。
|
||||
|
||||
## 11.2.3. 效率优化
|
||||
|
||||
我们发现,如果某轮冒泡操作中没有执行任何交换操作,说明数组已经完成排序,可直接返回结果。因此,可以增加一个标志位 `flag` 来监测这种情况,一旦出现就立即返回。
|
||||
我们发现,如果某轮“冒泡”中没有执行任何交换操作,说明数组已经完成排序,可直接返回结果。因此,可以增加一个标志位 `flag` 来监测这种情况,一旦出现就立即返回。
|
||||
|
||||
经过优化,冒泡排序的最差和平均时间复杂度仍为 $O(n^2)$ ;但当输入数组完全有序时,可达到最佳时间复杂度 $O(n)$ 。
|
||||
|
||||
@ -255,10 +246,10 @@ comments: true
|
||||
```java title="bubble_sort.java"
|
||||
/* 冒泡排序(标志优化) */
|
||||
void bubbleSortWithFlag(int[] nums) {
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for (int i = nums.length - 1; i > 0; i--) {
|
||||
boolean flag = false; // 初始化标志位
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
@ -279,10 +270,10 @@ comments: true
|
||||
```cpp title="bubble_sort.cpp"
|
||||
/* 冒泡排序(标志优化)*/
|
||||
void bubbleSortWithFlag(vector<int> &nums) {
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for (int i = nums.size() - 1; i > 0; i--) {
|
||||
bool flag = false; // 初始化标志位
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
@ -303,10 +294,10 @@ comments: true
|
||||
def bubble_sort_with_flag(nums: list[int]) -> None:
|
||||
"""冒泡排序(标志优化)"""
|
||||
n = len(nums)
|
||||
# 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
# 外循环:未排序区间为 [0, i]
|
||||
for i in range(n - 1, 0, -1):
|
||||
flag = False # 初始化标志位
|
||||
# 内循环:冒泡操作
|
||||
# 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for j in range(i):
|
||||
if nums[j] > nums[j + 1]:
|
||||
# 交换 nums[j] 与 nums[j + 1]
|
||||
@ -321,10 +312,10 @@ comments: true
|
||||
```go title="bubble_sort.go"
|
||||
/* 冒泡排序(标志优化)*/
|
||||
func bubbleSortWithFlag(nums []int) {
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for i := len(nums) - 1; i > 0; i-- {
|
||||
flag := false // 初始化标志位
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for j := 0; j < i; j++ {
|
||||
if nums[j] > nums[j+1] {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
@ -344,10 +335,10 @@ comments: true
|
||||
```javascript title="bubble_sort.js"
|
||||
/* 冒泡排序(标志优化)*/
|
||||
function bubbleSortWithFlag(nums) {
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for (let i = nums.length - 1; i > 0; i--) {
|
||||
let flag = false; // 初始化标志位
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for (let j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
@ -367,10 +358,10 @@ comments: true
|
||||
```typescript title="bubble_sort.ts"
|
||||
/* 冒泡排序(标志优化)*/
|
||||
function bubbleSortWithFlag(nums: number[]): void {
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for (let i = nums.length - 1; i > 0; i--) {
|
||||
let flag = false; // 初始化标志位
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for (let j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
@ -390,10 +381,10 @@ comments: true
|
||||
```c title="bubble_sort.c"
|
||||
/* 冒泡排序(标志优化)*/
|
||||
void bubbleSortWithFlag(int nums[], int size) {
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for (int i = 0; i < size - 1; i++) {
|
||||
bool flag = false;
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for (int j = 0; j < size - 1 - i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
int temp = nums[j];
|
||||
@ -413,10 +404,10 @@ comments: true
|
||||
```csharp title="bubble_sort.cs"
|
||||
/* 冒泡排序(标志优化)*/
|
||||
void bubbleSortWithFlag(int[] nums) {
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for (int i = nums.Length - 1; i > 0; i--) {
|
||||
bool flag = false; // 初始化标志位
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
@ -436,7 +427,7 @@ comments: true
|
||||
```swift title="bubble_sort.swift"
|
||||
/* 冒泡排序(标志优化)*/
|
||||
func bubbleSortWithFlag(nums: inout [Int]) {
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for i in stride(from: nums.count - 1, to: 0, by: -1) {
|
||||
var flag = false // 初始化标志位
|
||||
for j in stride(from: 0, to: i, by: 1) {
|
||||
@ -460,12 +451,12 @@ comments: true
|
||||
```zig title="bubble_sort.zig"
|
||||
// 冒泡排序(标志优化)
|
||||
fn bubbleSortWithFlag(nums: []i32) void {
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
var i: usize = nums.len - 1;
|
||||
while (i > 0) : (i -= 1) {
|
||||
var flag = false; // 初始化标志位
|
||||
var j: usize = 0;
|
||||
// 内循环:冒泡操作
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
while (j < i) : (j += 1) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
@ -479,3 +470,9 @@ comments: true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 11.3.3. 算法特性
|
||||
|
||||
- **时间复杂度为 $O(n^2)$ 、自适应排序** :各轮“冒泡”遍历的数组长度依次为 $n - 1$ , $n - 2$ , $\cdots$ , $2$ , $1$ ,总和为 $\frac{(n - 1) n}{2}$ 。在引入 `flag` 优化后,最佳时间复杂度可达到 $O(n)$ 。
|
||||
- **空间复杂度为 $O(1)$ 、原地排序**:指针 $i$ , $j$ 使用常数大小的额外空间。
|
||||
- **稳定排序**:由于在“冒泡”中遇到相等元素不交换。
|
||||
|
@ -2,13 +2,13 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.6. 桶排序
|
||||
# 11.7. 桶排序
|
||||
|
||||
前述的几种排序算法都属于“基于比较的排序算法”,它们通过比较元素间的大小来实现排序。此类排序算法的时间复杂度无法超越 $O(n \log n)$ 。接下来,我们将探讨几种“非比较排序算法”,它们的时间复杂度可以达到线性水平。
|
||||
|
||||
「桶排序 Bucket Sort」是分治思想的一个典型应用。它通过设置一些具有大小顺序的桶,每个桶对应一个数据范围,将数据平均分配到各个桶中;然后,在每个桶内部分别执行排序;最终按照桶的顺序将所有数据合并。
|
||||
|
||||
## 11.6.1. 算法流程
|
||||
## 11.7.1. 算法流程
|
||||
|
||||
考虑一个长度为 $n$ 的数组,元素是范围 $[0, 1)$ 的浮点数。桶排序的流程如下:
|
||||
|
||||
@ -289,17 +289,14 @@ comments: true
|
||||
|
||||
桶排序适用于处理体量很大的数据。例如,输入数据包含 100 万个元素,由于空间限制,系统内存无法一次性加载所有数据。此时,可以将数据分成 1000 个桶,然后分别对每个桶进行排序,最后将结果合并。
|
||||
|
||||
## 11.6.2. 算法特性
|
||||
## 11.7.2. 算法特性
|
||||
|
||||
**时间复杂度 $O(n + k)$** :假设元素在各个桶内平均分布,那么每个桶内的元素数量为 $\frac{n}{k}$ 。假设排序单个桶使用 $O(\frac{n}{k} \log\frac{n}{k})$ 时间,则排序所有桶使用 $O(n \log\frac{n}{k})$ 时间。**当桶数量 $k$ 比较大时,时间复杂度则趋向于 $O(n)$** 。合并结果时需要遍历 $n$ 个桶,花费 $O(k)$ 时间。
|
||||
- **时间复杂度 $O(n + k)$** :假设元素在各个桶内平均分布,那么每个桶内的元素数量为 $\frac{n}{k}$ 。假设排序单个桶使用 $O(\frac{n}{k} \log\frac{n}{k})$ 时间,则排序所有桶使用 $O(n \log\frac{n}{k})$ 时间。**当桶数量 $k$ 比较大时,时间复杂度则趋向于 $O(n)$** 。合并结果时需要遍历 $n$ 个桶,花费 $O(k)$ 时间。
|
||||
- **自适应排序**:在最坏情况下,所有数据被分配到一个桶中,且排序该桶使用 $O(n^2)$ 时间。
|
||||
- **空间复杂度 $O(n + k)$ 、非原地排序** :需要借助 $k$ 个桶和总共 $n$ 个元素的额外空间。
|
||||
- 桶排序是否稳定取决于排序桶内元素的算法是否稳定。
|
||||
|
||||
在最坏情况下,所有数据被分配到一个桶中,且排序该桶使用 $O(n^2)$ 时间,因此是“自适应排序”。
|
||||
|
||||
**空间复杂度 $O(n + k)$** :需要借助 $k$ 个桶和总共 $n$ 个元素的额外空间,属于“非原地排序”。
|
||||
|
||||
桶排序是否稳定取决于排序桶内元素的算法是否稳定。
|
||||
|
||||
## 11.6.3. 如何实现平均分配
|
||||
## 11.7.3. 如何实现平均分配
|
||||
|
||||
桶排序的时间复杂度理论上可以达到 $O(n)$ ,**关键在于将元素均匀分配到各个桶中**,因为实际数据往往不是均匀分布的。例如,我们想要将淘宝上的所有商品按价格范围平均分配到 10 个桶中,但商品价格分布不均,低于 100 元的非常多,高于 1000 元的非常少。若将价格区间平均划分为 10 份,各个桶中的商品数量差距会非常大。
|
||||
|
||||
|
@ -2,11 +2,11 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.7. 计数排序
|
||||
# 11.8. 计数排序
|
||||
|
||||
「计数排序 Counting Sort」通过统计元素数量来实现排序,通常应用于整数数组。
|
||||
|
||||
## 11.7.1. 简单实现
|
||||
## 11.8.1. 简单实现
|
||||
|
||||
先来看一个简单的例子。给定一个长度为 $n$ 的数组 `nums` ,其中的元素都是“非负整数”。计数排序的整体流程如下:
|
||||
|
||||
@ -269,7 +269,7 @@ comments: true
|
||||
|
||||
从桶排序的角度看,我们可以将计数排序中的计数数组 `counter` 的每个索引视为一个桶,将统计数量的过程看作是将各个元素分配到对应的桶中。本质上,计数排序是桶排序在整型数据下的一个特例。
|
||||
|
||||
## 11.7.2. 完整实现
|
||||
## 11.8.2. 完整实现
|
||||
|
||||
细心的同学可能发现,**如果输入数据是对象,上述步骤 `3.` 就失效了**。例如,输入数据是商品对象,我们想要按照商品价格(类的成员变量)对商品进行排序,而上述算法只能给出价格的排序结果。
|
||||
|
||||
@ -647,15 +647,13 @@ $$
|
||||
[class]{}-[func]{countingSort}
|
||||
```
|
||||
|
||||
## 11.7.3. 算法特性
|
||||
## 11.8.3. 算法特性
|
||||
|
||||
**时间复杂度 $O(n + m)$** :涉及遍历 `nums` 和遍历 `counter` ,都使用线性时间。一般情况下 $n \gg m$ ,时间复杂度趋于 $O(n)$ 。
|
||||
- **时间复杂度 $O(n + m)$** :涉及遍历 `nums` 和遍历 `counter` ,都使用线性时间。一般情况下 $n \gg m$ ,时间复杂度趋于 $O(n)$ 。
|
||||
- **空间复杂度 $O(n + m)$ 、非原地排序** :借助了长度分别为 $n$ 和 $m$ 的数组 `res` 和 `counter` 。
|
||||
- **稳定排序**:由于向 `res` 中填充元素的顺序是“从右向左”的,因此倒序遍历 `nums` 可以避免改变相等元素之间的相对位置,从而实现稳定排序。实际上,正序遍历 `nums` 也可以得到正确的排序结果,但结果是非稳定的。
|
||||
|
||||
**空间复杂度 $O(n + m)$** :借助了长度分别为 $n$ 和 $m$ 的数组 `res` 和 `counter` ,因此是“非原地排序”。
|
||||
|
||||
**稳定排序**:由于向 `res` 中填充元素的顺序是“从右向左”的,因此倒序遍历 `nums` 可以避免改变相等元素之间的相对位置,从而实现“稳定排序”。实际上,正序遍历 `nums` 也可以得到正确的排序结果,但结果是“非稳定”的。
|
||||
|
||||
## 11.7.4. 局限性
|
||||
## 11.8.4. 局限性
|
||||
|
||||
看到这里,你也许会觉得计数排序非常巧妙,仅通过统计数量就可以实现高效的排序工作。然而,使用计数排序的前置条件相对较为严格。
|
||||
|
||||
|
@ -2,23 +2,26 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.3. 插入排序
|
||||
# 11.4. 插入排序
|
||||
|
||||
「插入排序 Insertion Sort」是一种基于数组插入操作的排序算法。具体来说,选择一个待排序的元素作为基准值 `base` ,将 `base` 与其左侧已排序区间的元素逐一比较大小,并将其插入到正确的位置。
|
||||
「插入排序 Insertion Sort」是一种简单的排序算法,它的工作原理与手动整理一副牌的过程非常相似。
|
||||
|
||||
回顾数组插入操作,我们需要将从目标索引到 `base` 之间的所有元素向右移动一位,然后再将 `base` 赋值给目标索引。
|
||||
具体来说,我们在未排序区间选择一个基准元素,将该元素与其左侧已排序区间的元素逐一比较大小,并将该元素插入到正确的位置。
|
||||
|
||||
回忆数组的元素插入操作,设基准元素为 `base` ,我们需要将从目标索引到 `base` 之间的所有元素向右移动一位,然后再将 `base` 赋值给目标索引。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 单次插入操作 </p>
|
||||
|
||||
## 11.3.1. 算法流程
|
||||
## 11.4.1. 算法流程
|
||||
|
||||
插入排序的整体流程如下:
|
||||
|
||||
1. 首先,选取数组的第 2 个元素作为 `base` ,执行插入操作后,**数组的前 2 个元素已排序**。
|
||||
2. 接着,选取第 3 个元素作为 `base` ,执行插入操作后,**数组的前 3 个元素已排序**。
|
||||
3. 以此类推,在最后一轮中,选取数组尾元素作为 `base` ,执行插入操作后,**所有元素均已排序**。
|
||||
1. 初始状态下,数组的第 1 个元素已完成排序。
|
||||
2. 选取数组的第 2 个元素作为 `base` ,将其插入到正确位置后,**数组的前 2 个元素已排序**。
|
||||
3. 选取第 3 个元素作为 `base` ,将其插入到正确位置后,**数组的前 3 个元素已排序**。
|
||||
4. 以此类推,在最后一轮中,选取最后一个元素作为 `base` ,将其插入到正确位置后,**所有元素均已排序**。
|
||||
|
||||

|
||||
|
||||
@ -29,15 +32,15 @@ comments: true
|
||||
```java title="insertion_sort.java"
|
||||
/* 插入排序 */
|
||||
void insertionSort(int[] nums) {
|
||||
// 外循环:base = nums[1], nums[2], ..., nums[n-1]
|
||||
// 外循环:已排序元素数量为 1, 2, ..., n
|
||||
for (int i = 1; i < nums.length; i++) {
|
||||
int base = nums[i], j = i - 1;
|
||||
// 内循环:将 base 插入到左边的正确位置
|
||||
// 内循环:将 base 插入到已排序部分的正确位置
|
||||
while (j >= 0 && nums[j] > base) {
|
||||
nums[j + 1] = nums[j]; // 1. 将 nums[j] 向右移动一位
|
||||
nums[j + 1] = nums[j]; // 将 nums[j] 向右移动一位
|
||||
j--;
|
||||
}
|
||||
nums[j + 1] = base; // 2. 将 base 赋值到正确位置
|
||||
nums[j + 1] = base; // 将 base 赋值到正确位置
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -47,15 +50,15 @@ comments: true
|
||||
```cpp title="insertion_sort.cpp"
|
||||
/* 插入排序 */
|
||||
void insertionSort(vector<int> &nums) {
|
||||
// 外循环:base = nums[1], nums[2], ..., nums[n-1]
|
||||
// 外循环:已排序元素数量为 1, 2, ..., n
|
||||
for (int i = 1; i < nums.size(); i++) {
|
||||
int base = nums[i], j = i - 1;
|
||||
// 内循环:将 base 插入到左边的正确位置
|
||||
// 内循环:将 base 插入到已排序部分的正确位置
|
||||
while (j >= 0 && nums[j] > base) {
|
||||
nums[j + 1] = nums[j]; // 1. 将 nums[j] 向右移动一位
|
||||
nums[j + 1] = nums[j]; // 将 nums[j] 向右移动一位
|
||||
j--;
|
||||
}
|
||||
nums[j + 1] = base; // 2. 将 base 赋值到正确位置
|
||||
nums[j + 1] = base; // 将 base 赋值到正确位置
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -65,15 +68,15 @@ comments: true
|
||||
```python title="insertion_sort.py"
|
||||
def insertion_sort(nums: list[int]) -> None:
|
||||
"""插入排序"""
|
||||
# 外循环:base = nums[1], nums[2], ..., nums[n-1]
|
||||
# 外循环:已排序区间为 [0, i-1]
|
||||
for i in range(1, len(nums)):
|
||||
base = nums[i]
|
||||
j = i - 1
|
||||
# 内循环:将 base 插入到左边的正确位置
|
||||
# 内循环:将 base 插入到已排序区间 [0, i-1] 中的正确位置
|
||||
while j >= 0 and nums[j] > base:
|
||||
nums[j + 1] = nums[j] # 1. 将 nums[j] 向右移动一位
|
||||
nums[j + 1] = nums[j] # 将 nums[j] 向右移动一位
|
||||
j -= 1
|
||||
nums[j + 1] = base # 2. 将 base 赋值到正确位置
|
||||
nums[j + 1] = base # 将 base 赋值到正确位置
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
@ -81,16 +84,16 @@ comments: true
|
||||
```go title="insertion_sort.go"
|
||||
/* 插入排序 */
|
||||
func insertionSort(nums []int) {
|
||||
// 外循环:待排序元素数量为 n-1, n-2, ..., 1
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for i := 1; i < len(nums); i++ {
|
||||
base := nums[i]
|
||||
j := i - 1
|
||||
// 内循环:将 base 插入到左边的正确位置
|
||||
// 内循环:将 base 插入到已排序部分的正确位置
|
||||
for j >= 0 && nums[j] > base {
|
||||
nums[j+1] = nums[j] // 1. 将 nums[j] 向右移动一位
|
||||
nums[j+1] = nums[j] // 将 nums[j] 向右移动一位
|
||||
j--
|
||||
}
|
||||
nums[j+1] = base // 2. 将 base 赋值到正确位置
|
||||
nums[j+1] = base // 将 base 赋值到正确位置
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -100,16 +103,16 @@ comments: true
|
||||
```javascript title="insertion_sort.js"
|
||||
/* 插入排序 */
|
||||
function insertionSort(nums) {
|
||||
// 外循环:base = nums[1], nums[2], ..., nums[n-1]
|
||||
// 外循环:已排序元素数量为 1, 2, ..., n
|
||||
for (let i = 1; i < nums.length; i++) {
|
||||
let base = nums[i],
|
||||
j = i - 1;
|
||||
// 内循环:将 base 插入到左边的正确位置
|
||||
// 内循环:将 base 插入到已排序部分的正确位置
|
||||
while (j >= 0 && nums[j] > base) {
|
||||
nums[j + 1] = nums[j]; // 1. 将 nums[j] 向右移动一位
|
||||
nums[j + 1] = nums[j]; // 将 nums[j] 向右移动一位
|
||||
j--;
|
||||
}
|
||||
nums[j + 1] = base; // 2. 将 base 赋值到正确位置
|
||||
nums[j + 1] = base; // 将 base 赋值到正确位置
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -119,16 +122,16 @@ comments: true
|
||||
```typescript title="insertion_sort.ts"
|
||||
/* 插入排序 */
|
||||
function insertionSort(nums: number[]): void {
|
||||
// 外循环:base = nums[1], nums[2], ..., nums[n-1]
|
||||
// 外循环:已排序元素数量为 1, 2, ..., n
|
||||
for (let i = 1; i < nums.length; i++) {
|
||||
const base = nums[i];
|
||||
let j = i - 1;
|
||||
// 内循环:将 base 插入到左边的正确位置
|
||||
// 内循环:将 base 插入到已排序部分的正确位置
|
||||
while (j >= 0 && nums[j] > base) {
|
||||
nums[j + 1] = nums[j]; // 1. 将 nums[j] 向右移动一位
|
||||
nums[j + 1] = nums[j]; // 将 nums[j] 向右移动一位
|
||||
j--;
|
||||
}
|
||||
nums[j + 1] = base; // 2. 将 base 赋值到正确位置
|
||||
nums[j + 1] = base; // 将 base 赋值到正确位置
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -138,16 +141,16 @@ comments: true
|
||||
```c title="insertion_sort.c"
|
||||
/* 插入排序 */
|
||||
void insertionSort(int nums[], int size) {
|
||||
// 外循环:base = nums[1], nums[2], ..., nums[n-1]
|
||||
// 外循环:已排序元素数量为 1, 2, ..., n
|
||||
for (int i = 1; i < size; i++) {
|
||||
int base = nums[i], j = i - 1;
|
||||
// 内循环:将 base 插入到左边的正确位置
|
||||
// 内循环:将 base 插入到已排序部分的正确位置
|
||||
while (j >= 0 && nums[j] > base) {
|
||||
// 1. 将 nums[j] 向右移动一位
|
||||
// 将 nums[j] 向右移动一位
|
||||
nums[j + 1] = nums[j];
|
||||
j--;
|
||||
}
|
||||
// 2. 将 base 赋值到正确位置
|
||||
// 将 base 赋值到正确位置
|
||||
nums[j + 1] = base;
|
||||
}
|
||||
}
|
||||
@ -158,15 +161,15 @@ comments: true
|
||||
```csharp title="insertion_sort.cs"
|
||||
/* 插入排序 */
|
||||
void insertionSort(int[] nums) {
|
||||
// 外循环:base = nums[1], nums[2], ..., nums[n-1]
|
||||
// 外循环:已排序元素数量为 1, 2, ..., n
|
||||
for (int i = 1; i < nums.Length; i++) {
|
||||
int bas = nums[i], j = i - 1;
|
||||
// 内循环:将 base 插入到左边的正确位置
|
||||
// 内循环:将 base 插入到已排序部分的正确位置
|
||||
while (j >= 0 && nums[j] > bas) {
|
||||
nums[j + 1] = nums[j]; // 1. 将 nums[j] 向右移动一位
|
||||
nums[j + 1] = nums[j]; // 将 nums[j] 向右移动一位
|
||||
j--;
|
||||
}
|
||||
nums[j + 1] = bas; // 2. 将 base 赋值到正确位置
|
||||
nums[j + 1] = bas; // 将 base 赋值到正确位置
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -176,16 +179,16 @@ comments: true
|
||||
```swift title="insertion_sort.swift"
|
||||
/* 插入排序 */
|
||||
func insertionSort(nums: inout [Int]) {
|
||||
// 外循环:base = nums[1], nums[2], ..., nums[n-1]
|
||||
// 外循环:已排序元素数量为 1, 2, ..., n
|
||||
for i in stride(from: 1, to: nums.count, by: 1) {
|
||||
let base = nums[i]
|
||||
var j = i - 1
|
||||
// 内循环:将 base 插入到左边的正确位置
|
||||
// 内循环:将 base 插入到已排序部分的正确位置
|
||||
while j >= 0, nums[j] > base {
|
||||
nums[j + 1] = nums[j] // 1. 将 nums[j] 向右移动一位
|
||||
nums[j + 1] = nums[j] // 将 nums[j] 向右移动一位
|
||||
j -= 1
|
||||
}
|
||||
nums[j + 1] = base // 2. 将 base 赋值到正确位置
|
||||
nums[j + 1] = base // 将 base 赋值到正确位置
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -195,38 +198,36 @@ comments: true
|
||||
```zig title="insertion_sort.zig"
|
||||
// 插入排序
|
||||
fn insertionSort(nums: []i32) void {
|
||||
// 外循环:base = nums[1], nums[2], ..., nums[n-1]
|
||||
// 外循环:已排序元素数量为 1, 2, ..., n
|
||||
var i: usize = 1;
|
||||
while (i < nums.len) : (i += 1) {
|
||||
var base = nums[i];
|
||||
var j: usize = i;
|
||||
// 内循环:将 base 插入到左边的正确位置
|
||||
// 内循环:将 base 插入到已排序部分的正确位置
|
||||
while (j >= 1 and nums[j - 1] > base) : (j -= 1) {
|
||||
nums[j] = nums[j - 1]; // 1. 将 nums[j] 向右移动一位
|
||||
nums[j] = nums[j - 1]; // 将 nums[j] 向右移动一位
|
||||
}
|
||||
nums[j] = base; // 2. 将 base 赋值到正确位置
|
||||
nums[j] = base; // 将 base 赋值到正确位置
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 11.3.2. 算法特性
|
||||
## 11.4.2. 算法特性
|
||||
|
||||
**时间复杂度 $O(n^2)$** :最差情况下,每次插入操作分别需要循环 $n - 1$ , $n-2$ , $\cdots$ , $2$ , $1$ 次,求和得到 $\frac{(n - 1) n}{2}$ ,因此时间复杂度为 $O(n^2)$ 。当输入数组完全有序时,插入排序达到最佳时间复杂度 $O(n)$ ,因此是“自适应排序”。
|
||||
- **时间复杂度 $O(n^2)$ 、自适应排序** :最差情况下,每次插入操作分别需要循环 $n - 1$ , $n-2$ , $\cdots$ , $2$ , $1$ 次,求和得到 $\frac{(n - 1) n}{2}$ ,因此时间复杂度为 $O(n^2)$ 。在遇到有序数据时,插入操作会提前终止。当输入数组完全有序时,插入排序达到最佳时间复杂度 $O(n)$ 。
|
||||
- **空间复杂度 $O(1)$ 、原地排序** :指针 $i$ , $j$ 使用常数大小的额外空间。
|
||||
- **稳定排序**:在插入操作过程中,我们会将元素插入到相等元素的右侧,不会改变它们的顺序。
|
||||
|
||||
**空间复杂度 $O(1)$** :指针 $i$ , $j$ 使用常数大小的额外空间,所以插入排序是“原地排序”。
|
||||
## 11.4.3. 插入排序优势
|
||||
|
||||
在插入操作过程中,我们会将元素插入到相等元素的右侧,不会改变它们的顺序,因此是“稳定排序”。
|
||||
插入排序的时间复杂度为 $O(n^2)$ ,而我们即将学习的快速排序的时间复杂度为 $O(n \log n)$ 。尽管插入排序的时间复杂度相比快速排序更高,**但在数据量较小的情况下,插入排序通常更快**。
|
||||
|
||||
## 11.3.3. 插入排序优势
|
||||
这个结论与线性查找和二分查找的适用情况的结论类似。快速排序这类 $O(n \log n)$ 的算法属于基于分治的排序算法,往往包含更多单元计算操作。而在数据量较小时,$n^2$ 和 $n \log n$ 的数值比较接近,复杂度不占主导作用;每轮中的单元计算操作数量起到决定性因素。
|
||||
|
||||
回顾冒泡排序和插入排序的复杂度分析,两者的循环轮数都是 $\frac{(n - 1) n}{2}$ 。然而,它们之间存在以下差异:
|
||||
实际上,许多编程语言(例如 Java)的内置排序函数都采用了插入排序,大致思路为:对于长数组,采用基于分治的排序算法,例如快速排序;对于短数组,直接使用插入排序。
|
||||
|
||||
- 冒泡操作基于元素交换实现,需要借助一个临时变量,共涉及 3 个单元操作;
|
||||
- 插入操作基于元素赋值实现,仅需 1 个单元操作;
|
||||
虽然冒泡排序、选择排序和插入排序的时间复杂度都为 $O(n^2)$ ,但在实际情况中,**插入排序的使用频率显著高于冒泡排序和选择排序**。这是因为:
|
||||
|
||||
粗略估计下来,冒泡排序的计算开销约为插入排序的 3 倍,因此插入排序更受欢迎。实际上,许多编程语言(如 Java)的内置排序函数都采用了插入排序,大致思路为:
|
||||
|
||||
- 对于长数组,采用基于分治的排序算法,例如「快速排序」,时间复杂度为 $O(n \log n)$ ;
|
||||
- 对于短数组,直接使用「插入排序」,时间复杂度为 $O(n^2)$ ;
|
||||
|
||||
尽管插入排序的时间复杂度高于快速排序,**但在数据量较小的情况下,插入排序实际上更快**。这是因为在数据量较小时,复杂度中的常数项(即每轮中的单元操作数量)起主导作用。这个现象与「线性查找」和「二分查找」的情况相似。
|
||||
- 冒泡排序基于元素交换实现,需要借助一个临时变量,共涉及 3 个单元操作;插入排序基于元素赋值实现,仅需 1 个单元操作。因此,**冒泡排序的计算开销通常比插入排序更高**。
|
||||
- 选择排序在任何情况下的时间复杂度都为 $O(n^2)$ 。**如果给定一组部分有序的数据,插入排序通常比选择排序效率更高**。
|
||||
- 选择排序不稳定,无法应用于多级排序。
|
||||
|
@ -2,7 +2,7 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.5. 归并排序
|
||||
# 11.6. 归并排序
|
||||
|
||||
「归并排序 Merge Sort」基于分治思想实现排序,包含“划分”和“合并”两个阶段:
|
||||
|
||||
@ -13,7 +13,7 @@ comments: true
|
||||
|
||||
<p align="center"> Fig. 归并排序的划分与合并阶段 </p>
|
||||
|
||||
## 11.5.1. 算法流程
|
||||
## 11.6.1. 算法流程
|
||||
|
||||
“划分阶段”从顶至底递归地将数组从中点切为两个子数组,直至长度为 1 ;
|
||||
|
||||
@ -533,15 +533,13 @@ comments: true
|
||||
- **在阅读代码时,需要特别注意各个变量的含义**。`nums` 的待合并区间为 `[left, right]` ,但由于 `tmp` 仅复制了 `nums` 该区间的元素,因此 `tmp` 对应区间为 `[0, right - left]` 。
|
||||
- 在比较 `tmp[i]` 和 `tmp[j]` 的大小时,**还需考虑子数组遍历完成后的索引越界问题**,即 `i > leftEnd` 和 `j > rightEnd` 的情况。索引越界的优先级是最高的,如果左子数组已经被合并完了,那么不需要继续比较,直接合并右子数组元素即可。
|
||||
|
||||
## 11.5.2. 算法特性
|
||||
## 11.6.2. 算法特性
|
||||
|
||||
**时间复杂度 $O(n \log n)$** :划分产生高度为 $\log n$ 的递归树,每层合并的总操作数量为 $n$ ,因此总体时间复杂度为 $O(n \log n)$ 。
|
||||
- **时间复杂度 $O(n \log n)$ 、非自适应排序** :划分产生高度为 $\log n$ 的递归树,每层合并的总操作数量为 $n$ ,因此总体时间复杂度为 $O(n \log n)$ 。
|
||||
- **空间复杂度 $O(n)$ 、非原地排序** :递归深度为 $\log n$ ,使用 $O(\log n)$ 大小的栈帧空间。合并操作需要借助辅助数组实现,使用 $O(n)$ 大小的额外空间。
|
||||
- **稳定排序**:在合并过程中,相等元素的次序保持不变。
|
||||
|
||||
**空间复杂度 $O(n)$** :递归深度为 $\log n$ ,使用 $O(\log n)$ 大小的栈帧空间;合并操作需要借助辅助数组实现,使用 $O(n)$ 大小的额外空间;因此是“非原地排序”。
|
||||
|
||||
在合并过程中,相等元素的次序保持不变,因此归并排序是“稳定排序”。
|
||||
|
||||
## 11.5.3. 链表排序 *
|
||||
## 11.6.3. 链表排序 *
|
||||
|
||||
归并排序在排序链表时具有显著优势,空间复杂度可以优化至 $O(1)$ ,原因如下:
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.4. 快速排序
|
||||
# 11.5. 快速排序
|
||||
|
||||
「快速排序 Quick Sort」是一种基于分治思想的排序算法,运行高效,应用广泛。
|
||||
|
||||
@ -313,9 +313,9 @@ comments: true
|
||||
}
|
||||
```
|
||||
|
||||
## 11.4.1. 算法流程
|
||||
## 11.5.1. 算法流程
|
||||
|
||||
1. 首先,对原数组执行一次「哨兵划分」,得到待排序的左子数组和右子数组;
|
||||
1. 首先,对原数组执行一次「哨兵划分」,得到未排序的左子数组和右子数组;
|
||||
2. 然后,对左子数组和右子数组分别递归执行「哨兵划分」;
|
||||
3. 持续递归,直至子数组长度为 1 时终止,从而完成整个数组的排序;
|
||||
|
||||
@ -507,17 +507,13 @@ comments: true
|
||||
}
|
||||
```
|
||||
|
||||
## 11.4.2. 算法特性
|
||||
## 11.5.2. 算法特性
|
||||
|
||||
**时间复杂度 $O(n \log n)$** :在平均情况下,哨兵划分的递归层数为 $\log n$ ,每层中的总循环数为 $n$ ,总体使用 $O(n \log n)$ 时间。
|
||||
- **时间复杂度 $O(n \log n)$ 、自适应排序** :在平均情况下,哨兵划分的递归层数为 $\log n$ ,每层中的总循环数为 $n$ ,总体使用 $O(n \log n)$ 时间。在最差情况下,每轮哨兵划分操作都将长度为 $n$ 的数组划分为长度为 $0$ 和 $n - 1$ 的两个子数组,此时递归层数达到 $n$ 层,每层中的循环数为 $n$ ,总体使用 $O(n^2)$ 时间。
|
||||
- **空间复杂度 $O(n)$ 、原地排序** :在输入数组完全倒序的情况下,达到最差递归深度 $n$ ,使用 $O(n)$ 栈帧空间。排序操作是在原数组上进行的,未借助额外数组。
|
||||
- **非稳定排序**:在哨兵划分的最后一步,基准数可能会被交换至相等元素的右侧。
|
||||
|
||||
在最差情况下,每轮哨兵划分操作都将长度为 $n$ 的数组划分为长度为 $0$ 和 $n - 1$ 的两个子数组,此时递归层数达到 $n$ 层,每层中的循环数为 $n$ ,总体使用 $O(n^2)$ 时间;因此快速排序是“自适应排序”。
|
||||
|
||||
**空间复杂度 $O(n)$** :在输入数组完全倒序的情况下,达到最差递归深度 $n$ 。由于未使用辅助数组,因此算法是“原地排序”。
|
||||
|
||||
在哨兵划分的最后一步,基准数可能会被交换至相等元素的右侧,因此是“非稳定排序”。
|
||||
|
||||
## 11.4.3. 快排为什么快?
|
||||
## 11.5.3. 快排为什么快?
|
||||
|
||||
从名称上就能看出,快速排序在效率方面应该具有一定的优势。尽管快速排序的平均时间复杂度与「归并排序」和「堆排序」相同,但通常快速排序的效率更高,原因如下:
|
||||
|
||||
@ -525,7 +521,7 @@ comments: true
|
||||
- **缓存使用效率高**:在执行哨兵划分操作时,系统可将整个子数组加载到缓存,因此访问元素的效率较高。而像「堆排序」这类算法需要跳跃式访问元素,从而缺乏这一特性。
|
||||
- **复杂度的常数系数低**:在上述三种算法中,快速排序的比较、赋值、交换等操作的总数量最少。这与「插入排序」比「冒泡排序」更快的原因类似。
|
||||
|
||||
## 11.4.4. 基准数优化
|
||||
## 11.5.4. 基准数优化
|
||||
|
||||
**快速排序在某些输入下的时间效率可能降低**。举一个极端例子,假设输入数组是完全倒序的,由于我们选择最左端元素作为基准数,那么在哨兵划分完成后,基准数被交换至数组最右端,导致左子数组长度为 $n - 1$ 、右子数组长度为 $0$ 。如此递归下去,每轮哨兵划分后的右子数组长度都为 $0$ ,分治策略失效,快速排序退化为「冒泡排序」。
|
||||
|
||||
@ -908,7 +904,7 @@ comments: true
|
||||
}
|
||||
```
|
||||
|
||||
## 11.4.5. 尾递归优化
|
||||
## 11.5.5. 尾递归优化
|
||||
|
||||
**在某些输入下,快速排序可能占用空间较多**。以完全倒序的输入数组为例,由于每轮哨兵划分后右子数组长度为 $0$ ,递归树的高度会达到 $n - 1$ ,此时需要占用 $O(n)$ 大小的栈帧空间。
|
||||
|
||||
@ -926,10 +922,10 @@ comments: true
|
||||
// 对两个子数组中较短的那个执行快排
|
||||
if (pivot - left < right - pivot) {
|
||||
quickSort(nums, left, pivot - 1); // 递归排序左子数组
|
||||
left = pivot + 1; // 剩余待排序区间为 [pivot + 1, right]
|
||||
left = pivot + 1; // 剩余未排序区间为 [pivot + 1, right]
|
||||
} else {
|
||||
quickSort(nums, pivot + 1, right); // 递归排序右子数组
|
||||
right = pivot - 1; // 剩余待排序区间为 [left, pivot - 1]
|
||||
right = pivot - 1; // 剩余未排序区间为 [left, pivot - 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -947,10 +943,10 @@ comments: true
|
||||
// 对两个子数组中较短的那个执行快排
|
||||
if (pivot - left < right - pivot) {
|
||||
quickSort(nums, left, pivot - 1); // 递归排序左子数组
|
||||
left = pivot + 1; // 剩余待排序区间为 [pivot + 1, right]
|
||||
left = pivot + 1; // 剩余未排序区间为 [pivot + 1, right]
|
||||
} else {
|
||||
quickSort(nums, pivot + 1, right); // 递归排序右子数组
|
||||
right = pivot - 1; // 剩余待排序区间为 [left, pivot - 1]
|
||||
right = pivot - 1; // 剩余未排序区间为 [left, pivot - 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -968,10 +964,10 @@ comments: true
|
||||
# 对两个子数组中较短的那个执行快排
|
||||
if pivot - left < right - pivot:
|
||||
self.quick_sort(nums, left, pivot - 1) # 递归排序左子数组
|
||||
left = pivot + 1 # 剩余待排序区间为 [pivot + 1, right]
|
||||
left = pivot + 1 # 剩余未排序区间为 [pivot + 1, right]
|
||||
else:
|
||||
self.quick_sort(nums, pivot + 1, right) # 递归排序右子数组
|
||||
right = pivot - 1 # 剩余待排序区间为 [left, pivot - 1]
|
||||
right = pivot - 1 # 剩余未排序区间为 [left, pivot - 1]
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
@ -986,10 +982,10 @@ comments: true
|
||||
// 对两个子数组中较短的那个执行快排
|
||||
if pivot-left < right-pivot {
|
||||
q.quickSort(nums, left, pivot-1) // 递归排序左子数组
|
||||
left = pivot + 1 // 剩余待排序区间为 [pivot + 1, right]
|
||||
left = pivot + 1 // 剩余未排序区间为 [pivot + 1, right]
|
||||
} else {
|
||||
q.quickSort(nums, pivot+1, right) // 递归排序右子数组
|
||||
right = pivot - 1 // 剩余待排序区间为 [left, pivot - 1]
|
||||
right = pivot - 1 // 剩余未排序区间为 [left, pivot - 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1007,10 +1003,10 @@ comments: true
|
||||
// 对两个子数组中较短的那个执行快排
|
||||
if (pivot - left < right - pivot) {
|
||||
this.quickSort(nums, left, pivot - 1); // 递归排序左子数组
|
||||
left = pivot + 1; // 剩余待排序区间为 [pivot + 1, right]
|
||||
left = pivot + 1; // 剩余未排序区间为 [pivot + 1, right]
|
||||
} else {
|
||||
this.quickSort(nums, pivot + 1, right); // 递归排序右子数组
|
||||
right = pivot - 1; // 剩余待排序区间为 [left, pivot - 1]
|
||||
right = pivot - 1; // 剩余未排序区间为 [left, pivot - 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1028,10 +1024,10 @@ comments: true
|
||||
// 对两个子数组中较短的那个执行快排
|
||||
if (pivot - left < right - pivot) {
|
||||
this.quickSort(nums, left, pivot - 1); // 递归排序左子数组
|
||||
left = pivot + 1; // 剩余待排序区间为 [pivot + 1, right]
|
||||
left = pivot + 1; // 剩余未排序区间为 [pivot + 1, right]
|
||||
} else {
|
||||
this.quickSort(nums, pivot + 1, right); // 递归排序右子数组
|
||||
right = pivot - 1; // 剩余待排序区间为 [left, pivot - 1]
|
||||
right = pivot - 1; // 剩余未排序区间为 [left, pivot - 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1050,10 +1046,10 @@ comments: true
|
||||
// 对两个子数组中较短的那个执行快排
|
||||
if (pivot - left < right - pivot) {
|
||||
quickSortTailCall(nums, left, pivot - 1); // 递归排序左子数组
|
||||
left = pivot + 1; // 剩余待排序区间为 [pivot + 1, right]
|
||||
left = pivot + 1; // 剩余未排序区间为 [pivot + 1, right]
|
||||
} else {
|
||||
quickSortTailCall(nums, pivot + 1, right); // 递归排序右子数组
|
||||
right = pivot - 1; // 剩余待排序区间为 [left, pivot - 1]
|
||||
right = pivot - 1; // 剩余未排序区间为 [left, pivot - 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1071,10 +1067,10 @@ comments: true
|
||||
// 对两个子数组中较短的那个执行快排
|
||||
if (pivot - left < right - pivot) {
|
||||
quickSort(nums, left, pivot - 1); // 递归排序左子数组
|
||||
left = pivot + 1; // 剩余待排序区间为 [pivot + 1, right]
|
||||
left = pivot + 1; // 剩余未排序区间为 [pivot + 1, right]
|
||||
} else {
|
||||
quickSort(nums, pivot + 1, right); // 递归排序右子数组
|
||||
right = pivot - 1; // 剩余待排序区间为 [left, pivot - 1]
|
||||
right = pivot - 1; // 剩余未排序区间为 [left, pivot - 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1094,10 +1090,10 @@ comments: true
|
||||
// 对两个子数组中较短的那个执行快排
|
||||
if (pivot - left) < (right - pivot) {
|
||||
quickSortTailCall(nums: &nums, left: left, right: pivot - 1) // 递归排序左子数组
|
||||
left = pivot + 1 // 剩余待排序区间为 [pivot + 1, right]
|
||||
left = pivot + 1 // 剩余未排序区间为 [pivot + 1, right]
|
||||
} else {
|
||||
quickSortTailCall(nums: &nums, left: pivot + 1, right: right) // 递归排序右子数组
|
||||
right = pivot - 1 // 剩余待排序区间为 [left, pivot - 1]
|
||||
right = pivot - 1 // 剩余未排序区间为 [left, pivot - 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1117,10 +1113,10 @@ comments: true
|
||||
// 对两个子数组中较短的那个执行快排
|
||||
if (pivot - left < right - pivot) {
|
||||
quickSort(nums, left, pivot - 1); // 递归排序左子数组
|
||||
left = pivot + 1; // 剩余待排序区间为 [pivot + 1, right]
|
||||
left = pivot + 1; // 剩余未排序区间为 [pivot + 1, right]
|
||||
} else {
|
||||
quickSort(nums, pivot + 1, right); // 递归排序右子数组
|
||||
right = pivot - 1; // 剩余待排序区间为 [left, pivot - 1]
|
||||
right = pivot - 1; // 剩余未排序区间为 [left, pivot - 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,13 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.8. 基数排序
|
||||
# 11.9. 基数排序
|
||||
|
||||
上一节我们介绍了计数排序,它适用于数据量 $n$ 较大但数据范围 $m$ 较小的情况。假设我们需要对 $n = 10^6$ 个学号进行排序,而学号是一个 $8$ 位数字,这意味着数据范围 $m = 10^8$ 非常大,使用计数排序需要分配大量内存空间,而基数排序可以避免这种情况。
|
||||
|
||||
「基数排序 Radix Sort」的核心思想与计数排序一致,也通过统计个数来实现排序。在此基础上,基数排序利用数字各位之间的递进关系,依次对每一位进行排序,从而得到最终的排序结果。
|
||||
|
||||
## 11.8.1. 算法流程
|
||||
## 11.9.1. 算法流程
|
||||
|
||||
以学号数据为例,假设数字的最低位是第 $1$ 位,最高位是第 $8$ 位,基数排序的步骤如下:
|
||||
|
||||
@ -587,10 +587,10 @@ $$
|
||||
|
||||
在连续的排序轮次中,后一轮排序会覆盖前一轮排序的结果。举例来说,如果第一轮排序结果 $a < b$ ,而第二轮排序结果 $a > b$ ,那么第二轮的结果将取代第一轮的结果。由于数字的高位优先级高于低位,我们应该先排序低位再排序高位。
|
||||
|
||||
## 11.8.2. 算法特性
|
||||
## 11.9.2. 算法特性
|
||||
|
||||
**时间复杂度 $O(nk)$** :设数据量为 $n$ 、数据为 $d$ 进制、最大位数为 $k$ ,则对某一位执行计数排序使用 $O(n + d)$ 时间,排序所有 $k$ 位使用 $O((n + d)k)$ 时间。通常情况下,$d$ 和 $k$ 都相对较小,时间复杂度趋向 $O(n)$ 。
|
||||
相较于计数排序,基数排序适用于数值范围较大的情况,**但前提是数据必须可以表示为固定位数的格式,且位数不能过大**。例如,浮点数不适合使用基数排序,因为其位数 $k$ 过大,可能导致时间复杂度 $O(nk) \gg O(n^2)$ 。
|
||||
|
||||
**空间复杂度 $O(n + d)$** :与计数排序相同,基数排序需要借助长度为 $n$ 和 $d$ 的数组 `res` 和 `counter` ,因此它是一种“非原地排序”。
|
||||
|
||||
基数排序与计数排序一样,都属于稳定排序。相较于计数排序,基数排序适用于数值范围较大的情况,**但前提是数据必须可以表示为固定位数的格式,且位数不能过大**。例如,浮点数不适合使用基数排序,因为其位数 $k$ 过大,可能导致时间复杂度 $O(nk) \gg O(n^2)$ 。
|
||||
- **时间复杂度 $O(nk)$** :设数据量为 $n$ 、数据为 $d$ 进制、最大位数为 $k$ ,则对某一位执行计数排序使用 $O(n + d)$ 时间,排序所有 $k$ 位使用 $O((n + d)k)$ 时间。通常情况下,$d$ 和 $k$ 都相对较小,时间复杂度趋向 $O(n)$ 。
|
||||
- **空间复杂度 $O(n + d)$ 、非原地排序** :与计数排序相同,基数排序需要借助长度为 $n$ 和 $d$ 的数组 `res` 和 `counter` 。
|
||||
- **稳定排序**:与计数排序相同。
|
||||
|
161
chapter_sorting/selection_sort.md
Normal file
161
chapter_sorting/selection_sort.md
Normal file
@ -0,0 +1,161 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.2. 选择排序
|
||||
|
||||
「选择排序 Insertion Sort」的工作原理非常直接:开启一个循环,每轮从未排序区间选择最小的元素,将其放到已排序区间的末尾。完整步骤如下:
|
||||
|
||||
1. 初始状态下,所有元素未排序,即未排序(索引)区间为 $[0, n-1]$ 。
|
||||
2. 选取区间 $[0, n-1]$ 中的最小元素,将其与索引 $0$ 处元素交换。完成后,数组前 1 个元素已排序。
|
||||
3. 选取区间 $[1, n-1]$ 中的最小元素,将其与索引 $1$ 处元素交换。完成后,数组前 2 个元素已排序。
|
||||
4. 以此类推。经过 $n - 1$ 轮选择与交换后,数组前 $n - 1$ 个元素已排序。
|
||||
5. 仅剩的一个元素必定是最大元素,无需排序,因此数组排序完成。
|
||||
|
||||
=== "<1>"
|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
=== "<7>"
|
||||

|
||||
|
||||
=== "<8>"
|
||||

|
||||
|
||||
=== "<9>"
|
||||

|
||||
|
||||
=== "<10>"
|
||||

|
||||
|
||||
=== "<11>"
|
||||

|
||||
|
||||
在代码中,我们用 $k$ 来记录未排序区间内的最小元素。
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="selection_sort.java"
|
||||
/* 选择排序 */
|
||||
void selectionSort(int[] nums) {
|
||||
int n = nums.length;
|
||||
// 外循环:未排序区间为 [i, n-1]
|
||||
for (int i = 0; i < n - 1; i++) {
|
||||
// 内循环:找到未排序区间 [i, n-1] 中的最小元素
|
||||
int k = i;
|
||||
for (int j = i + 1; j < n; j++) {
|
||||
if (nums[j] < nums[k]) {
|
||||
k = j; // 更新最小元素
|
||||
}
|
||||
}
|
||||
// 将该最小元素与未排序区间的首个元素交换
|
||||
int temp = nums[i];
|
||||
nums[i] = nums[k];
|
||||
nums[k] = temp;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="selection_sort.cpp"
|
||||
/* 选择排序 */
|
||||
void selectionSort(vector<int> &nums) {
|
||||
int n = nums.size();
|
||||
// 外循环:未排序区间为 [i, n-1]
|
||||
for (int i = 0; i < n - 1; i++) {
|
||||
// 内循环:找到未排序区间 [i, n-1] 中的最小元素
|
||||
int k = i;
|
||||
for (int j = i + 1; j < n; j++) {
|
||||
if (nums[j] < nums[k]) {
|
||||
k = j; // 更新最小元素
|
||||
}
|
||||
}
|
||||
// 将该最小元素与未排序区间的首个元素交换
|
||||
swap(nums[i], nums[k]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="selection_sort.py"
|
||||
def selection_sort(nums: list[int]):
|
||||
"""选择排序"""
|
||||
n = len(nums)
|
||||
# 外循环:未排序区间为 [i, n-1]
|
||||
for i in range(n - 1):
|
||||
# 内循环:找到未排序区间 [i, n-1] 中的最小元素
|
||||
k = i
|
||||
for j in range(i + 1, n):
|
||||
if nums[j] < nums[k]:
|
||||
k = j # 更新最小元素
|
||||
# 将该最小元素与未排序区间的首个元素交换
|
||||
nums[i], nums[k] = nums[k], nums[i]
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="selection_sort.go"
|
||||
[class]{}-[func]{selectionSort}
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
|
||||
```javascript title="selection_sort.js"
|
||||
[class]{}-[func]{selectionSort}
|
||||
```
|
||||
|
||||
=== "TypeScript"
|
||||
|
||||
```typescript title="selection_sort.ts"
|
||||
[class]{}-[func]{selectionSort}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="selection_sort.c"
|
||||
[class]{}-[func]{selectionSort}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="selection_sort.cs"
|
||||
[class]{selection_sort}-[func]{selectionSort}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="selection_sort.swift"
|
||||
[class]{}-[func]{selectionSort}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="selection_sort.zig"
|
||||
[class]{}-[func]{selectionSort}
|
||||
```
|
||||
|
||||
## 11.2.1. 算法特性
|
||||
|
||||
- **时间复杂度为 $O(n^2)$ 、非自适应排序**:共有 $n - 1$ 轮外循环,分别包含 $n$ , $n - 1$ , $\cdots$ , $2$ , $2$ 轮内循环,求和为 $\frac{(n - 1)(n + 2)}{2}$ 。
|
||||
- **空间复杂度 $O(1)$ 、原地排序**:指针 $i$ , $j$ 使用常数大小的额外空间。
|
||||
- **非稳定排序**:在交换元素时,有可能将 `nums[i]` 交换至其相等元素的右边,导致两者的相对顺序发生改变。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 选择排序非稳定示例 </p>
|
@ -2,10 +2,10 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.9. 小结
|
||||
# 11.10. 小结
|
||||
|
||||
- 冒泡排序通过交换相邻元素来实现排序。通过添加一个标志位来实现提前返回,我们可以将冒泡排序的最佳时间复杂度优化到 $O(n)$ 。
|
||||
- 插入排序每轮将待排序区间内的元素插入到已排序区间的正确位置,从而完成排序。虽然插入排序的时间复杂度为 $O(n^2)$ ,但由于单元操作相对较少,它在小数据量的排序任务中非常受欢迎。
|
||||
- 插入排序每轮将未排序区间内的元素插入到已排序区间的正确位置,从而完成排序。虽然插入排序的时间复杂度为 $O(n^2)$ ,但由于单元操作相对较少,它在小数据量的排序任务中非常受欢迎。
|
||||
- 快速排序基于哨兵划分操作实现排序。在哨兵划分中,有可能每次都选取到最差的基准数,导致时间复杂度劣化至 $O(n^2)$ 。引入中位数基准数或随机基准数可以降低这种劣化的概率。尾递归方法可以有效地减少递归深度,将空间复杂度优化到 $O(\log n)$ 。
|
||||
- 归并排序包括划分和合并两个阶段,典型地体现了分治策略。在归并排序中,排序数组需要创建辅助数组,空间复杂度为 $O(n)$ ;然而排序链表的空间复杂度可以优化至 $O(1)$ 。
|
||||
- 桶排序包含三个步骤:数据分桶、桶内排序和合并结果。它同样体现了分治策略,适用于数据体量很大的情况。桶排序的关键在于对数据进行平均分配。
|
||||
|
Loading…
Reference in New Issue
Block a user