从0开始学算法——第五天(初级排序算法)

写在开头的话

学习完了数据结构也进行了几道简单的练习,今天开始我们就正式开始学习第一个简单的算法------排序算法啦。不过这一部分内容有些多,我会分成多天的内容来书写,学习进度快的朋友可以自行补充资料哈。

第一节

知识点:

(1)常用排序算法介绍(2)排序的稳定性分析(3)排序算法的比较与应用场景

常用排序算法介绍

在计算机科学中,一个排序算法是一种能将一串资料依照特定排序方式排列的算法。最常用到的排序方式是数值顺序以及字典顺序。排序算法是计算机科学中最基本、最常用的算法之一,在数据处理和算法设计中起着至关重要的作用。接下来介绍一些常用的排序算法。

(1)冒泡排序(Bubble Sort)

  • 排序流程 :比较相邻的元素,如果顺序错误则交换它们,直到没有需要交换的元素为止。
  • 时间复杂度 :平均情况和最坏情况下均为 O(),最好情况下为 O(n)。
  • 空间复杂度为 O(1)。

(2)选择排序(Selection Sort)

  • 排序流程:每次从未排序的部分选取最小(或最大)的元素,放到已排序部分的末尾。
  • 时间复杂度:平均情况、最坏情况和最好情况下均为 O()。
  • 空间复杂度为 O(1)。

(3)插入排序(Insertion Sort)

  • 排序流程:将未排序的元素逐个插入到已排序部分的合适位置。
  • 时间复杂度:平均情况和最坏情况下均为 O(),最好情况下为O(n)。
  • 空间复杂度: O(1)。

(4)快速排序(Quick Sort)

  • 排序流程:通过选择一个基准值,将数组分为两部分,小于基准值的放在左边,大于基准值的放在右边,然后对左右两部分递归地进行快速排序。
  • 时间复杂度:平均情况下为 O(nlogn),最坏情况下为 O()。
  • 空间复杂度: O(logn)。

(5)归并排序(Merge Sort)

  • 排序流程:将数组递归地分成两半,对每个子数组进行归并排序,然后将已排序的子数组合并成一个有序数组。
  • 时间复杂度:平均情况、最坏情况和最好情况下均为 O(nlogn)。
  • 空间复杂度: O(n)。

(6)堆排序(Heap Sort)

  • 排序流程:利用堆这种数据结构,将数组构建成最大堆或最小堆,然后依次将堆顶元素与堆尾元素交换并调整堆,最终得到有序数组。
  • 时间复杂度:平均情况、最坏情况和最好情况下均为 O(nlogn)。
  • 空间复杂度: O(1)。

(7)基数排序(Radix Sort)

  • 排序流程:将整数按照位数划分成不同的数字位(个位、十位、百位等),然后分别对每个数字位进行排序。
  • 时间复杂度:平均情况、最坏情况和最好情况下均为 O(d⋅(n+k)),其中 d 是数字位数,n 是元素个数,k 是每个位数可能的取值范围。
  • 空间复杂度:取决于用来辅助排序的数据结构,通常为O(n+k)。

排序的稳定性分析

排序算法的稳定性是指在排序过程中相同元素的相对位置是否会发生改变。具体来说,如果两个相同元素的原始位置在排序后仍然保持不变,那么这个排序算法就是稳定的;如果排序过程中相同元素的相对位置发生了改变,那么这个排序算法就是不稳定的。

稳定性在某些情况下非常重要,特别是在对具有相同值的元素进行排序时。以下是几种常见排序算法的稳定性分析:

稳定排序算法

  • 插入排序(Insertion Sort):相同元素的相对位置不会改变,所以是稳定的排序算法。
  • 归并排序(Merge Sort):在合并过程中会保持相同元素的相对位置,因此也是稳定的排序算法。
  • 冒泡排序(Bubble Sort)、基数排序(Radix Sort):这些排序算法也是稳定的。

不稳定排序算法

  • 选择排序(Selection Sort):因为在选择最小(或最大)元素的过程中可能改变相同元素的相对位置,所以是不稳定的排序算法。
  • 快速排序(Quick Sort)、堆排序(Heap Sort):这些排序算法在排序过程中涉及元素的交换,可能会导致相同元素的相对位置改变,因此是不稳定的排序算法。

排序算法的比较和应用场景

对于不同的排序算法,有各自的长处和短处,我们需要根据使用场景来选择适当的排序算法。

冒泡排序(Bubble Sort)

  • 比较简单直观,适用于小规模数据的排序。
  • 由于其时间复杂度为 O(),对于大规模数据不适用,性能较差。
  • 不推荐在实际项目中使用,除非数据量较小且对性能要求不高。

选择排序(Selection Sort)

  • 类似冒泡排序,简单直观,适用于小规模数据的排序。
  • 时间复杂度为 O(),性能也较差,不适用于大规模数据的排序。

插入排序(Insertion Sort)

  • 对于基本有序的数据效率较高,适用于小规模或部分有序的数据排序。
  • 时间复杂度为 O(),在实际中对于小规模数据排序效果较好。

归并排序(Merge Sort)

  • 基于分治思想,稳定且效率较高,适用于大规模数据的排序。
  • 时间复杂度为 O(nlogn),适用于需要稳定排序且对性能要求较高的场景。

快速排序(Quick Sort)

  • 高效的排序算法,对于大规模数据性能优秀。
  • 时间复杂度平均为 O(nlogn),最坏情况下为 O(),适用于对性能要求较高且数据量较大的场景。
  • 快速排序也是许多编程语言中默认的排序算法实现。

堆排序(Heap Sort)

  • 基于堆数据结构实现的排序算法,对于大规模数据也有较好的性能。
  • 时间复杂度为 O(nlogn),是不稳定的排序算法。
  • 适用于需要稳定性能且对排序稳定性要求不高的场景。

基数排序(Radix Sort)

  • 这是非比较性的排序算法,与传统的比较排序算法(如快速排序、归并排序等)不同,它利用了数字的位数信息进行排序。
  • 时间复杂度为O(d⋅(n+k)),其中 d 是数字位数,n 是元素个数,k 是每个位数可能的取值范围。是稳定的排序算法。
  • 适用于对非负整数进行稳定排序的场景,特别是当位数较少但数值范围较大时。

综合来看,不同的排序算法适用于不同的场景。在实际项目中,应根据数据量、数据特点以及对性能和稳定性的要求来选择合适的排序算法。

简单总结

以上我们了解了计算机科学中排序算法的重要性,初步了解了各种排序算法的比较与应用场景,以及针对各个排序算法的稳定性也进行了分析。在未来的实践中,需要根据不同的使用场景来调用不同的排序算法。

第二节

知识点:

(1)冒泡排序(2)冒泡排序变体之鸡尾酒排序

1.冒泡排序

作为初次学习排序算法,冒泡排序是个很好的用来理解排序算法的例子。

图示

冒泡排序演示

流程分析

基本思想
  • 从头到尾依次比较相邻的元素,如果顺序不对则交换位置,每次遍历将最大的元素沉到最后。
  • 重复以上步骤,直到整个数组有序。
具体步骤
  • 从第一个元素开始,依次比较相邻的两个元素,如果前面的元素大于后面的元素,则交换它们的位置。
  • 重复上述比较和交换的过程,直到最后一个元素。
  • 经过一轮比较和交换后,最大的元素就会被移动到数组的末尾。
  • 重复上述过程,每次比较的元素数量减少一,直到整个数组有序。

复杂度分析

时间复杂度
  • 最坏情况下,需要进行 n−1 轮比较( n 为数组长度),每轮比较需要进行 n−i−1 次比较和交换( i 为已经排好序的元素个数)。
  • 总的比较次数为 (n-1) + (n-2) + ... + 1 =(n−1)+(n−2)+...+1=n⋅(n−1)/2,时间复杂度为 O()。
  • 最好情况下,数组已经有序,只需要进行一轮比较,时间复杂度为 O(n)。
空间复杂度

冒泡排序是原地排序算法,不需要额外的存储空间,空间复杂度为 O(1)。

稳定性分析

冒泡排序是一种稳定的排序算法。在冒泡排序的过程中,只有相邻元素的比较才会发生位置交换,相同元素之间不会交换位置,因此相同元素的相对位置在排序后不会改变,保证了稳定性。

代码实现

C++代码实现
cpp 复制代码
#include <iostream>
#include <vector>

void bubbleSort(std::vector<int>& arr) {
    int n = arr.size();
    for (int i = 0; i < n - 1; ++i) {
        bool swapped = false;
        for (int j = 0; j < n - i - 1; ++j) {
            if (arr[j] > arr[j + 1]) {
                std::swap(arr[j], arr[j + 1]);
                swapped = true;
            }
        }
        if (!swapped) break;  // 如果本轮没有发生交换,说明数组已经有序,提前结束
    }
}

int main() {
    std::vector<int> arr = {64, 34, 25, 12, 22, 11, 90};
    bubbleSort(arr);
    std::cout << "Sorted array:";
    for (int num : arr) {
        std::cout << " " << num;
    }
    std::cout << std::endl;
    return 0;
}
Java代码实现
java 复制代码
import java.util.Arrays;

public class BubbleSort {
    public static void bubbleSort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n - 1; i++) {
            boolean swapped = false;
            for (int j = 0; j < n - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                    swapped = true;
                }
            }
            if (!swapped) break;  // 如果本轮没有发生交换,说明数组已经有序,提前结束
        }
    }

    public static void main(String[] args) {
        int[] arr = {64, 34, 25, 12, 22, 11, 90};
        bubbleSort(arr);
        System.out.println("Sorted array: " + Arrays.toString(arr));
    }
}
Python代码实现
python 复制代码
def bubble_sort(arr):
    n = len(arr)
    for i in range(n - 1):
        swapped = False
        for j in range(n - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
                swapped = True
        if not swapped:
            break  # 如果本轮没有发生交换,说明数组已经有序,提前结束

arr = [64, 34, 25, 12, 22, 11, 90]
bubble_sort(arr)
print("Sorted array:", arr)
运行结果

2.冒泡排序变体之鸡尾酒排序

鸡尾酒排序定义

鸡尾酒排序(Cocktail Sort)是冒泡排序的一种改进版本,也称为双向冒泡排序。它在每一轮排序中通过从左到右和从右到左两个方向交替进行冒泡操作,可以在某些情况下提高排序效率。

图示

双向冒泡排序演示

下面对鸡尾酒排序的流程、时间复杂度和稳定性进行详细分析

流程分析

基本思想

类似于冒泡排序,但是鸡尾酒排序在每一轮排序中可以同时从左到右和从右到左两个方向进行元素比较和交换。

具体步骤
  • 从左到右的冒泡过程:首先将最大的元素通过从左到右的冒泡操作移动到数组的最右侧。
  • 从右到左的冒泡过程:接着从右到左进行冒泡操作,将最小的元素移动到数组的最左侧。
  • 重复以上两个步骤,直到整个数组有序。

复杂度分析

时间复杂度
  • 最坏情况下,鸡尾酒排序的时间复杂度和冒泡排序相同,为 O()。
  • 最好情况下,如果数组已经有序,可以通过优化减少比较和交换次数,此时时间复杂度为 O(n)。
空间复杂度

鸡尾酒排序是原地排序算法,不需要额外的存储空间,空间复杂度为 O(1)。

稳定性分析

鸡尾酒排序是基于冒泡排序的一种变体,因此它也是稳定的排序算法。在排序过程中,只有相邻元素的比较才会发生位置交换,相同元素之间不会交换位置,因此相同元素的相对位置在排序后不会改变,保证了稳定性。

综上所述,鸡尾酒排序相对于普通冒泡排序在特定情况下可以提高排序效率,但是它的平均时间复杂度仍然为 O(),不适合用于大规模数据的排序。它适用于对稳定性有要求且数据规模较小的排序场景。因此,在实际应用中,需要根据具体情况选择合适的排序算法。

代码实现

C++代码实现
cpp 复制代码
#include <iostream>
#include <vector>

using namespace std;

void cocktailSort(vector<int>& arr) {
    int n = arr.size();
    bool swapped = true;
    int start = 0;
    int end = n - 1;

    while (swapped) {
        swapped = false;

        // 从左到右进行比较交换
        for (int i = start; i < end; ++i) {
            if (arr[i] > arr[i + 1]) {
                swap(arr[i], arr[i + 1]);
                swapped = true;
            }
        }

        if (!swapped) break;

        swapped = false;

        // 从右到左进行比较交换
        end--;
        for (int i = end - 1; i >= start; --i) {
            if (arr[i] > arr[i + 1]) {
                swap(arr[i], arr[i + 1]);
                swapped = true;
            }
        }

        start++;
    }
}

int main() {
    vector<int> arr = {5, 2, 9, 1, 5, 6};
    cocktailSort(arr);

    cout << "Sorted array: ";
    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;

    return 0;
}
Java代码实现
java 复制代码
import java.util.Arrays;

public class CocktailSort {

    public static void cocktailSort(int[] arr) {
        int n = arr.length;
        boolean swapped = true;
        int start = 0;
        int end = n - 1;

        while (swapped) {
            swapped = false;

            // 从左到右进行比较交换
            for (int i = start; i < end; ++i) {
                if (arr[i] > arr[i + 1]) {
                    int temp = arr[i];
                    arr[i] = arr[i + 1];
                    arr[i + 1] = temp;
                    swapped = true;
                }
            }

            if (!swapped) break;

            swapped = false;

            // 从右到左进行比较交换
            end--;
            for (int i = end - 1; i >= start; --i) {
                if (arr[i] > arr[i + 1]) {
                    int temp = arr[i];
                    arr[i] = arr[i + 1];
                    arr[i + 1] = temp;
                    swapped = true;
                }
            }

            start++;
        }
    }

    public static void main(String[] args) {
        int[] arr = {5, 2, 9, 1, 5, 6};
        cocktailSort(arr);

        System.out.print("Sorted array: ");
        System.out.println(Arrays.toString(arr));
    }
}
Python代码实现
python 复制代码
def cocktail_sort(arr):
    n = len(arr)
    swapped = True
    start = 0
    end = n - 1

    while swapped:
        swapped = False

        # 从左到右进行比较交换
        for i in range(start, end):
            if arr[i] > arr[i + 1]:
                arr[i], arr[i + 1] = arr[i + 1], arr[i]
                swapped = True

        if not swapped:
            break

        swapped = False

        # 从右到左进行比较交换
        end -= 1
        for i in range(end - 1, start - 1, -1):
            if arr[i] > arr[i + 1]:
                arr[i], arr[i + 1] = arr[i + 1], arr[i]
                swapped = True

        start += 1

# 测试代码
arr = [5, 2, 9, 1, 5, 6]
cocktail_sort(arr)
print("Sorted array:", arr)
运行结果

简单总结

本节主要实现了冒泡排序和鸡尾酒排序,二者的时间复杂度均为 O() 。对于初步学习排序算法的学习者来说是一个很好的入门。但是这些排序的复杂度太高,在实际生产生活中通常会选择更好的排序算法。

第三节

知识点:

(1)选择排序(2)插入排序

选择排序

选择排序定义

选择排序(Selection Sort)是一种简单直观的排序算法。它的基本思想是在未排序序列中找到最小(或最大)的元素,将其放到已排序序列的起始位置,然后再从剩余未排序的元素中继续寻找最小(或最大)的元素,放到已排序序列的末尾。以此类推,直到所有元素都排好序。

图示

选择排序图示

具体步骤和复杂度

具体步骤如下:

  1. 从未排序序列中找到最小的元素。
  2. 将最小元素与未排序序列的第一个元素交换位置,即放到已排序序列的末尾。
  3. 从剩余未排序的元素中继续寻找最小的元素,重复上述步骤。

选择排序是一种不稳定的排序算法,时间复杂度为 O(),空间复杂度为 O(1)。它的优点是简单易实现,对于小规模数据或基本有序的数据效果也不错,但对于大规模数据性能相对较差。

代码实现

C++代码实现
cpp 复制代码
#include <iostream>
#include <vector>

// 函数:selectionSort
// 描述:实现选择排序算法,对输入数组进行排序。
// 参数:
// - arr: 需要排序的整数数组
void selectionSort(std::vector<int>& arr) {
    int n = arr.size(); // 获取数组的长度
    for (int i = 0; i < n - 1; ++i) { // 遍历数组,逐步确定每个元素的位置
        int minIndex = i; // 假设当前索引 i 处的元素是未排序部分的最小值
        for (int j = i + 1; j < n; ++j) { // 在未排序部分中寻找实际的最小元素
            if (arr[j] < arr[minIndex]) { // 如果找到了更小的元素,则更新 minIndex
                minIndex = j;
            }
        }
        // 如果找到了比当前元素更小的元素,则交换二者的位置
        if (minIndex != i) {
            std::swap(arr[i], arr[minIndex]);
        }
    }
}

int main() {
    // 初始化数组
    std::vector<int> arr = {64, 25, 12, 22, 11};

    // 输出原始数组
    std::cout << "Original array: ";
    for (int num : arr) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    // 调用选择排序函数进行排序
    selectionSort(arr);

    // 输出排序后的数组
    std::cout << "Sorted array: ";
    for (int num : arr) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}
Java代码实现
java 复制代码
import java.util.Arrays;

public class SelectionSort {
    // 方法:selectionSort
    // 描述:实现选择排序算法,对传入的整数数组进行排序。
    // 参数:
    // - arr: 需要排序的整数数组
    public static void selectionSort(int[] arr) {
        int n = arr.length; // 获取数组的长度
        
        // 外层循环:从数组的第一个元素开始,逐步确定每个元素的最终位置
        for (int i = 0; i < n - 1; ++i) {
            int minIndex = i; // 假设当前元素是未排序部分的最小值
            
            // 内层循环:在未排序的部分中寻找实际的最小元素
            for (int j = i + 1; j < n; ++j) {
                if (arr[j] < arr[minIndex]) { // 如果找到更小的元素,更新最小值索引
                    minIndex = j;
                }
            }
            
            // 如果找到的最小值索引不是当前的索引,则进行交换
            if (minIndex != i) {
                int temp = arr[i];
                arr[i] = arr[minIndex];
                arr[minIndex] = temp;
            }
        }
    }

    public static void main(String[] args) {
        // 初始化数组
        int[] arr = {64, 25, 12, 22, 11};

        // 输出原始数组
        System.out.println("Original array: " + Arrays.toString(arr));

        // 调用选择排序方法进行排序
        selectionSort(arr);

        // 输出排序后的数组
        System.out.println("Sorted array: " + Arrays.toString(arr));
    }
}
Python代码实现
python 复制代码
def selection_sort(arr):
    # 获取数组的长度
    n = len(arr)
    
    # 外层循环:遍历数组,从第一个元素开始,到倒数第二个元素结束
    for i in range(n - 1):
        # 假设当前索引 i 处的元素为未排序部分的最小值
        min_index = i
        
        # 内层循环:在未排序部分中寻找最小的元素
        for j in range(i + 1, n):
            if arr[j] < arr[min_index]:  # 如果找到比当前最小值更小的元素,更新 min_index
                min_index = j
        
        # 如果找到了比当前元素更小的元素,则交换这两个元素
        if min_index != i:
            arr[i], arr[min_index] = arr[min_index], arr[i]

# 测试代码
arr = [64, 25, 12, 22, 11]
print("Original array:", arr)  # 输出排序前的数组
selection_sort(arr)
print("Sorted array:", arr)  # 输出排序后的数组
运行结果

插入排序

插入排序定义

插入排序(Insertion Sort)是一种简单直观的排序算法。它的基本思想是将未排序的元素逐个插入到已排序的部分中,构建有序序列。具体步骤是,每次将一个待排序的元素插入到已排序序列的适当位置,直到所有元素都插入完成。这个过程类似于打扑克牌时的排序方法,适用于对部分有序或小规模数据进行排序的情况。

图示

插入排序演示

具体步骤和时间复杂度

  1. 从第二个元素开始逐一向后枚举,每次枚举元素视为当前元素。
  2. 将当前元素与其前面已经排好序的元素依次比较,直到找到合适的位置。
  3. 将当前元素插入到找到的位置,同时将其前面的元素依次后移。
  4. 重复以上步骤,直到所有元素都被插入到正确的位置。

这种算法的时间复杂度为 O(),其中 n 是数组的大小。尽管算法简单,但在一些特定情况下(如部分有序的数组)效率仍然很高。插入排序是一种稳定的排序算法,适用于小规模数据或基本有序的数组。

代码实现

C++代码实现
cpp 复制代码
#include <iostream>
#include <vector>

// 函数:insertionSort
// 描述:实现插入排序算法,对输入数组进行排序。
// 参数:
// - arr: 需要排序的整数数组(传引用,直接修改原数组)
void insertionSort(std::vector<int>& arr) {
    int n = arr.size(); // 获取数组的长度
    
    // 从数组的第二个元素开始遍历,逐步插入到已排序部分的正确位置
    for (int i = 1; i < n; ++i) {
        int key = arr[i]; // 当前待插入的元素
        int j = i - 1;
        
        // 向左扫描已排序部分,找到合适的插入位置
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j]; // 将大于 key 的元素向右移动一位
            j = j - 1;
        }
        arr[j + 1] = key; // 将 key 插入到正确的位置
    }
}

int main() {
    // 初始化数组
    std::vector<int> arr = {64, 25, 12, 22, 11};

    // 输出原始数组
    std::cout << "Original array: ";
    for (int num : arr) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    // 调用插入排序函数进行排序
    insertionSort(arr);

    // 输出排序后的数组
    std::cout << "Sorted array: ";
    for (int num : arr) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}
Java代码实现
java 复制代码
import java.util.Arrays;

public class InsertionSort {
    // 方法:insertionSort
    // 描述:实现插入排序算法,对传入的整数数组进行排序。
    // 参数:
    // - arr: 需要排序的整数数组
    public static void insertionSort(int[] arr) {
        int n = arr.length; // 获取数组的长度
        
        // 从数组的第二个元素开始遍历,逐步插入到已排序部分的正确位置
        for (int i = 1; i < n; ++i) {
            int key = arr[i]; // 当前待插入的元素
            int j = i - 1;
            
            // 向左扫描已排序部分,找到合适的插入位置
            while (j >= 0 && arr[j] > key) {
                arr[j + 1] = arr[j]; // 将大于 key 的元素向右移动一位
                j = j - 1;
            }
            arr[j + 1] = key; // 将 key 插入到正确的位置
        }
    }

    public static void main(String[] args) {
        // 初始化数组
        int[] arr = {64, 25, 12, 22, 11};

        // 输出原始数组
        System.out.println("Original array: " + Arrays.toString(arr));

        // 调用插入排序方法进行排序
        insertionSort(arr);

        // 输出排序后的数组
        System.out.println("Sorted array: " + Arrays.toString(arr));
    }
}
Python代码实现
python 复制代码
def insertion_sort(arr):
    # 获取数组的长度
    n = len(arr)
    
    # 从数组的第二个元素开始遍历
    for i in range(1, n):
        key = arr[i]  # 当前待插入的元素
        j = i - 1
        
        # 向左扫描已排序部分,找到合适的插入位置
        while j >= 0 and arr[j] > key:
            arr[j + 1] = arr[j]  # 将大于 key 的元素向右移动一位
            j = j - 1
        arr[j + 1] = key  # 将 key 插入到正确的位置

# 测试代码
arr = [64, 25, 12, 22, 11]
print("Original array:", arr)  # 输出排序前的数组
insertion_sort(arr)
print("Sorted array:", arr)  # 输出排序后的数组
运行结果

简单总结

在本次节中,我们学习了两种基本的排序算法:选择排序(Selection Sort)和插入排序(Insertion Sort)。通过实践编写这两种排序算法的代码,并进行性能测试,我们对它们的工作原理、时间复杂度和空间复杂度有了更深入的了解。

选择排序(Selection Sort) 是一种简单直观的排序算法,它的基本思想是每次从未排序序列中选择最小的元素,放到已排序序列的末尾。这个过程持续直到所有元素都排序完成。选择排序的时间复杂度为 O(),空间复杂度为 O(1)。

插入排序(Insertion Sort) 是一种类似于打扑克牌的排序方法,将未排序的元素逐个插入到已排序的部分中。它的时间复杂度也是 O(),空间复杂度为 O(1)。插入排序在处理基本有序的数据时效率较高。

过程中,我们发现选择排序适用于简单的排序任务,因为它的实现较为简单直观。而插入排序则更适用于对部分有序或小规模数据进行排序的场景。

第四节

知识点:

(1)基数排序(2)计数排序

基数排序

基数排序定义

基数排序是一种非比较性的排序算法,它根据元素的位值来对数字进行排序。它的核心思想是将待排序的元素分配到桶中,然后按照桶的顺序依次取出元素,重复这个过程直到所有位都被处理完毕。

基数排序通常用于对整数进行排序,但也可以扩展到其他数据类型。

图示

具体步骤和时间复杂度

基数排序的实现步骤如下:

确定最大位数: 首先确定待排序数组中的最大值,并计算出其位数,作为排序的轮数。

按位排序: 从最低有效位开始,依次对待排序数组进行按位排序。可以使用稳定的排序算法(如计数排序、桶排序等)对每个位上的数字进行排序。

重复操作: 对每一位进行排序后,重复这一过程直到对最高位完成排序。经过每一轮的排序,数组的元素顺序将越来越接近有序状态。

合并结果: 经过多轮的按位排序后,最终得到的数组即为有序的结果。

基数排序适用于整数排序,能够在一定范围内对大量数据进行排序,且稳定性较好。然而,基数排序的缺点是需要额外的存储空间来存放中间结果,当数据量很大时可能会占用较多内存

其时间复杂度为 O(d(n+k)) ,其中 d 是数字的最大位数, n 是元素个数, k​ 是基数(桶的数量)。

基数排序优缺点

优点

稳定性: 基数排序是一种稳定的排序算法,相等元素的相对顺序在排序后保持不变。这对某些需要保持顺序的应用场景非常重要。

适用范围广: 基数排序适用于整数排序,可以处理负数、正数和零,且不受数据分布情况的影响。它对于一定范围内的大量数据排序效果良好。

非比较性: 基数排序不涉及元素之间的比较操作,而是根据元素的位值进行分配和收集,因此其时间复杂度不受到元素的初始顺序的影响。

适用于大量数据: 基数排序的时间复杂度为 O(d(n+k)),其中 d 表示数字的最大位数,n 表示元素个数,k 表示基数范围。在一定范围内,基数排序能够有效地处理大量数据。

缺点

额外空间需求: 基数排序需要额外的空间来存储中间结果和计数数组,其空间复杂度为 O(n+k)。当数据量很大时,可能会占用较多内存。

不适用于负数: 基数排序通常不适用于包含负数的数据集,因为负数的处理需要额外的转换操作或修改算法。

稳定性开销: 保持基数排序的稳定性可能会增加额外的开销,特别是在对计数数组进行操作时可能需要更多的存储和处理时间。

位数限制: 基数排序的性能受到数字位数的限制,如果数字位数较大,可能会导致排序时间增加。因此,对于位数非常大的数据集,基数排序可能不是最佳选择。

适用场景

  • 当待排序元素具有固定位数,且每位数字的取值范围不是很大时,基数排序是一种有效的排序算法。
  • 基数排序适用于需要稳定排序、且元素为整数或字符串的排序问题。

基数排序虽然不常见于通用排序算法中,但在某些特定情况下,它能提供比较高效的排序解决方案。通过按位比较和排序,基数排序能够以线性时间复杂度完成排序任务,是一个值得了解和掌握的排序算法。

代码实现

C++代码实现(利用STL实现排序)
cpp 复制代码
#include <iostream>
using namespace std;

// 获取数组中最大值
int getMax(int arr[], int n) {
    int max = arr[0];
    for (int i = 1; i < n; ++i) {
        if (arr[i] > max) {
            max = arr[i];
        }
    }
    return max;
}

// 将元素放入桶中,使用计数排序实现
void countSort(int arr[], int n, int exp) {
    int output[n];
    int count[10] = {0};

    for (int i = 0; i < n; ++i) {
        count[(arr[i] / exp) % 10]++;
    }

    for (int i = 1; i < 10; ++i) {
        count[i] += count[i - 1];
    }

    for (int i = n - 1; i >= 0; --i) {
        output[count[(arr[i] / exp) % 10] - 1] = arr[i];
        count[(arr[i] / exp) % 10]--;
    }

    for (int i = 0; i < n; ++i) {
        arr[i] = output[i];
    }
}

/// 基数排序
void radixSort(int arr[], int n) {
    int max = getMax(arr, n);
    ///按照个位,十位,百位....进行排序
    ///确定最大位数: 首先确定待排序数组中的最大值,并计算出其位数,作为排序的轮数。
    for (int exp = 1; max / exp > 0; exp *= 10) {
        countSort(arr, n, exp);
    }
}

int main() {
    int arr[] = {170, 45, 75, 90, 802, 24, 2, 66};
    int n = sizeof(arr) / sizeof(arr[0]);

    cout << "原始数组:" << endl;
    for (int i = 0; i < n; i++) {
        cout << arr[i] << " ";
    }
    cout << endl;

    radixSort(arr, n);///基数排序的代码

    cout << "排序后的数组:" << endl;
    for (int i = 0; i < n; i++) {
        cout << arr[i] << " ";
    }
    cout << endl;

    return 0;
}
Java代码实现
java 复制代码
import java.util.Arrays;

public class RadixSort {
    
    // 获取数组中最大值
    public static int getMax(int[] arr) {
        int max = arr[0];
        for (int i = 1; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
        }
        return max;
    }

    // 将元素放入桶中
    public static void countSort(int[] arr, int exp) {
        int n = arr.length;
        int[] output = new int[n];
        int[] count = new int[10];

        Arrays.fill(count, 0);

        for (int i = 0; i < n; i++) {
            count[(arr[i] / exp) % 10]++;
        }

        for (int i = 1; i < 10; i++) {
            count[i] += count[i - 1];
        }

        for (int i = n - 1; i >= 0; i--) {
            output[count[(arr[i] / exp) % 10] - 1] = arr[i];
            count[(arr[i] / exp) % 10]--;
        }

        System.arraycopy(output, 0, arr, 0, n);
    }

    // 基数排序
    public static void radixSort(int[] arr) {
        int max = getMax(arr);

        for (int exp = 1; max / exp > 0; exp *= 10) {
            countSort(arr, exp);
        }
    }

    public static void main(String[] args) {
        int[] arr = {170, 45, 75, 90, 802, 24, 2, 66};

        System.out.println("原始数组:");
        for (int num : arr) {
            System.out.print(num + " ");
        }
        System.out.println();

        radixSort(arr);

        System.out.println("排序后的数组:");
        for (int num : arr) {
            System.out.print(num + " ");
        }
        System.out.println();
    }
}
Python代码实现
python 复制代码
def counting_sort(arr, exp):
    n = len(arr)
    output = [0] * n
    count = [0] * 10

    for i in range(n):
        index = arr[i] // exp
        count[index % 10] += 1

    for i in range(1, 10):
        count[i] += count[i - 1]

    i = n - 1
    while i >= 0:
        index = arr[i] // exp
        output[count[index % 10] - 1] = arr[i]
        count[index % 10] -= 1
        i -= 1

    for i in range(n):
        arr[i] = output[i]

def radix_sort(arr):
    max_value = max(arr)

    exp = 1
    while max_value // exp > 0:
        counting_sort(arr, exp)
        exp *= 10

arr = [170, 45, 75, 90, 802, 24, 2, 66]

print("原始数组:")
print(arr)

radix_sort(arr)

print("排序后的数组:")
print(arr)
运行结果

计数排序

计数排序定义

计数排序是一种非比较性的稳定排序算法,它通过统计数组中每个元素出现的次数,然后根据统计信息重新对数组进行排序。

计数排序适用于一定范围内的整数或非负整数排序,其核心思想是将输入的数据值转化为键存储在额外开辟的数组空间中,利用键值与原始数据之间的映射关系实现排序。

图示

具体步骤和时间复杂度

计数排序的实现步骤如下:

找出待排序数组中的最大值 max ,创建一个大小为 max+1 的计数数组 count ,并初始化为 0 。

遍历待排序数组,统计每个元素出现的次数,并存储到计数数组的对应位置上。

根据计数数组中的内容,重新生成排序后的数组。

将排序后的数组拷贝回原始数组。

计数排序的时间复杂度为 O(n+k) ,其中 n 表示元素个数, k=(max−min+1),同时 max 表示数组元素最大值, min​ 表示最小值 。计数排序是一种简单且高效的排序算法,特别适合用于排序整数类型 的数据。但是计数排序的缺点是需要额外的空间来存储中间结果,当数据范围很大时可能会造成空间浪费。

计数排序优缺点

优点

简单易懂: 计数排序是一种直观的排序算法,易于理解和实现。它不涉及比较操作,因此适用于初学者或需要快速实现的场景。

线性时间复杂度: 计数排序的时间复杂度为 O(n+k) ,其中 n 表示元素个数, k=(max−min+1),同时 max 表示数组元素最大值, min 表示最小值 。当 k 相对于 n 较小且是一个常数时,计数排序可以达到线性时间复杂度

稳定性: 计数排序是一种稳定的排序算法,即相等元素的相对顺序在排序后保持不变。这使得它特别适用于需要保持原始顺序的场景。

适用范围广: 计数排序适用于待排序元素值分布相对密集且范围较小的情况。它可以处理整数或非负整数,无论数据是否均匀分布。

缺点

额外空间需求: 计数排序需要额外的空间来存储计数数组,其大小取决于数据范围。当数据范围很大时,可能会导致内存消耗较大。

数据范围限制: 计数排序对数据范围有一定的限制,如果范围过大,会造成计数数组过大,进而增加空间消耗和排序时间。

不适用于负数: 计数排序通常不适用于包含负数的数据集,因为负数的处理需要额外的转换操作或修改算法。

不适用于浮点数: 计数排序无法直接应用于浮点数的排序,因为浮点数的范围通常很大且不可枚举,而计数排序需要枚举所有可能的值。

适用场景

  • 当待排序的元素范围相对较小,且待排序数组的长度较大时,计数排序是一种高效的排序算法。

  • 计数排序适用于待排序元素都是非负整数的情况,且元素之间的差值不是很大。

计数排序虽然不如快速排序、归并排序等比较排序算法在一般情况下的效率高,但在特定情况下能够以较高的效率完成排序任务,因此是值得了解和掌握的一种排序算法。

代码实现

C++代码实现
cpp 复制代码
#include <iostream>
///计数排序
void countingSort(int arr[], int n) {
    int max = arr[0];///记录数组最大值
    int min = arr[0];///记录数组最小值
    //确定范围: 首先确定待排序数组中元素的范围,即最大值和最小值。这一步是为了确定计数数组的大小。
    for (int i = 1; i < n; i++) {
        if (arr[i] > max) {
            max = arr[i];
        }
        if(arr[i]<min){
            min = arr[i];
        }
    }

    int count[max + 1 - min] = {0};
    int output[n];
    //计数: 创建一个计数数组,其长度等于待排序数组中元素范围的大小。遍历待排序数组,统计每个元素出现的次数,并将统计结果存储在计数数组中。计数数组的索引表示元素的取值,数组中存储的值表示对应元素出现的次数。
    for (int i = 0; i < n; i++) {
        count[arr[i]-min]++;
    }
    //累加: 对计数数组进行累加操作,使得每个索引位置存储的值表示小于或等于该索引的元素个数。这一步是为了确定每个元素在排序后的数组中的位置。
    for (int i = 1; i <= max - min; i++) {
        count[i] += count[i - 1];
    }
    //排序: 根据计数数组中的信息,将待排序数组中的元素依次放置到排序后的数组中的正确位置上。
    for (int i = n - 1; i >= 0; i--) {
        output[count[ arr[i]-min ] - 1] = arr[i];
        count[ arr[i]-min ]--;
    }
    //复制: 将排序后的数组拷贝回原始数组,完成排序过程。
    for (int i = 0; i < n; i++) {
        arr[i] = output[i];
    }
}

int main() {
    int arr[] = {4, 2, 2, 8, 3, 3, 1};
    int n = sizeof(arr) / sizeof(arr[0]);

    std::cout << "原始数组: ";
    for (int i = 0; i < n; i++) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
    countingSort(arr, n);

    std::cout << "排序后的数组: ";
    for (int i = 0; i < n; i++) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;

    return 0;
}
Java代码实现
java 复制代码
public class CountingSort {

    public static void countingSort(int[] arr) {
        int max = arr[0]; // 记录数组最大值
        int min = arr[0]; // 记录数组最小值
        for (int i = 1; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
            if (arr[i] < min) {
                min = arr[i];
            }
        }

        int[] count = new int[max - min + 1];
        int[] output = new int[arr.length];

        for (int i = 0; i < arr.length; i++) {
            count[arr[i] - min]++;
        }

        for (int i = 1; i <= max - min; i++) {
            count[i] += count[i - 1];
        }

        for (int i = arr.length - 1; i >= 0; i--) {
            output[count[arr[i] - min] - 1] = arr[i];
            count[arr[i] - min]--;
        }

        System.arraycopy(output, 0, arr, 0, arr.length);
    }

    public static void main(String[] args) {
        int[] arr = {4, 2, 2, 8, 3, 3, 1};

        System.out.print("原始数组: ");
        for (int num : arr) {
            System.out.print(num + " ");
        }
        System.out.println();

        countingSort(arr);

        System.out.print("排序后的数组: ");
        for (int num : arr) {
            System.out.print(num + " ");
        }
        System.out.println();
    }
}
Python代码实现
python 复制代码
def countingSort(arr):
    max_val = arr[0]  # 记录数组最大值
    min_val = arr[0]  # 记录数组最小值
    for num in arr:
        if num > max_val:
            max_val = num
        if num < min_val:
            min_val = num

    count = [0] * (max_val - min_val + 1)
    output = [0] * len(arr)

    for num in arr:
        count[num - min_val] += 1

    for i in range(1, max_val - min_val + 1):
        count[i] += count[i - 1]

    for i in range(len(arr) - 1, -1, -1):
        output[count[arr[i] - min_val] - 1] = arr[i]
        count[arr[i] - min_val] -= 1

    for i in range(len(arr)):
        arr[i] = output[i]

if __name__ == "__main__":
    arr = [4, 2, 2, 8, 3, 3, 1]

    print("原始数组:", end=" ")
    print(*arr)

    countingSort(arr)

    print("排序后的数组:", end=" ")
    print(*arr)
运行结果

简单总结

在本节中,我们学习了基数排序和计数排序的定义,步骤和时间复杂度。通过学习这些排序的实现原理,我们可以对这些排序的理解进一步加深,从而更好的使用这些排序。

相关推荐
摇滚侠1 小时前
零基础小白自学 Git_Github 教程,Git 命令行操作2,笔记19
笔记·git·github
Q741_1471 小时前
C++ 高精度计算的讲解 模拟 力扣67.二进制求和 题解 每日一题
c++·算法·leetcode·高精度·模拟
夏乌_Wx1 小时前
练题100天——DAY19:含退格的字符串+有序数组的平方
算法
走在路上的菜鸟1 小时前
Android学Dart学习笔记第十节 循环
android·笔记·学习·flutter
Ayanami_Reii1 小时前
进阶数据结构应用-线段树扫描线
数据结构·算法·线段树·树状数组·离散化·fenwick tree·线段树扫描线
leoufung1 小时前
LeetCode 98 Validate Binary Search Tree 深度解析
算法·leetcode·职场和发展
水木姚姚1 小时前
C++ begin
开发语言·c++·算法
浅川.251 小时前
xtuoj 素数个数
数据结构·算法
jyyyx的算法博客2 小时前
LeetCode 面试题 16.18. 模式匹配
算法·leetcode