LeetCode刷题系列(三)排序

简介: LeetCode刷题系列(三)排序

时间复杂度为O ( n 2 ) O(n^{2})O(n2)

  下面是时间复杂度为O ( n 2 ) O(n^{2})O(n2)的排序算法:

冒泡排序

  1. 冒泡排序的思想非常简单,一开始交换的区间是0 ∼ n − 1 ,之后第一个数和第二个数开始比较,将大的数据放在后面。然后是第二个数和第三个数开始比较,哪个大哪个就放在后面,这样交换过去,最大的数据会放在数组的最后一个位置。此时第一轮排序就完成了。
  2. 之后把数组的范围从0 ∼ n − 1 变为0 ∼ n − 2 ,重新开始排序。这样开始新的一轮冒泡,这样整个数组倒数第二大的数据会放在数组的倒数第二个位置上面。

  依次循环做下去,知道数组的范围变成了只有一个数的时候,整个数组排序完成。

  重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序错误就把他们交换过来。比如升序排序中nums[j] < nums[j+1]的话,我们就将其交换,让大的数据放在后面。

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        for(int i=nums.size()-1; i>0; --i){
            for(int j=0; j<i; ++j){
                if(nums[j] >nums[j+1]) swap(nums[j], nums[j+1]);
            }
        }
        return nums;
    }
};

  冒泡排序的最好情况可以达到O ( n ),就是用一个布尔变量记录是否已经全部排好序了,也就是是否交换过了,如果交换过了则为false,否则为true,这样当数组已经是排好序的情况下,我们就不用再做循环了。


选择排序

  1. 一开始从数组0 ∼ n − 1 上选择一个最小值,然后将其放在位置0上。
  2. 然后在1 ∼ n − 1 范围上选择一个最小值,将其放在位置1上。

  依次进行下去,随着范围的缩小,到最终范围里面只有一个元素的时候,整个数组就变成了一个有序数组。

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        for(int i=0; i<nums.size();++i){
            int min_pos = i; // 最小的数字
            for(int j=i+1;j<nums.size();++j){
                if(nums[j]<nums[min_pos]) min_pos=j;
            }
            swap(nums[min_pos], nums[i]); // 最小值的位置和起始位置交换
        }
        return nums;
    }
};

  进一步的优化我们可以一次循环遍历的时候找到一个最小值和一个最大值,最小值放到前面,最大值放到后面。

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        chose_sort(nums, 0, nums.size()-1);
        return nums;
    }
    void chose_sort(vector<int>& nums, int left, int right){
        if(left>right) return;
        int min_pos=left, max_pos=right; // 初始化最小值和最大值的位置
        for(int j=left;j<=right;++j){ // 找小的,交换小的
            if(nums[j]<nums[min_pos]) min_pos=j;
        }
        swap(nums[left], nums[min_pos]);
        for(int j=left;j<=right;++j){ // 找大的,交换大的
            if(nums[j]>nums[max_pos]) max_pos=j;
        }
        swap(nums[max_pos], nums[right]);
        chose_sort(nums, left+1, right-1);
    }
};

插入排序

  插入排序的时间复杂度仍然为O ( n 2 ) 。首先是位置0上的数和位置1上的数开始比较:

  1. 如果位置1上的数更小,那么它就和位置0上的数开始交换。
  2. 接下来考察位置2上的数,将位置2上的数的值记为aa就和前面的数进行比较。如果它比位置1上的数更小,那么a就和位置1上的数进行交换,交换之后a再和位置0上的数进行比较,如果它还是变小,就和位置0上的数进行比较。
  3. 接下来对于位置k上的数,假设它的位置记为b的话,b就依次和前面的数进行比较,如果b一直小于前面的数,就一直进行这样的交换过程。直到它前面的数小于等于b,那么b就插入当前位置。
class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        for(int i=1; i<nums.size(); ++i){
            for(int j=i; j>0; --j){
                if(nums[j]<nums[j-1]) swap(nums[j], nums[j-1]);
            }
        }
        return nums;
    }

  我们依次从1位置到n − 1 位置,所有的数都进行这样的插入的话,最终整个数组就变成有序了。


时间复杂度为O ( N ∗ l o g N )


以下是时间复杂度为O ( N ∗ l o g N ) 的排序算法:

归并排序

  归并排序的时间复杂度为O ( N ∗ l o g N ) 。归并排序首先要将数组中的每一个数成为长度为1的有序区间,然后把相邻的长度为1的有序区间之间进行合并,得到最大长度为2的有序区间。接下来再把相邻有序区间进行合并,得到最大长度为4的有序区间。依次这样进行下去,48816直到数组中所有的数合并到一个有序区间。整个过程结束。

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        vector<int> tmp(nums.size(),0);
        merge_sort(nums, tmp, 0, nums.size()-1);
        return nums;
    }
    void merge_sort(vector<int>& nums, vector<int>& tmp, int left, int right){
        if(left>=right) return;
        int mid = left + (right-left)/2; // 找到中间位置就开始一直拆分
        merge_sort(nums, tmp, left, mid);
        merge_sort(nums, tmp, mid+1, right);
        merge(nums, tmp, left, right); // 拆分到最后将其合并
    }
    void merge(vector<int>& nums, vector<int>& tmp, int left, int right){
        int i=left, mid=left+(right-left)/2, j=mid+1, pos=left;
        while(i<=mid && j<=right){ // 在没有越界的情况下哪个元素小,哪个元素就放入tmp中
            if(nums[i]<nums[j]) tmp[pos++]=nums[i++];
            else tmp[pos++]=nums[j++];
        }
        while(i<=mid) tmp[pos++]=nums[i++]; // 左半边剩余没有排完的元素依次放进去
        while(j<=right) tmp[pos++]=nums[j++]; // 右半边剩余没有排完的元素依次放进去
        for(int i=left;i<=right;++i){ // tmp中的元素放回到nums[i]中
            nums[i]=tmp[i];
        }
    }
};

快速排序

  快速排序是随机地从数组中选择一个数,如果一个数小于等于这个被选中的数,统一放在这个数的左边,如果一个数大于这个被选中的数就放在右边。接下来对左右两边分别递归地调用快速排序的过程即可。这样就使得整个数组都有序了。

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        quick_sort(nums, 0, nums.size()-1);
        return nums;
    }
    void quick_sort(vector<int>& nums, int left, int right){
        if(left>=right) return; // 左边的数字大于或等于右边的数字的时候,我们直接返回
        int i=left, j=right;
        while(i<j){
            while(i<j && nums[j] >= nums[left]) --j;
            while(i<j && nums[i] <= nums[left]) ++i;
            swap(nums[i], nums[j]);
        }
        // left与中间元素交换位置,这一步之后才是真正的小的在左边,大的在右边
        swap(nums[left], nums[i]); 
        quick_sort(nums, left, i-1);
        quick_sort(nums, i+1, right);
    }
};

堆排序


希尔排序

  希尔排序实际上是插入排序的一个改良算法。在插入排序中,一个数想插入到前面的有序的序列中的话,每次是和前面的一个数进行比较。希尔排序的步长是逐渐调整的,准确来说是从大到小逐渐调整的。

  假设初始数组为【6,5,3,1,8,7,2,4】

  1. 当初始步长为3的情况下,【6,5,3】作为前3个数,是不需要调整的。从1开始,向前跳3位,来与6进行比较,发现16小,于是16进行交换,此时数组变为:【1,5,3,6,8,7,2,4】
  2. 之后再处理下一个数据88向前跳3位,与5进行比较。与5比较发现比5要大,所以直接停止交换过程,进行下一个数的调整。
  3. 之后再处理下一个数,来到7这个数,7向前跳3位,发现7又是大于3的。所以7这一步也不需要进行交换。
  4. 接下来又来到2这个位置,2向前跳3位,与6进行比较,26小,所以26之间进行交换。此时数组变为:【1,5,3,2,8,7,6,4】2再向前跳3位,此时发现2应该与1进行比较,它比1要大,所以2停在原位置。整个交换过程停止。
  5. 接下来处理4这个数,4向前跳3位,来到8这个数,4与8比,4比8小,所以4与8互换位置,此时数组变为:【1,5,3,2,4,7,6,8】。接下来4再向前跳3个位置,发现其小于5,所以4和5之间进行交换,此时数组变为:【1,4,3,2,5,7,6,8】,此时4来到了位置1,再向前跳3位的话就越接了。此时整个步长为3的插入排序就结束了。

  接下来还有步长为2,步长为1的插入排序过程。希尔排序完全取决于步长的选择,步长越优,时间复杂度就会越低。步长选择越劣的话,时间复杂度就越趋近于O ( N 2 ) 级别。


class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        for(int gap=nums.size()/2; gap>0; gap/=2){
            for(int i=gap; i<nums.size(); ++i){
                for(int j=i; j>gap-1; j-=gap){ // 每次跳跃gap间隔
                    if(nums[j]<nums[j-gap]) swap(nums[j], nums[j-gap]);
                }
            }
        }
        return nums;
    }
};

时间复杂度为O ( N )


  以下是时间复杂度为O ( N ) 的排序算法。这里的排序算法不是基于比较的排序算法,它思想的原型都来自于桶排序。桶排序不是具体的实现,它只是一种思想。比较常规的有计数排序基数排序


计数排序


  比如现在需要把员工按照身高来进行排序。我们知道一个成年人的身高基本是在1米到3米之间。对应的厘米数就是100300。所以依据厘米数依次建立从100300号桶。然后把所有的员工按照他们对应的各自的身高,把它放到相对应的桶里面。当所有员工都进入桶之后,从100号桶开始,依次倒出员工。此时员工被倒出的顺序就是按照身高排序的顺序。


基数排序


  假设我们要排序的数都是十进制的,然后我们准备从0-9这样十个桶。然后将每位数依据个位上的数值选择进入几号桶。当所有的数都进入桶之后,我们再从0号桶到9号桶依次倒出所有的数,组成了一个序列。

  接下来依据这个序列的顺序,按照十位上的数值选择进入0号桶到9号桶中的一个。当所有的数再次进入桶中之后,我再把从0号桶到9号桶中的数据依次倒出。再组成一个新的序列,下一轮依据这样一个序列的顺序依次考察每一个数它百位上的数值进入0号桶到9号桶中的一个。

  依次这样迭代下去,最后是依据最高位的数值选择进入0号桶还是9号桶。最后一次倒出的序列就是整个数排序的序列了。


经典排序算法的空间复杂度


  1. 时间复杂度为O ( n 2 ) 插入排序选择排序冒泡排序的空间复杂度都是O ( 1 ) )的。
  2. O ( N ∗ l o g N ) 的排序算法中,堆排序希尔排序的空间复杂度都是O ( 1 )
  3. 快速排序的空间复杂度为O ( l o g N ) ∼ O ( N ) ,这完全取决于它划分的情况。
  4. 归并排序它的空间复杂度为O ( N ) 。虽然很多书上写过他的空间复杂度可以优化到O ( 1 ) ,通过手调算法。但是手调算法时间复杂度就会上升。
  5. 对于时间复杂度为O ( N ) 的不基于比较的排序算法来说,他的空间复杂度是O ( M ) ,这个M 是选择桶的数量。


经典排序算法的稳定性


  稳定性的概念说的是,假定待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,称这种排序算法是稳定的,否者称为不稳定的。

稳定的排序算法

  冒泡排序、插入排序、归并排序、计数排序、基数排序、桶排序。

不稳定的排序算法

  选择排序、快速排序、希尔排序、堆排序

相关文章
|
3月前
|
Unix Shell Linux
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行
本文提供了几个Linux shell脚本编程问题的解决方案,包括转置文件内容、统计词频、验证有效电话号码和提取文件的第十行,每个问题都给出了至少一种实现方法。
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行
|
4月前
|
搜索推荐 索引 Python
【Leetcode刷题Python】牛客. 数组中未出现的最小正整数
本文介绍了牛客网题目"数组中未出现的最小正整数"的解法,提供了一种满足O(n)时间复杂度和O(1)空间复杂度要求的原地排序算法,并给出了Python实现代码。
123 2
|
1月前
|
机器学习/深度学习 人工智能 自然语言处理
280页PDF,全方位评估OpenAI o1,Leetcode刷题准确率竟这么高
【10月更文挑战第24天】近年来,OpenAI的o1模型在大型语言模型(LLMs)中脱颖而出,展现出卓越的推理能力和知识整合能力。基于Transformer架构,o1模型采用了链式思维和强化学习等先进技术,显著提升了其在编程竞赛、医学影像报告生成、数学问题解决、自然语言推理和芯片设计等领域的表现。本文将全面评估o1模型的性能及其对AI研究和应用的潜在影响。
40 1
|
3月前
|
数据采集 负载均衡 安全
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
本文提供了多个多线程编程问题的解决方案,包括设计有限阻塞队列、多线程网页爬虫、红绿灯路口等,每个问题都给出了至少一种实现方法,涵盖了互斥锁、条件变量、信号量等线程同步机制的使用。
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
|
2月前
【LeetCode-每日一题】 删除排序数组中的重复项
【LeetCode-每日一题】 删除排序数组中的重复项
21 4
|
2月前
|
索引
Leetcode第三十三题(搜索旋转排序数组)
这篇文章介绍了解决LeetCode第33题“搜索旋转排序数组”的方法,该问题要求在旋转过的升序数组中找到给定目标值的索引,如果存在则返回索引,否则返回-1,文章提供了一个时间复杂度为O(logn)的二分搜索算法实现。
22 0
Leetcode第三十三题(搜索旋转排序数组)
|
4月前
|
存储 算法
LeetCode第83题删除排序链表中的重复元素
文章介绍了LeetCode第83题"删除排序链表中的重复元素"的解法,使用双指针技术在原链表上原地删除重复元素,提供了一种时间和空间效率都较高的解决方案。
LeetCode第83题删除排序链表中的重复元素
|
4月前
|
算法 索引
LeetCode第34题在排序数组中查找元素的第一个和最后一个位置
这篇文章介绍了LeetCode第34题"在排序数组中查找元素的第一个和最后一个位置"的解题方法,通过使用双指针法从数组两端向中间同时查找目标值,有效地找到了目标值的首次和最后一次出现的索引位置。
LeetCode第34题在排序数组中查找元素的第一个和最后一个位置
|
4月前
|
算法 Python
【Leetcode刷题Python】 LeetCode 2038. 如果相邻两个颜色均相同则删除当前颜色
本文介绍了LeetCode 2038题的解法,题目要求在一个由'A'和'B'组成的字符串中,按照特定规则轮流删除颜色片段,判断Alice是否能够获胜,并提供了Python的实现代码。
57 3
|
4月前
|
Python
【Leetcode刷题Python】50. Pow(x, n)
本文介绍了LeetCode第50题"Pow(x, n)"的解法,题目要求实现计算x的n次幂的函数,文章提供了递归分治法的详细解析和Python实现代码。
28 1