C++数据结构算法(二)排序算法集合(二)

简介: C++数据结构算法(二)排序算法集合(二)

插入排序:

详细算法描述


整理插入排序算法描述如下:

枚举序列中第2~n个元素。

当枚举元素i时,前i-1个元素已经有序。将第i个元素插入到前i-1个元素的有序序列中,形成长度为i的有序序列。

枚举过程结束后,整个序列有序。

所以,我们总结一下插入操作的算法描述:


假设序列1~(i-1)已经有序, 从i到1枚举分界线的下标j;


如果分界线前面的元素a[j-1]大于x,说明a[j-1]应该在分界线后面。所以将a[j-1]移动到a[j],分界线前移变成j-1。


如果分界线前面没有元素(j=1),就将x放在数组第1位。否则如果碰到一个j-1号元素小于等于x,说明分界线位置正确,就将x插到j位。


完整代码:

#include <bits/stdc++.h>
#define N 1550
using namespace std;
int a[N], n;
int main() {
    // 输入
    cin >> n; 
    for (int i = 1; i <= n; ++i) cin >> a[i];
    // 插入排序
    for (int i = 2; i <= n; ++i) {    // 按照第2个到第n个的顺序依次插入
        int j, x = a[i];    // 先将i号元素用临时变量保存防止被修改。
        // 插入过程,目的是空出分界线位置j,使得所有<j的部分<=x,所有>j的部分>x。
        // 循环维持条件,j>1,并且j前面的元素>x。
        for (j = i; j > 1 && a[j - 1] > x; --j) {   
            // 满足循环条件,相当于分界线应向前移,
            // 分界线向前移,就等于将分界线前面>x的元素向后移
            a[j] = a[j - 1];              
        }
        // 找到分界线位置,插入待插入元素x
        a[j] = x;                         
    }
    // 输出
    for (int i = 1; i <= n; ++i) cout << a[i] << ' ';
    cout << endl;
    return 0;
}


快速排序:

image.png


快速排序的基本思想:

image.png

当需要将1到n的n个数排序时,我们通过分解,将该问题分解为两个将n/2个数排序的子问题;


在每个子问题中,我们继续分解,直到最后子问题长度为1;


此时,整个序列就完成排序了。


所以该算法的总复杂度变成了n^2/4 + 2nn2/4+2n。可以发现,在该算法中,多分解了一层,而复杂度也进一步减少了。


由此我们可以产生一个感觉,分解得越复杂度越小。所以,我们完全可以进一步分解,直到最后每个单位排序的长度为1。


image.png


任意长度为n的序列排序


当然快速排序也可用来给任意n个数的序列排序。但是与和1~n排序不同的是,对于任意n个数的序列,我们在划分子段的时候并不能很容易找到整个序列的“中位数”。所以只能在序列中任意取一个数。比如


取整个序列中最左边的数。

取整个序列中最右边的数。

在整个序列中随机一个位置并取该位置上的数。

都是常见的取数策略。


但由于不能保证每次取的数字都刚好是中位数,所以每次划分时也不能保证左边子段长度和右边子段长度非常平均。如果“不幸”选到不合适的数(比如整个子段中最小的数或最大的数),整个算法的效率会降低很多。


在此,我们详细描述一下给任意n个数排序的快速排序算法:


假设我们要对数组a[1..n]排序。初始化区间[1..n]。


令l和r分别为当前区间的左右端点。下面假设我们对l到r子段内的数字进行划分。取pivot = a[l]为分界线,将<pivot的数字移到左边,>pivot的数字移到右边,然后将pivot放在中间。假设pivot的位置是k。


如果左边区间[l..k-1]长度大于1,则对于新的区间[l..k-1],重复调用上面的过程。


如果右边区间[k+1..r]长度大于1,则设置新的区间[k+1, r],重复调用上面的过程。


当整个过程结束以后,整个序列排序完毕。


递归函数实现快速排序:

// 该代码参考 https://www.geeksforgeeks.org/quick-sort/
#include <bits/stdc++.h>
#define N 100010 
using namespace std; 
int n; 
int a[N]; 
void quick_sort(int l, int r) {  
    // l和r分别代表当前排序子段在原序列中左右端点的位置
    // 设置最右边的数为分界线
    int pivot = a[r];
    int k;
    /* 此处省略了元素移动和确定分界线新位置k的过程 */
    if (l < k - 1) quick_sort(l, k - 1); // 如果序列的分界线左边的子段长度>1,排序
    if (k + 1 < r) quick_sort(k + 1, r); // 如果序列的分界线右边的子段长度>1,排序
    // 上面的过程结束后,到这里左子段和右子段已经分别排好序。又因为确定分界线以后的移动操作
    // 保证了左子段中的元素都小于等于分界线,右子段中的元素都大于分界线。所以整个序列也是有序的。
} 
int main() { 
    // 输入
    scanf("%d", &n); 
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]); 
    // 快速排序
    quick_sort(1, n); 
    // 输出
    for (int i = 1; i <= n; ++i) printf("%d ", a[i]);  
    return 0; 
}


快速排序完整代码:

// 该代码参考 https://www.geeksforgeeks.org/quick-sort/
#include <bits/stdc++.h>
#define N 100010 
using namespace std; 
int n; 
int a[N]; 
void quick_sort(int l, int r) { 
    // 设置最右边的数为分界线
    int pivot = a[r];
    // 元素移动
    int k = l - 1;
    for (int j = l; j < r; ++j)
        if (a[j] < pivot) swap(a[j], a[++k]); 
    swap(a[r], a[++k]); 
    if (l < k - 1) quick_sort(l, k - 1); // 如果序列的分界线左边的子段长度>1,排序
    if (k + 1 < r) quick_sort(k + 1, r); // 如果序列的分界线右边的子段长度>1,排序
    // 上面的过程结束后,到这里左子段和右子段已经分别排好序。又因为确定分界线以后的移动操作
    // 保证了左子段中的元素都小于等于分界线,右子段中的元素都大于分界线。所以整个序列也是有序的。
} 
int main() { 
    // 输入
    scanf("%d", &n); 
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]); 
    // 快速排序
    quick_sort(1, n); 
    // 输出
    for (int i = 1; i <= n; ++i) printf("%d ", a[i]);  
    return 0; 
}

复杂度分析


空间复杂度


首先该算法的空间复杂度是O(n)O(n),具体来说,在整个排序过程中,元素的移动都在原始数组中进行。所以快速排序是一种原地排序算法。


时间复杂度


可以看出,在「详细算法描述」中,我们的算法分为若干层。每一层中都是分治法的三个步骤:我们首先进行问题拆分,然后进入下一层,下一层的问题解决后,我们返回这一层进行子问题解的合并。


image.png


我们首先分析对1~n的n个数字进行快速排序的情况。


在每一层中,问题拆分的复杂度是O(n)O(n),因为我们移动数组元素的时候,需要将每个子段扫一遍。那么把所有层的子段一起看,就相当于在每一层都把整个序列完整扫了一遍。对于子段解的合并,其复杂度是O(1)O(1),因为有分界线的存在,当我们把左边和右边都排好序后,它们和分界线元素一起天然形成了原序列的完整排序。


那么一共有多少层呢?因为每次我们都知道当前子段的中位数,所以可以保证每次划分,两个字段长度比较平衡,所以下一层子段的长度都比上一层减少了一半,直到长度为1算法停止。所以整个算法有\log nlogn层。


那么我们分析出在这种情况下,算法的复杂度是O(n\log n)O(nlogn)。这样,在1秒之内,计算机能非常轻松地排序10^6106及以上的数据。


但对于任意n个数的排序,每次划分情况取决于选取的分界线情况。如果每次分界线刚好取到最小值或者最大值,会导致划分时所有数字都会移动到同一边,整个算法的复杂度也会下降为O(n^2)O(n2)。如下图:



我们很容易想到两种尽量避免出现这种情况的方法:


在排序之前,先把整个数组随机打乱顺序。

在选取分界线时,与之前固定选取某个位置的方法相比,我们换成随机选择分界线的位置。

这两种方法都能极大概率避免上面提到的极端情况的发生。


分治思想:


快速排序用到的一个很重要的思想就是分治思想,也是分治法运用在排序中的很重要的实例。


分治思想是一种“分而治之”的思想,反应在解决问题当中,就是将一个复杂问题不断分解为规模更小、更容易解决的问题,从而提升解决问题的效率。而分治法就是基于分治思想得到的解决问题的方法,它分为下面三个步骤:


问题的拆分。例如在快速排序中,我们以某个元素为分界线,将待排序的数字分为两部分。

解决子问题。例如在快速排序中,如果子问题的规模为1,我们就直接解决它,否则,我们就使用和划分主问题同样的办法继续划分子问题直到子问题规模达到很容易直接解决为止。

合并子问题的解。例如在快速排序中,我们将左边右边分别排序后,将前后排好序的部分与中间的分界线连接,形成主问题的解。

分治思想在算法领域有非常广泛的应用,在很多分解和合并都非常容易的问题上,分治法都能够提升其算法效率。


总结


快速排序是一种基于分治法的排序。其基本思想在于固定一个分界线,将整个序列按照小于分界线和大于分界线划分,然后再分别对划分好的子段进行排序。


快速排序的时间复杂度在理想情况下是O(n \log n)O(nlogn),但如果选取分界线每次都是子段中的最大值或最小值的话,时间复杂度可能会退化到O(n^2)O(n2)。在内存使用上,因为整个移动过程都在原数组中进行的,所以属于原地排序。


sort函数是C++标准模板库(STL)中一种对快速排序的优化实现,可以通过传入头指针、尾指针和比较函数来对数组中的对象进行排序。


快速排序示例:


将数组{2, 3, 1, 5, 4}从小到大排列。


不使用sort函数


将「整体框架」和「移动元素」进行合并,我们得到快速排序完整代码:

// 该代码参考 https://www.geeksforgeeks.org/quick-sort/
#include <bits/stdc++.h>
#define N 100010 
using namespace std; 
int n; 
int a[N]; 
void quick_sort(int l, int r) { 
    // 设置最右边的数为分界线
    int pivot = a[r];
    // 元素移动
    int k = l - 1;
    for (int j = l; j < r; ++j)
        if (a[j] < pivot) swap(a[j], a[++k]); 
    swap(a[r], a[++k]); 
    if (l < k - 1) quick_sort(l, k - 1); // 如果序列的分界线左边的子段长度>1,排序
    if (k + 1 < r) quick_sort(k + 1, r); // 如果序列的分界线右边的子段长度>1,排序
    // 上面的过程结束后,到这里左子段和右子段已经分别排好序。又因为确定分界线以后的移动操作
    // 保证了左子段中的元素都小于等于分界线,右子段中的元素都大于分界线。所以整个序列也是有序的。
} 
int main() { 
    // 输入
    scanf("%d", &n); 
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]); 
    // 快速排序
    quick_sort(1, n); 
    // 输出
    for (int i = 1; i <= n; ++i) printf("%d ", a[i]);  
    return 0; 
}


使用sort函数


sort函数有三个参数,分别为头指针、尾指针和比较函数,其中如果排序对象定义了小于号的话,比较函数可省略。例如对于一个长为n的数组排序:

#include <bits/stdc++.h>
using namespace std;
int a[10] = {2, 3, 1, 5, 4};
int n = 5;
int main() {
    sort(a, a + n);  //sort函数的两个参数,头指针和尾指针
    for (int i = 0; i < n; ++i) cout << a[i] << ' ';
    cout << endl;
}

例题:


image.png



相关文章
|
2月前
|
算法 数据处理 C语言
C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合
本文深入解析了C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合,旨在帮助读者掌握这一高效的数据处理方法。
49 1
|
2月前
|
机器学习/深度学习 算法 数据挖掘
K-means聚类算法是机器学习中常用的一种聚类方法,通过将数据集划分为K个簇来简化数据结构
K-means聚类算法是机器学习中常用的一种聚类方法,通过将数据集划分为K个簇来简化数据结构。本文介绍了K-means算法的基本原理,包括初始化、数据点分配与簇中心更新等步骤,以及如何在Python中实现该算法,最后讨论了其优缺点及应用场景。
117 4
|
11天前
|
存储 运维 监控
探索局域网电脑监控软件:Python算法与数据结构的巧妙结合
在数字化时代,局域网电脑监控软件成为企业管理和IT运维的重要工具,确保数据安全和网络稳定。本文探讨其背后的关键技术——Python中的算法与数据结构,如字典用于高效存储设备信息,以及数据收集、异常检测和聚合算法提升监控效率。通过Python代码示例,展示了如何实现基本监控功能,帮助读者理解其工作原理并激发技术兴趣。
49 20
|
3天前
|
存储 算法 安全
基于红黑树的局域网上网行为控制C++ 算法解析
在当今网络环境中,局域网上网行为控制对企业和学校至关重要。本文探讨了一种基于红黑树数据结构的高效算法,用于管理用户的上网行为,如IP地址、上网时长、访问网站类别和流量使用情况。通过红黑树的自平衡特性,确保了高效的查找、插入和删除操作。文中提供了C++代码示例,展示了如何实现该算法,并强调其在网络管理中的应用价值。
|
1天前
|
存储 算法 安全
基于哈希表的文件共享平台 C++ 算法实现与分析
在数字化时代,文件共享平台不可或缺。本文探讨哈希表在文件共享中的应用,包括原理、优势及C++实现。哈希表通过键值对快速访问文件元数据(如文件名、大小、位置等),查找时间复杂度为O(1),显著提升查找速度和用户体验。代码示例展示了文件上传和搜索功能,实际应用中需解决哈希冲突、动态扩容和线程安全等问题,以优化性能。
|
8天前
|
算法 安全 C++
用 C++ 算法控制员工上网的软件,关键逻辑是啥?来深度解读下
在企业信息化管理中,控制员工上网的软件成为保障网络秩序与提升办公效率的关键工具。该软件基于C++语言,融合红黑树、令牌桶和滑动窗口等算法,实现网址精准过滤、流量均衡分配及异常连接监测。通过高效的数据结构与算法设计,确保企业网络资源优化配置与安全防护升级,同时尊重员工权益,助力企业数字化发展。
32 4
|
2月前
|
存储 算法 搜索推荐
Python 中数据结构和算法的关系
数据结构是算法的载体,算法是对数据结构的操作和运用。它们共同构成了计算机程序的核心,对于提高程序的质量和性能具有至关重要的作用
|
21天前
|
存储 缓存 安全
Java 集合江湖:底层数据结构的大揭秘!
小米是一位热爱技术分享的程序员,本文详细解析了Java面试中常见的List、Set、Map的区别。不仅介绍了它们的基本特性和实现类,还深入探讨了各自的使用场景和面试技巧,帮助读者更好地理解和应对相关问题。
37 5
|
2月前
|
数据采集 存储 算法
Python 中的数据结构和算法优化策略
Python中的数据结构和算法如何进行优化?
|
2月前
|
算法
数据结构之路由表查找算法(深度优先搜索和宽度优先搜索)
在网络通信中,路由表用于指导数据包的传输路径。本文介绍了两种常用的路由表查找算法——深度优先算法(DFS)和宽度优先算法(BFS)。DFS使用栈实现,适合路径问题;BFS使用队列,保证找到最短路径。两者均能有效查找路由信息,但适用场景不同,需根据具体需求选择。文中还提供了这两种算法的核心代码及测试结果,验证了算法的有效性。
112 23