Post

排序算法

总览

常见的排序算法一览表如下,复杂的不常见的还有希尔排序、计数排序、基数排序、桶排序就不看了。

排序算法时间复杂度(最好)时间复杂度(最差)时间复杂度(平均)空间复杂度稳定排序相关函数
冒泡排序$O(n)$$O(n^2)$$O(n^2)$$O(1)$ 
选择排序$O(n^2)$$O(n^2)$$O(n^2)$$O(1)$FindMinIndex
插入排序$O(n)$$O(n^2)$$O(n^2)$$O(1)$ 
快速排序   $O(nlogn)$Partition
堆排序   $O(1)$Heapify

冒泡排序

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void BubbleSort(vector<int> &nums) {
    int n = nums.size();
    bool exchange = false;
    // 我们要依次把n-1个最大的数冒泡到最右边有序部分,还剩一个数字已经是最小,就有序了
    for (int i = 0; i < n - 1; ++i) {
        // 遍历当前无序部分,即[0,n-1-i],因为要依次对比相邻数字,左边数字范围就是[0,n-1-i)
        for (int j = 0; j < n - i - 1; ++j) {
            if (nums[j] > nums[j + 1]) {
                swap(nums[j], nums[j + 1]);
                exchange = true;
            }
        }
        // 如果本轮冒泡,从0到最后一个无序数字,没发生过一次交换
        // 那说明这段数字,已经有序,非递减,因此整个数组已经有序,可以提前退出了
        if (!exchange)
            break;
    }
}

分析

选择排序

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void SelectSort(vector<int> &nums) {
    int n = nums.size();
    // 找到范围内最小元素的下标
    auto FindMinIndex = [] (vector<int> &nums, int low, int high) -> int {
        int minIndex = low;
        for (int i = low + 1; i <= high; ++i) {
            if (nums[i] < nums[minIndex])
                minIndex = i;
        }
        return minIndex;
    };
    for (int i = 0; i < n - 1; ++i) {
        int minIndex = FindMinIndex(nums, i, n - 1);  // i前面是有序部分,i以及i右边的元素是无序部分,从中找到最小元素的下标
        if (minIndex != i)
            swap(nums[i], nums[minIndex]);  // 如果最小元素不是i自己,那就交换一下,把本轮最小元素放到当前位置
    }
}

分析

插入排序

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void InsertSort(vector<int> &nums) {
    int n = nums.size();
    for (int j = 1; j < n; ++j) {
        // // 方法一:
        // int i = j - 1;
        // // 其实这个思路不太符合插入排序的思想,更像是冒泡排序
        // // 每次都是比较相邻元素,将元素j往左逐位冒泡到他应该在的位置
        // while (i >= 0 && nums[i] > nums[i + 1]) {
        //     swap(nums[i], nums[i + 1]);
        //     --i;
        // }

        // 方法二:
        int i = j - 1;
        int key = nums[j];  // 这里我们先记下当前数字,因为它最终要插入到前面,前面的元素要后移,会占用它当前位置
        while (i >= 0 && nums[i] > key) {
            // 比较完元素i后,先把它后移,再继续比较下一个元素
            nums[i + 1] = nums[i];
            --i;
        }
        // 最终,不管有没有到头,到头的话,i就是-1,最终key都插入到i+1位置
        nums[i + 1] = key;
    }
}

分析

快速排序

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int Partition(vector<int> &nums, int low, int high) {
    int pivot = nums[high];  // 先初始化选最右边的数字为目标
    int i = low - 1;  // i用来写数组,默认放在-1的位置,写元素的时候先++
    for (int j = low; j < high; ++j) {  // 遍历除了最后一个数字的其它数字
        // 如果当前元素比pivot大于等于,那么,先不管他,继续下一个
        // 如果当前元素比pivot小,那么把它和数组i位置的数字交换
        //  这一步相当于把比pivot小的数字放在前面,前面那个位置本来的元素,交换到后面
        //  这会导致比pivot大的元素,在遍历后面的元素时候,被交换到后面,顺序会乱掉
        //  但最终结果是不断把大的元素交换到后面
        if (nums[j] < pivot) {
            swap(nums[++i], nums[j]);
        }
    }
    // 直到交换到最后pivot的位置,把pivot放到i的位置
    //  这时候,pivot左边都是小的,pivot右边都是大的
    swap(nums[++i], nums[high]);
    return i;
};

void QuickSort(vector<int> &nums, int low, int high) {
    if (low < high) {
        // 找一个目标,比他小的放在左边,比他大的放在右边
        int pi = Partition(nums, low, high);
        // pi的位置就对了,然后再分别去快速排序它左边和右边
        QuickSort(nums, low, pi - 1);
        QuickSort(nums, pi + 1, high);
    }
}

void QuickSort(vector<int> &nums) {
    int n = nums.size();
    QuickSort(nums, 0, n - 1);
}

分析

堆排序

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
void Heapify1(vector<int>& nums, int i, int n) {  // 迭代版本
    while (i * 2 + 1 < n) {
        int left = i * 2 + 1;
        int right = i * 2 + 2;
        int max = i;
        if (left < n && nums[left] > nums[max])
            max = left;
        if (right < n && nums[right] > nums[max])
            max = right;
        // 这里需要注意,堆化一个节点,只需要将最大值和节点i交换即可
        // 因此,不需要先和左孩子对比交换,再和右孩子对比交换
        // 只需要找到三者的最大值,直接交换一次即可
        if (max != i) {
            swap(nums[i], nums[max]);
            i = max;  // 如果节点i和节点max交换了,那么节点max也需要重新堆化
        } else {
            break;  // 如果节点i本身就是最大值,那堆化就到此为止了,返回
        }
    }
}

void Heapify2(vector<int>& nums, int i, int n) {  // 递归版本
    int left = i * 2 + 1;
    int right = i * 2 + 2;
    int max = i;
    if (left < n && nums[left] > nums[max])
        max = left;
    if (right < n && nums[right] > nums[max])
        max = right;
    // 这里需要注意,堆化一个节点,只需要将最大值和节点i交换即可
    // 因此,不需要先和左孩子对比交换,再和右孩子对比交换
    // 只需要找到三者的最大值,直接交换一次即可
    if (max != i) {
        swap(nums[i], nums[max]);
        Heapify2(nums, max, n);  // 如果节点i和节点max交换了,那么节点max也需要重新堆化
    }
    // 如果节点i本身就是最大值,那堆化就到此为止了,返回
    // 如果已经是叶子节点,没有孩子节点,那么left和right就会不满足前面两个<n的if条件判断
    //  max==i,所以还是会在这里结束堆化,返回
}

/**
 * 堆排序
 */
void HeapSort(vector<int>& nums) {
    /**
     * 大体思路是,先把大顶堆构造好,此时,第0个元素就是堆顶,即最大值,将堆顶交换到最后边,它就不用再动了
     * 然后对于前面n-1的数组,本来是大顶堆,但是最大值被一个较小的值交换走了,因此这时大顶堆应该不对了,需要重新堆化第0个元素
     * 堆化一个节点,可能导致它左右孩子变化,变化的节点,也要递归往下堆化,直到叶子结点,重新完成堆化
     * 这时候,又是一个大顶堆了,再次把堆顶元素交换走
     * 重复这个操作
     */
    // 构建大顶堆
    int n = nums.size();
    for (int i = (n - 1) / 2; i >= 0; --i) {  // 从第一个非叶子节点开始,自右向左,自下而上堆化每一个节点,构建大顶堆
        Heapify2(nums, i, n);  // 堆化这个非叶子节点
    }
    // 循环把最大值交换到后面,然后重新堆化节点0,因为节点0变动了
    for (int i = n - 1; i > 0; --i) {
        swap(nums[0], nums[i]);
        Heapify2(nums, 0, i);
    }
}

分析

This post is licensed under CC BY 4.0 by the author.