首页 > 编程语言 >5 排序算法总结

5 排序算法总结

时间:2023-09-10 15:23:35浏览次数:47  
标签:总结 nums int 元素 算法 序列 排序

5 排序算法总结

首先总结表如下:

排序方法 平均时间复杂度 最好情况 最坏情况 空间复杂度 是否稳定 排序方式
冒泡排序 \(O(n^2)\) \(O(n)\) \(O(n^2)\) \(O(1)\) 稳定 内部排序
选择排序 \(O(n^2)\) \(O(n^2)\) \(O(n^2)\) \(O(1)\) 不稳定 内部排序
插入排序 \(O(n^2)\) \(O(n)\) \(O(n^2)\) \(O(1)\) 稳定 内部排序
希尔排序 \(O(n\log n)\) \(O(n\log ^2 n)\) \(O(n\log ^2 n)\) \(O(1)\) 不稳定 内部排序
快速排序 \(O(n\log n)\) \(O(n\log n)\) \(O(n^2)\) \(O(\log n)\) 不稳定 内部排序
归并排序 \(O(n\log n)\) \(O(n\log n)\) \(O(n\log n)\) \(O(n)\) 稳定 外部排序
堆排序 \(O(n\log n)\) \(O(n\log n)\) \(O(n\log n)\) \(O(1)\) 不稳定 内部排序
桶排序 \(O(n+k)\) \(O(n+k)\) \(O(n^2)\) \(O(k)\) 稳定 外部排序
计数排序 \(O(n+k)\) \(O(n+k)\) \(O(n+k)\) \(O(k)\) 稳定 外部排序

1 冒泡排序

1.1 算法简述

两两比较,选出较大值,每一轮选出单轮最大值到数组末端,这个过程就像泡泡变大的过程,所以交冒泡算法。

代码

void bubbleSort(vector<int>& nums) {
    for (int i = 0; i < nums.size() - 1; i++) {
        //int flag = 0;
        for (int j = 0; j < nums.size() - 1 - i; j++) {
            if (nums[j] > nums[j + 1]) {
                swap(nums[j], nums[j + 1]);
                //flag = 1;
            }
        }
        //if(flag == 0) return;
    }
}

1.2 优化

定义一个flag,只要进行交换就置为1.当某一次内层for循环没有交换过flag为0,那么说明已经排序好了

1.3 最好情况

数组已经是正序排列时,仅有比较,没有交换

1.4 最坏情况

数组是反序排列时,每一轮都需要交换

2选择排序

选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。

2.1 算法简述

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。

再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。

重复第二步,直到所有元素均排序完毕。

2.2 代码

void selection_sort(vector<int>& nums) {
    for (int i = 0; i < nums.size(); i++) {
        int min = i;
        for (int j = i + 1; j < nums.size(); j++) {
            if (nums[min] > nums[j]) min = j;
        }
        swap(nums[min], nums[i]);
    }
}

3 插入排序

插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。

3.1 算法简述

将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。

从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

3.2 代码

void insertion_sort(vector<int>& nums) {
    for (int i = 1; i < nums.size(); i++) {
        int num = nums[i];
        int j = i - 1;
        while (j >= 0 && num < nums[j]) {
            // 当j大于0且有序区间值大于num时,向左移动
            nums[j + 1] = nums[j];
            j--;
        }
        //优化,折半查找法
        nums[j + 1] = num;
    }
}

3.3优化

已排序的序列中,序列元素都是按照顺序排列的,直接比较已排序序列的中位数,这个中间的位置将一排序列分为左右两部分,通过一次比较后,就缩小了比较的范围,重复这样的操作,需要插入的元素就找到了合适的位置了。

4 希尔排序

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。

4.1 算法简述

选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;

按增量序列个数 k,对序列进行 k 趟排序;

每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

4.2 代码

void shell_sort(vector<int>& nums) {
    int h = 1;
    while (3 * h < nums.size()) {
        h = 3 * h + 1;
    }
    while (h >= 1) {
        for (int i = h; i < nums.size(); i++) {
            for (int j = i; j >= h && nums[j] < nums[j - h]; j -= h) {
                swap(nums[j], nums[j - h]);
            }
        }
        h = h / 3;
    }
}

5 快速排序

快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。

快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。

快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好,可是这是为什么呢,我也不知道。好在我的强迫症又犯了,查了 N 多资料终于在《算法艺术与信息学竞赛》上找到了满意的答案:

快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。

5.1 算法简述

  1. 从数列中挑出一个元素,称为 "基准"(pivot);

  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;

  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;

5.2 代码

// 快速排序
int Paritition(vector<int>& nums, int low, int high) {
    //返回基准pivot的nums的序号
    int pivot = nums[low];
    while (low < high) {
        //将nums分为小于基准和大于基准的两侧
        while (low < high && nums[high] >= pivot) --high;
        nums[low] = nums[high];
        while (low < high && nums[low] <= pivot) ++low;
        nums[high] = nums[low];
    }
    nums[low] = pivot;
    return low;
}

void QuickSort(vector<int>& nums, int low, int high) {
    if (low < high) {
        int pivot = Paritition(nums, low, high);
        QuickSort(nums, low, pivot - 1);//递归小于基准部分
        QuickSort(nums, pivot + 1, high);//递归大于基准部分
    }
}

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

6 归并排序

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
自下而上的迭代;

和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。

6.1 算法简述

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;

  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;

  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;

  4. 重复步骤 3 直到某一指针达到序列尾;

  5. 将另一序列剩下的所有元素直接复制到合并序列尾。

6.2 代码

// 归并排序
void Merge(vector<int>& nums, int front, int mid, int end) {
    vector<int> LeftSubArray(nums.begin() + front, nums.begin() + mid + 1);
    vector<int> RightSubArray(nums.begin() + mid + 1, nums.begin() + end + 1);
    int idxLeft = 0, idxRight = 0;
    LeftSubArray.insert(LeftSubArray.end(), numeric_limits<int>::max());
    RightSubArray.insert(RightSubArray.end(), numeric_limits<int>::max());
    for (int i = front; i <= end; i++) {
        if (LeftSubArray[idxLeft] < RightSubArray[idxRight]) {
            nums[i] = LeftSubArray[idxLeft];
            idxLeft++;
        }
        else {
            nums[i] = RightSubArray[idxRight];
            idxRight++;
        }
    }
}

void MergeSort(vector<int>& nums, int front, int end) {
    if (front >= end) return;
    int mid = (front + end) / 2;
    MergeSort(nums, front, mid);
    MergeSort(nums, mid + 1, end);
    Merge(nums, front, mid, end);
}

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

7 堆排序

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
堆排序的平均时间复杂度为 Ο(nlogn)。

7.1 算法简述

  1. 创建一个堆 H[0……n-1];

  2. 把堆首(最大值)和堆尾互换;

  3. 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;

  4. 重复步骤 2,直到堆的尺寸为 1。

7.2 代码

void max_heapify(int arr[], int start, int end) {
    int dad = start;
    int son = dad * 2 + 1;
    while (son <= end) { 
        if (son + 1 <= end && arr[son] < arr[son + 1]) 
            son++;
        if (arr[dad] > arr[son]) 
            return;
        else { 
            swap(arr[dad], arr[son]);
            dad = son;
            son = dad * 2 + 1;
        }
    }
}

void heap_sort(int arr[], int len) {
    for (int i = len / 2 - 1; i >= 0; i--)
        max_heapify(arr, i, len - 1);
    for (int i = len - 1; i > 0; i--) {
        swap(arr[0], arr[i]);
        max_heapify(arr, 0, i - 1);
    }
}

8 计数排序

计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。

由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。

类似于哈希法

8.1 算法简述

(1)找出待排序的数组中最大和最小的元素
(2)统计数组中每个值为i的元素出现的次数,存入数组C的第i项
(3)对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
(4)反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1

8.2 代码


9 桶排序

9.1 算法简述

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

在额外空间充足的情况下,尽量增大桶的数量
使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

9.2 代码

const int BUCKET_NUM = 10;
struct ListNode{
        explicit ListNode(int i=0):mData(i),mNext(NULL){}
        ListNode* mNext;
        int mData;
};

ListNode* insert(ListNode* head,int val){
        ListNode dummyNode;
        ListNode *newNode = new ListNode(val);
        ListNode *pre,*curr;
        dummyNode.mNext = head;
        pre = &dummyNode;
        curr = head;
        while(NULL!=curr && curr->mData<=val){
                pre = curr;
                curr = curr->mNext;
        }
        newNode->mNext = curr;
        pre->mNext = newNode;
        return dummyNode.mNext;
}


ListNode* Merge(ListNode *head1,ListNode *head2){
        ListNode dummyNode;
        ListNode *dummy = &dummyNode;
        while(NULL!=head1 && NULL!=head2){
                if(head1->mData <= head2->mData){
                        dummy->mNext = head1;
                        head1 = head1->mNext;
                }else{
                        dummy->mNext = head2;
                        head2 = head2->mNext;
                }
                dummy = dummy->mNext;
        }
        if(NULL!=head1) dummy->mNext = head1;
        if(NULL!=head2) dummy->mNext = head2;
       
        return dummyNode.mNext;
}

void BucketSort(int n,int arr[]){
        vector<ListNode*> buckets(BUCKET_NUM,(ListNode*)(0));
        for(int i=0;i<n;++i){
                int index = arr[i]/BUCKET_NUM;
                ListNode *head = buckets.at(index);
                buckets.at(index) = insert(head,arr[i]);
        }
        ListNode *head = buckets.at(0);
        for(int i=1;i<BUCKET_NUM;++i){
                head = Merge(head,buckets.at(i));
        }
        for(int i=0;i<n;++i){
                arr[i] = head->mData;
                head = head->mNext;
        }
}

9.3 最好情况

当输入的数据可以均匀的分配到每一个桶中。

9.4 最坏情况

当输入的数据被分配到了同一个桶中。

标签:总结,nums,int,元素,算法,序列,排序
From: https://www.cnblogs.com/mobbu/p/17691298.html

相关文章

  • 练习:冒泡排序法
    冒泡排序法:是在每一轮排序结束之后都有一个体积最大的气泡冒出来,这也正是冒泡排序法名字的由来。(1)从集合第一个元素开始,每两个相邻的元素进行大小比较,令数值较大的元素向后移动,即交换两个元素的位置,不断对比直至数组的末尾。经过第一趟对比,找到整个集合中最大的元素,并将其移动到......
  • 【Redis总结】
    【一】Redis介绍与安装什么是Redis非关系型数据库和关系型数据库Redis的特点Redis的安装Redis的连接与停止【二】Redis使用Redis普通连接与连接池Redis之字符串操作Redis之hash类型操作Redis之列表操作Redis之其他操作Redis之管道事务的四大特性Re......
  • 代码随想录算法训练营-回溯算法|491.递增子序列
    491. 递增子序列 不对原数组进行排序,利用set对同层的子集进行去重。1classSolution:2deffindSubsequences(self,nums):3result=[]4path=[]5self.backtracking(nums,0,path,result)6returnresult78......
  • 机器学习算法原理实现——决策树里根据信息增益选择特征
    先说熵的定义:  再看信息增益信息增益是一种用于特征选择的指标,用于衡量特征对于数据集分类的贡献程度。它基于信息熵的概念,通过比较特征划分前后的信息熵差异来评估特征的重要性。信息熵是衡量数据集纯度的指标,表示数据集中的不确定性或混乱程度。信息熵越高,数据集的不确......
  • 9月4日总结
    欢迎访问我的GitHub这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos本篇概览本篇概览这是道高频面试题,值得一看首先,这道题的难度是中等来看题目描述:给你一个整数n,返回和为n的完全平方数的最少数量。完全平方数是一个整数,其值等......
  • 9月6日总结
    ,其中领域对象(实体和值对象)包含了业务逻辑和状态,并且负责自己的行为。在Spring框架中,如果你想使用充血模型,并且需要依赖其他服务或组件,在现在的Java开发中基本上都离不开Spring的框架,每个领域服务和公共服务都转换成Spring的bean对象,我们创建领域对象的时候采用new关键字创建对象,......
  • 9月5日总结
    MacSpringBoot项目Gradle7.3转Maven手把手教学,包学会~导读#最近我手上有个使用Gradle构建的项目,国内使用Gradle的人相对较少。而且我也觉得Gradle的依赖管理方式有些复杂,让我感到有些困惑。因此,我想将项目转换为Maven构建方式。Maven构建的SpringBoot的方式,想必大家都......
  • 9月7日总结
    }这种接口是我们部门以前审查出来的其中一个,类似这样的接口还有很多。这些接口都是不同的同事在紧凑的工作任务中写的,慢慢就积累出了一堆。还有些是为了方便,直接通过代码生成器生成的,而代码生成器是把常用的CRUD接口都给你生成出来,如果研发人员没有责任心,可能就直接不管了,想......
  • 9月10日总结
    在2023年早些时候,Go1.20发布了供用户测试的概要版本的基于性能分析的优化(PGO)。经过解决预览版已知的限制,并得益于社区反馈和贡献的进一步改进,Go1.21中的PGO支持已经准备好供一般生产使用!请查阅性能分析优化用户指南以获取完整的文档。下面,我们将通过一个示例来演示如何使用PGO......
  • 9.8每日总结
    今日学习时间两小时,配置了idea的环境且能够在idea上进行javaweb编程,之后进行了一部分远程连接hive的尝试工作。连接类packagecom.example.hive.config;importjavax.sql.DataSource;importlombok.Data;importorg.springframework.beans.factory.annotation.Qualifier;i......