【算法导论】线性时间排序(计数排序、基数排序、桶排序)

news2024/11/20 13:28:25

引言:
  在排序的最终结果中,个元素的次序依赖于它们之间的比较,我们把这类排序算法称为比较排序,对于包含n个元素的输入序列来说,任何比较排序在最坏情况下都要经过 Ω ( n l g n ) \Omega(nlgn) Ω(nlgn)次比较,下面将讨论三种线性时间复杂度的排序算法。

1.排序算法的下界

  比较排序可以被抽象为一棵决策树,以下是作用于三个元素时的比较排序决策树:
在这里插入图片描述
  在决策树中,,每个内部节点都以 i : j i:j i:j 标记,其中,i 和 j 满足 1 ≤ i , j ≤ n 1 \leq i,j \leq n 1i,jn,n 是输入序列中的元素个数,每个叶节点上都标注一个序列 < π ( 1 ) , π ( 2 ) , . . . , π ( n ) > <\pi(1),\pi(2),...,\pi(n)> <π(1)π(2)...,π(n)>,排序算法的执行对应于一条从树的根节点到叶结点的路径。每一个内部节点表示一次比较 a i ≤ a j a_i \leq a_j aiaj。左子树表示一旦我们确定 a i ≤ a j a_i \leq a_j aiaj之后的后续比较,右子树表示在确定了 a i > a j a_i > a_j ai>aj的后续比较。当到达叶节点时,表示排序算法已经确定了一个顺序 a π ( 1 ) ≤ a π ( 2 ) ≤ . . . ≤ a π ( n ) a_{\pi(1)} \leq a_{\pi(2)} \leq ... \leq a_{\pi(n)} aπ(1)aπ(2)...aπ(n)
  在决策树中,从根节点到任意可达叶节点之间的最长简单路径的长度,表示的是对应排序算法中最坏情况下的比较次数。因此,一个比较排序算法中最坏情况比较次数就等于决策树的高度。
  对于正确的比较排序算法来说,n 个元素的 n!种可能的排列都应该出现在决策树的叶结点上。在一棵高为 h 、具有 l 个可达叶结点的决策树中,输入数据的 n!中可能的排列都是叶节点,所以有 n ! ≤ l n!\leq l n!l 。由于二叉树的高为h,叶结点的数目不多于 2 h 2^h 2h,我们可以得到: n ! ≤ l ≤ 2 h n!\leq l \leq 2^h n!l2h对该式两边取对数有 h ≥ l g ( n ! ) = Ω ( n l g n ) \begin{aligned} h& \geq lg(n!) \\&=\Omega(nlgn)\end{aligned} hlg(n!)=Ω(nlgn) 由此,我们可以得到比较排序算法中的最坏情况比较时间复杂度至少为 n l g n nlgn nlgn

2.计数排序

计数排序的要求: n个输入元素中的每一个都是在 0 到 k 之间的整数,其中 k 为某个整数。

计数排序的基本思路: 对于每一个输入的元素x,确定小于 x 的元素的个数。利用这一信息,我们可以直接把 x 放到它在输出数组中的位置上。

计数排序算法可视化:

  1. 统计数组A种 0 到 k 元素出现的个数,A数组中元素 i 的个数存放在 C 数组下标 i 的位置。
    在这里插入图片描述
  2. 对数组 C 进行前缀和处理。
    在这里插入图片描述
  3. 将数组A中的值倒序遍历按照其在映射到 C 数组中对应的下标位置idx,找到其元素值 val ,存放在 B 数组的相应位置,存放完成后,将 val 的值减一。
    在这里插入图片描述
  4. 重复过程三,遍历完数组 A ,完成排序。

计数排序伪代码:
  假设输入是一个数组 A[ 1…n ],A.length = n,B[ 1…n ]数组存放排序的输出,C[ 1…n ]提供临时存储空间。

	COUNT—SORT(A,B,k)
		// 初始化备用数组,将其 0 到 k 之间的下标元素都置为 0
		let i = 0 to k
			C[i] = 0
		// 统计元素值为 A[j] 的元素个数,A[j]作为下标,C[A[j]]作为A[j]的个数
		for j = 1 to A.length
			C[A[j]] = C[A[j]] + 1
		// 从 1 到 k 进行前缀和处理,统计比i小的元素有几个
		for i = 1 to k
			C[i] = C[i] + C[i-1]
		// 将A数组从后向前遍历(保证其稳定),是每一个元素插入相应的位置。
		for j = A.length downto 1
			B[C[A[j]]] = A[j]
			C[A[j] = C[A[j] - 1

C++实现计数排序:

  • 头文件:
#include <iostream>  
#include <vector>  
#include <algorithm> 
  • 调用与传参:
vector<int> A = { 4, 2, 2, 8, 3, 3, 1, 12, 56, 47 };
auto max_num_iter = max_element(A.begin(), A.end());
Counting_Sort(A,*max_num_iter);
  • 函数:
void Counting_Sort(vector<int>& A, int max_value) {
    // 创建计数数组,用于存储每个数字出现的次数  
    vector<int> C(max_value + 1, 0);

    // 统计每个数字出现的次数  
    for (int i = 0; i < A.size(); i++) {
        C[A[i]]++;
    }

    // 求前缀和
    for (int i = 1; i <= max_value; i++) {
        C[i] += C[i - 1];
    }

    // 创建临时数组,用于存储排序后的数字序列  
    vector<int> B(A.size());

    // 从后向前遍历原始数组,将数字按照计数数组中的顺序放入临时数组中  
    for (int i = A.size() - 1; i >= 0; i--) {
        B[--C[A[i]]] = A[i];
    }

    // 将临时数组中的数字复制回原始数组  
    for (int i = 0; i < A.size(); i++) {
        A[i] = B[i];
    }
}

计数排序时间复杂度分析: 第一个循环所花的时间为 θ ( k ) \theta(k) θ(k),第二个循环所花的时间为 θ ( n ) \theta(n) θ(n),第三个循环所花的时间为 θ ( k ) \theta(k) θ(k),第四个循环所花的时间为 θ ( n ) \theta(n) θ(n),所以,总的时间代价为 θ ( k + n ) \theta(k+n) θ(k+n),在实际工作中(例如对高考成绩排序),当 k = O ( n ) k=O(n) k=O(n) 时,我们会采用计数排序,这样运行时间为 O ( n ) O(n) O(n)

3.基数排序

基数排序的要求: 只能针对整数进行排序。

基数排序的基本思路: 针对待排数组元素中的每一个数,按照从低位(个位)到高位的方法,利用稳定的排序算法进行排序,得到的最终结果即为正确结果。

基数排序算法可视化:
在这里插入图片描述
基数排序伪代码:

RADIX-SORT(A,d)
	for i = 1 to d
		use a stable sort to sort array A on digit i

C++实现基数排序:

  • 头文件:
#include <iostream>  
#include <vector>  
  • 调用与传参:
vector<int> arr = { 170, 45, 75, 90, 802, 24, 2, 66};
Radix_Sort(arr);
  • 函数:
 void Radix_Sort(vector<int>& arr) {
    // 找到数组中的最大值  
    int max_num = arr[0];
    for (int i = 1; i < arr.size(); i++) {
        if (arr[i] > max_num) {
            max_num = arr[i];
        }
    }

    // 从最低位开始,按照每个数字的位数进行排序  
    int exp = 1; // 当前位数  
    while (max_num / exp > 0) {
        // 创建桶数组,用于存储当前位数的数字计数  
        vector<int> bucket(10, 0);
        for (int i = 0; i < arr.size(); i++) {
            // 将当前数字放入对应的桶中  
            bucket[(arr[i] / exp) % 10]++;
        }
        // 将桶中的计数转换为累计计数,方便后续放入数字  
        for (int i = 1; i < 10; i++) {
            bucket[i] += bucket[i - 1];
        }
        // 创建临时数组,用于存储排序后的数字序列  
        vector<int> temp(arr.size());
        // 从后向前遍历原始数组,将数字按照当前位数放入对应的桶中  
        for (int i = arr.size() - 1; i >= 0; i--) {
            temp[--bucket[(arr[i] / exp) % 10]] = arr[i];
        }
        // 将临时数组中的数字复制回原始数组  
        for (int i = 0; i < arr.size(); i++) {
            arr[i] = temp[i];
        }
        // 更新当前位数  
        exp *= 10;
    }
}

基数排序时间复杂度分析:

  • 引理1:给定 n 个 d 位数,其中每一个数位有 k 个可能的取值。如果RADIX-SORT使用的稳定排序方法耗时 θ ( n + k ) \theta(n+k) θ(n+k),那么它就可以在 θ ( d ∗ ( n + k ) ) \theta(d*(n+k)) θ(d(n+k)) 时间内将这些数排好序。
  • 引理2:给定 n 个 b 位二进制数和任何整数 r ≤ b r\leq b rb,如果RADIX-SORT使用的稳定排序算法对数据取值区间是 0 到 k 的输入进行排序耗时 θ ( n + k ) \theta(n+k) θ(n+k),那么它就可以在 θ ( ( b / r ) ( n + 2 r ) ) \theta((b/r)(n+2^r)) θ((b/r)(n+2r))时间内将这些数据排好序。

4.桶排序

桶排序的要求: 输入数据服从均匀分布,元素取值在 [0,1) 之间。

桶排序的基本思路: 桶排序将 [0,1) 区间划分为 n 个相同大小的子区间,或称为桶。然后,将 n 个输入数分别放到各个桶中。因为输入数据是均匀、独立分布在 [0,1) 区间上,所以一般不会出现很多数落在一个区间的情况。为了得到输出结果,我们先对每个桶中的数进行排序,然后遍历每一个桶,按照次序把各个桶中的元素列出来即可。

桶排序算法可视化:
在这里插入图片描述
桶排序伪代码:

BUCKET-SORT(A)
	n = A.length
	let B[0..n-1] be a new array
	for i =0 to n-1
		make B[i] an empty list
	for i = 1 to n
		insert A[i] into list B[floor(nA[i])]
	for i = 0 to n-1
		sort list B[i] with insertion sort
	concatenate the list B[0],B[1],...,B[n-1] together in order

C++实现桶排序:

  • 头文件:
# include <vector>  
# include <iostream>
# include <algorithm>  
# include <cmath>
  • 调用与传参:
vector<float> A = { 0.78,0.17,0.39,0.26,0.72,0.94,0.21,0.12,0.23,0.68 };
Bucket_Sort(A);
  • 函数:
void Bucket_Sort(vector<float>& A) {
    int n = A.size();
    // Step 1: 初始化桶数组 B  
    vector<vector<float>> B(n);
    // Step 2: 将元素插入到对应的桶中  
    for (int i = 0; i < n; i++) {
        int bucketIndex = n * A[i]; // 计算元素应进入哪个桶  
        B[bucketIndex].push_back(A[i]);
    }
    // Step 3: 对每个桶中的元素进行排序,这里使用插入排序  
    for (int i = 0; i < n; i++) {
        sort(B[i].begin(), B[i].end());
    }
    // Step 4: 将排序后的桶合并到一起  
    int index = 0; // 用于跟踪合并后的数组中的下一个空位置  
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < B[i].size(); j++) {
            A[index++] = B[i][j];
        }
    }
}

桶排序时间复杂度分析:
  假设 n i n_i ni 表示桶 B[i] 中元素个数的随机变量,因为插入排序的的时间是平方阶的,索引桶排序的时间代价为: T ( n ) = θ ( n ) + ∑ i = 1 n − 1 O ( n i 2 ) T(n) = \theta (n)+\sum_{i=1}^{n-1}O(n_i^2) T(n)=θ(n)+i=1n1O(ni2)

  对上式两边取期望,并利用期望的线性性质得到: E [ T ( n ) ] = E [ θ ( n ) + ∑ i = 1 n − 1 O ( n i 2 ) ] = θ ( n ) + ∑ i = 1 n − 1 E [ O ( n i 2 ) ] = θ ( n ) + ∑ i = 1 n − 1 O E [ ( n i 2 ) ] \begin{aligned} E[T(n)] &= E[\theta (n)+\sum_{i=1}^{n-1}O(n_i^2)]\\&= \theta (n)+\sum_{i=1}^{n-1}E[O(n_i^2)]\\&= \theta (n)+\sum_{i=1}^{n-1}OE[(n_i^2)]\end{aligned} E[T(n)]=E[θ(n)+i=1n1O(ni2)]=θ(n)+i=1n1E[O(ni2)]=θ(n)+i=1n1OE[(ni2)]

  因为输入数组 A 的每一个元素是等概率的落入任意一个桶中,所以每个桶 i 具有相同的期望值 E [ n i 2 ] E[n_i^2] E[ni2],我们定义随机变量:对所有 i = 0,1,…,n-1 和 j = 1,2,…,n, X i j = I { A [ j ] 落入桶 i } X_{ij}=I \{ A[j]落入桶 i \} Xij=I{A[j]落入桶i}

  因此有: n i = ∑ j = 1 n X i j n_i=\sum_{j=1}^nX_{ij} ni=j=1nXij

  为了计算 E [ n i 2 ] E[n_i^2] E[ni2] ,我们展开平方项,并重新组合各项:
E [ n i 2 ] = E [ ( ∑ j = 1 n X i j ) 2 ] = E [ ∑ j = 1 n ∑ k = 1 n X i j X i k ] = E [ ∑ j = 1 n X i j 2 + ∑ j = 1 n ∑ k = 1 , k ≠ j n X i j X i k ] = ∑ j = 1 n E [ X i j 2 ] + ∑ j = 1 n ∑ k = 1 , k ≠ j n E [ X i j X i k ] \begin{aligned} E[n_i^2] &= E[(\sum_{j=1}^nX_{ij})^2] \\&=E[\sum_{j=1}^n\sum_{k=1}^nX_{ij}X_{ik}] \\&=E[\sum_{j=1}^nX_{ij}^2+\sum_{j=1}^n\sum_{k=1,k \neq j}^nX_{ij}X_{ik}] \\&=\sum_{j=1}^nE[X_{ij}^2]+\sum_{j=1}^n\sum_{k=1,k \neq j}^nE[X_{ij}X_{ik}] \end{aligned} E[ni2]=E[(j=1nXij)2]=E[j=1nk=1nXijXik]=E[j=1nXij2+j=1nk=1,k=jnXijXik]=j=1nE[Xij2]+j=1nk=1,k=jnE[XijXik]

  接下来我们计算最后的两项, X i j X_{ij} Xij 为 1 的概率为 1 n \frac{1}{n} n1 ,其他情况都是 0,于是有: E [ X i j 2 ] = 1 2 ∗ 1 n + 0 2 ∗ ( 1 − 1 n ) = 1 n E[X_{ij}^2] = 1^2*\frac{1}{n}+0^2*(1-\frac{1}{n})=\frac{1}{n} E[Xij2]=12n1+02(1n1)=n1

  当 k ≠ j k\neq j k=j时,随机变量 X i j 和 X i k X_{ij}和X_{ik} XijXik是独立的,因此有:
E [ X i j X i k ] = E [ X i j ] E [ X i k ] = 1 n ∗ 1 n = 1 n 2 E[X_{ij}X_{ik}] =E[X_{ij}]E[X_{ik}]=\frac{1}{n}*\frac{1}{n}=\frac{1}{n^2} E[XijXik]=E[Xij]E[Xik]=n1n1=n21

  将这两个期望值代入公式得到:
E [ n i 2 ] = ∑ j = 1 n 1 n + ∑ j = 1 n ∑ k = 1 , k ≠ j n 1 n 2 = n ∗ 1 n + n ( n − 1 ) ∗ 1 n 2 = 1 + n − 1 n = 2 − 1 n \begin{aligned}E[n_i^2] &=\sum_{j=1}^n\frac{1}{n}+\sum_{j=1}^n\sum_{k=1,k \neq j}^n\frac{1}{n^2} \\&= n*\frac{1}{n}+n(n-1)*\frac{1}{n^2}\\& = 1+\frac{n-1}{n}\\&=2-\frac{1}{n}\end{aligned} E[ni2]=j=1nn1+j=1nk=1,k=jnn21=nn1+n(n1)n21=1+nn1=2n1

  最终我们可以得到结论,桶排序的期望运行时间为: θ ( n ) + n ∗ O ( 2 − 1 / n ) = θ ( n ) \theta(n)+n*O(2-1/n)=\theta(n) θ(n)+nO(21/n)=θ(n)
  即使输入数据不服从均匀分布,只要输入的数据满足:所有的桶的大小的平方和与总的元素的个数呈线性关系,那么桶排序仍能在线性时间内完成。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1059070.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【RuoYi项目分析】在RuoYi网关实现验证码功能

文章目录 1. 验证码功能的类清单2. 验证码的实现2.1. 验证码的获取2.2. 验证码的校验 3. 总结4. 资料参考 本文主要介绍了用户如何实现验证码&#xff0c;以及该功能如何与 Spring Gateway 联系起来。 1. 验证码功能的类清单 类功能CaptchaProperties验证码的 yml 配置Captcha…

算法-数学-斜率-直线上最多的点数

算法-数学-斜率-直线上最多的点数 1 题目概述 1.1 题目出处 https://leetcode.cn/problems/max-points-on-a-line/ 1.2 题目描述 给你一个数组 points &#xff0c;其中 points[i] [xi, yi] 表示 X-Y 平面上的一个点。求最多有多少个点在同一条直线上。 2 暴力搜索斜率…

Go 基本数据类型和 string 类型介绍

Go 基础之基本数据类型 文章目录 Go 基础之基本数据类型一、整型1.1 平台无关整型1.1.1 基本概念1.1.2 分类有符号整型&#xff08;int8~int64&#xff09;无符号整型&#xff08;uint8~uint64&#xff09; 1.2 平台相关整型1.2.1 基本概念1.2.2 注意点1.2.3 获取三个类型在目标…

postgresql-自增字段

postgresql-自增字段 标识列IdentitySerial类型Sequence序列 标识列Identity -- 测试表 create table t_user( -- 标识列自增字段user_id integer generated always as identity primary key,user_name varchar(50) not null unique );-- 自动生成序列 CREATE SEQUENCE public…

学位论文的写作方法,较好的参考文章

摘要 结合2个文章&#xff1a; [1]程鑫. 网联环境下交通状态预测与诱导技术研究[D]. 长安大学, 2017. [2]吴昊. 关中平原水资源变化特征与干旱脆弱性研究[D]. 长安大学, 2018. 主要研究内容及技术路线 各章小结和引言的写作 [1]程鑫. 网联环境下交通状态预测与诱导技术…

一文拿捏分布式协调Redis客服端-Redisson

Redisson 1.介绍 Redisson - 是一个高级的分布式协调Redis客服端 , 专注于分布式系统开发&#xff0c;让用户可以在分布式系统中很方便的去使用Redis。 2.相关使用 1.加锁 //底层是lua脚本保证了加锁的原子性 // 一直等待获取锁&#xff0c;直到获取到锁为止! 默认锁的存活…

【SimpleDateFormat】线程不安全问题分析及解决方案

前言 在日常开发中&#xff0c;我们经常需要去做日期格式转换&#xff0c;可能就会用到SimpleDateFormat类。但是&#xff0c;如果使用不当&#xff0c;就很容易引发生产事故&#xff01; 1. 问题推演 1.1 初始日期工具类 刚开始的日期转换工具类可能长这样&#xff1a; p…

nodejs+vue晓海网上订餐系统elementui

管理员功能需求 管理员登陆后&#xff0c;主要模块包括首页、个人中心、用户管理、菜单信息管理等功能。 第三章 系统分析 10 3.1需求分析 10 3.2可行性分析 10 3.2.1技术可行性&#xff1a;技术背景 10 3.2.2经济可行性 11 3.2.3操作可行性&#xff1a; 11 3.3性能分析 11 3.4…

创建GCP service账号并管理权限

列出当前GCP项目的所有service account 我们可以用gcloud 命令 gcloud iam service-accounts list gcloud iam service-accounts list DISPLAY NAME EMAIL DISABLED terraform …

数据结构 1.1 初学数据结构

数据结构的基本概念 数据结构在学什么&#xff1f; 如何用程序代码把现实世界的问题信息化 如何用计算机高效处理信息从而创造价值 数据&#xff1a; 数据元素、数据项&#xff1a; 数据元素——描述一个个体 数据对象——数据元素之间具有同样的性质 同一个数据对象里的数…

BGP高级特性——4字节AS号

目录 4字节AS号 相关概念 两种过渡属性 4字节AS号的格式 4字节AS号建立邻居 4字节AS号路由传递 配置命令 4字节AS号 相比于2字节AS号&#xff0c;范围更大。由1~65535扩展到1~4294967295 支持4字节AS号的BGP设备兼容仅支持2字节AS号的BGP设备 相关概念 Speaker&#…

13链表-简单思路练习

目录 LeetCode之路——876. 链表的中间结点 分析&#xff1a; 解法一&#xff1a;常规思路 解法二&#xff1a;快慢指针 LeetCode之路——876. 链表的中间结点 给你单链表的头结点 head &#xff0c;请你找出并返回链表的中间结点。 如果有两个中间结点&#xff0c;则返回…

详解Linux的系统调用fork()函数

在Linux系统中&#xff0c;fork()是一个非常重要的系统调用&#xff0c;它的作用是创建一个新的进程。具体来说&#xff0c;fork()函数会在当前进程的地址空间中复制一份子进程&#xff0c;并且这个子进程几乎完全与父进程相同&#xff0c;包括进程代码、数据、堆栈以及打开的文…

利用Redis实现全局唯一ID

利用Redis实现全局唯一ID 背景 场景分析&#xff1a;如果我们的id具有太明显的规则&#xff0c;用户或者说商业对手很容易猜测出来我们的一些敏感信息&#xff0c;比如商城在一天时间内&#xff0c;卖出了多少单&#xff0c;这明显不合适。 场景分析二&#xff1a;随着我们商…

怎么将本地代码文件夹通过Git 命令上传到启智平台仓库

在本地创建一个与启智平台仓库同样名字的文件夹 然后在本地文件夹右键–>选择Git Bash Here,就会打开Git命令窗口 初始化本地仓库 git init将项目文件添加到Git git add .提交更改&#xff1a; 使用以下命令提交您的更改&#xff0c;并为提交添加一条描述性的消息&#…

ArcGIS Engine:实现Shp/Mxd数据的加载、图层的简单查询

本博客参考&#xff1a;BiliBili UP主 <羊羊旸> &#xff1a; Arcgis Engine学习 目录 01 加载控件以及控件的基本信息等调整 02 编写 <菜单-地图控件> 中各个子工具的代码 2.1 加载Shapefile数据-代码 2.2 加载地图文档数据-代码 2.3 获取图层数量-代码 2.…

lv7 嵌入式开发-网络编程开发 04 IP地址与端口号

目录 1 IP地址 1.1 IP 地址及其表示方法 1.2 分类的 IP 地址 1.3 无分类编址 CIDR 1.3.1 网络前缀 1.3.2 地址块 1.3.3 地址掩码 (address mask) 1.4 IPv6 的地址 1.4.1 表示方式 1.4.2 零压缩 2 端口号 2.1 进程之间的通信 2.2 运输层的作用 2.3 屏蔽作用 2.4…

CompletableFuture 异步编排

目录 CompletableFuture 的详解代码测试配置类的引入Demo1Demo2CompletableFuture的async后缀函数与不带async的函数的区别ThreadPoolTaskExecutor 和 ThreadPoolExecutor 的区别Spring 线程池的使用业务使用多线程的原因场景一:场景二:FutureTask介绍线程池为什么要使用阻塞队…

(三) Markdown插入互联网或本地视频解决方案

前言 不论博客系统是WordPress还是Typecho&#xff0c;绕不开的是两种书写语言&#xff0c;一种称之为富文本&#xff0c;一种叫做Markdown。 Markdown有很多好处&#xff0c;也有很多坏处&#xff0c;比如Markdown本身不具备段落居中的功能&#xff0c;以及Markdown也不具有…

聊聊JAVA中的锁优化锁升级及其底层原理剖析

文章目录 1. 基础介绍java中的锁什么是锁优化和锁升级 2. Java中的锁升级过程及底层原理1. 偏向锁实现机制和原理1. 偏向锁的原理2. 偏向锁的底层实现 2. 轻量级锁1. 轻量级锁的原理2. 轻量级锁的底层实现 3. 重量级锁1. 重量级锁的原理2. 重量级锁的底层实现 3. Java中锁升级的…