【基础算法】排序算法详述

系列综述:
💞目的:本系列是个人整理为了秋招算法的,整理期间苛求每个知识点,平衡理解简易度与深入程度。
🥰来源:材料主要源于代码随想录进行的,每个算法代码参考leetcode高赞回答和其他平台热门博客,其中也可能含有一些的个人思考。
🤭结语:如果有帮到你的地方,就点个赞关注一下呗,谢谢🎈🎄🌷!!!
🌈【C++】秋招&实习面经汇总篇🌈



😊点此到文末惊喜↩︎

排序算法概述

算法简介

  1. 作用:将一组无序的记录序列调整为有序的记录序列
  2. 类型
    • 内部排序:待排序列完全存放在内存中所进行的排序过程,适合不太大的元素序列。
    • 外部排序:待排序的记录存储在外存储器上,通常待排序的文件无法一次装入内存。
  3. 逆序对
    • 一个序列中有两个数前面一个数比后面一个数大
    • 排序实质就是减少逆序对的过程
  4. 有序无序分区思想
    • 每次通过某种算法迭代,将无序区的最值添加到有序区后面
    • 每次从无序区遍历,不超过个分界线
    • 优化:如果迭代一次交换都没有发生的话,是可以说明这个序列都是有序的,可以直接推出。
      在这里插入图片描述
  5. 常见排序算法
    • 冒泡排序:属于暴力排序,对cpu的运算负载非常大
    • 插入排序,希尔排序,选择排序:属于低阶排序算法,一般用于其他排序算法的辅助性工具
    • 堆排序:要求空间复杂度低,没有用到辅助性的数组,所以它占用的空间可以忽略为待排序数组的大小,耗时较短,对栈的依赖适中
    • 归并排序:要求稳定性,用到了辅助性数组,所以占用的空间大约为两倍的待排序数组的大小,耗时最短,对栈的依赖适中(比堆排序稍差)
    • 快速排序:不要求稳定性和时间复杂度,没有用到辅助性的数组,所以占用的空间认为是待排序数组的大小,耗时偏短,但对栈的依赖最高,所以递归深度最深
    • 桶排序:严格意义上说是一种策略,且桶排序的执行效率与编码人员的综合素质息息相关,是一种下限极低,上限极高的极度自由的排序算法,通常用于大数据量排序
    • timeSort:是 jdk 官方默认排序算法,能被官方作为默认的排序算法,必然非常优秀;且它结合了归并排序和插入排序的优点
    • 基数排序:用到了辅助性数组,所以占用的空间约为两倍的待排序数组的大小,耗时偏高,但空间复杂度极低,也比较优秀
    • 计数排序:用到了模板性的辅助数组,所以占用的空间可以很小,也可以很大,取决于待排序数组的分布情况,但时间复杂度极低,也比较优秀,如果待排序数组的最大最小值较小,可以考虑
  6. 排序算法口诀:直冒简希,快堆二基
算法种类最好情况平均情况最坏情况空间复杂度是否稳定
直接插入排序 O ( n ) O(n) O(n) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)
冒泡排序 O ( n ) O(n) O(n) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)
简单选择排序 O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)
希尔排序适合大规模数据 O ( n 1.3 ) O(n^{1.3}) O(n1.3) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)
快速排序 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) O ( n 2 ) O(n^2) O(n2) O ( l o g 2 n ) O(log_2n) O(log2n)
堆排序 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)O(1)
2路归并排序 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) O ( n ) O(n) O(n)
基数排序 O ( d ( n + r ) ) O(d(n+r)) O(d(n+r)) O ( d ( n + r ) ) O(d(n+r)) O(d(n+r)) O ( d ( n + r ) ) O(d(n+r)) O(d(n+r)) O ( r ) O(r) O(r)
待补充···
待补充···

在这里插入图片描述

  • 稳定性:若经过排序,记录的相对次序保持不变
  • 当n比较小或关键字基本有序的时候,使用直接插入排序。
  • 当n比较大,使用 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)的排序算法
    • 快速排序,排序关键字随机分布时,性能表现最好
    • 堆排序,辅助空间少,不会出现最坏情况
    • 归并排序,排序稳定,不交换相等数据元素
  • 数据元素本身信息量比较大,可以使用简单选择排序,减少数据元素的移动次数。或者使用链表作为存储结构。
  • n很大且关键字位数少可以分解,可以采用基数排序
  1. 工程上的排序优化
    • 原理:将多种排序进行合并的综合排序
    • 先通过反射,查看排序的是值传递还是引用传递
      • 按值传递:直接快排,如何快排范围小于60直接插入排序(调度不好,但是常数项低,所以可以使用直接插入进行复杂度常数项的优化)
      • 按引用传递:归并排序,进行稳定性的保持

常见排序算法

0.直接插入排序

  1. 算法基本思路:每次迭代时,取无序区的首元素,插入到有序区中适当的位置
  2. 算法优化:可以在进行插入时,对有序区使用二分查找算法
  3. 基本算法
    void insertSort(vector<int> &nums){
        // 第一个元素认为已经排序完,共进行n-1轮
        for (int i = 1; i < nums.size(); ++i){
        	// 将无序区第一个元素作为待排序元素
            int flag = nums[i]; 
            // 在有序区从后向前进行遍历
            int j = i;
            while (j > 0 && nums[j - 1] > flag){
                nums[j] = nums[j - 1];
                --j;
            }
            nums[j] = flag;	// 前面已经--j了
        }
    }
    
    
    在这里插入图片描述

1.冒泡排序

  1. 冒泡排序是一个最基础的交换排序,每次迭代中会有一个数据元素像小气泡一样慢慢上升(下降)到有序区
  2. 基本算法
    void bullbleSort(vector<int> &nums){
    	int n = nums.size();
    	// n个数据元素只需要n-1次排序,因为排完最后一个元素已经有序
    	for(int i = 0; i < n-1; ++i){
    		flag = false;// 发生交换的标志
    		// 每次将无序区间最小的数据元素放到有序区的后面
    		for(int j = n-1; j > i; --j){
    			if(nums[j] < nums[j-1]){
    				swap(nums[j-1], nums[j]);
    				flag = true;
    			}	
    		// 本次遍历没有发生交换,说明已经有序
    		if(flag == false)
    			return ;
    		}
    	}
    }
    
    在这里插入图片描述

2.简单选择排序

  1. 基本思路: 从无序区中找最小值的下标,与有序区的后一个交换
  2. 基本算法
    void selectSort(vector<int> &nums){
        int select = 0; // 每次迭代记录无序区中最小的数的下标
        // 从第一个数开始迭代n-1次,最后一次末尾的两个数值一起排序完成
        for (int i = 0; i < nums.size() - 1; ++i)
        {
            // 每次更新最小值下标为初始值
            select = i;
            // 从无序区中找到最小值的下标
            for (int j = i; j < nums.size() - 1; ++j){
                if (nums[j] < nums[select]){
                    select = j;
                }
            }
            // 交换有序区的后一个和无序区中最小的那个
            swap(nums[i], nums[select]);
        }
    }
    

在这里插入图片描述


3.希尔排序

  1. 希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”,是直接插入排序算法的改进版
  2. 原理
    • 前提:直接插入排序在元素有序的情况下,每次插入一个元素只需要比较一次而不用移动元素,时间复杂度为O(n)
    • 每次先取一个小于n的步长 d 1 d_1 d1,把表中的全部数据元素相距 d 1 d_1 d1的倍数的放在同一组,在各组内进行直接插入排序。然后取第二个步长 d 2 d_2 d2< d 1 d_1 d1,重复上述过程,直到所取到的 d t d_t dt = 1,即所有记录已放在同一组中,再进行直接插入排序
    void shell_sort(int arr[], int len) {
            int gap, i, j;
            int temp;
            for (gap = len >> 1; gap > 0; gap >>= 1)
                    for (i = gap; i < len; i++) {
                            temp = arr[i];
                            for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap)
                                    arr[j + gap] = arr[j];
                            arr[j + gap] = temp;
                    }
    }
    

在这里插入图片描述
在这里插入图片描述


4.快速排序

  1. 基本思想
    • 选择基准:在待排序列中,按照某种方式挑出一个元素,作为 “基准”(pivot)
    • 分割操作:以该基准在序列中的实际位置,把序列分成两个子序列。此时,在基准左边的元素都比该基准小,在基准右边的元素都比基准大
    • 递归地对两个序列进行快速排序,直到序列为空或者只有一个元素。
  2. 特点
    • 不产生有序子序列,但每次排序后会将基准元素放到最终位置上
    • 每次排序划分子区间越相近越能发挥快排优势
  3. 算法
int partition(vector<int> &vec, int left, int right) {
     // 为了防止第一位元素是最小或者最大的那几个,取随机元素,尽量每次将区间对半分
    int idx = left + rand() % (right - left + 1);
    swap(vec[left], vec[idx]);

    int pos = left;
    int pivot = vec[left];
    while (left < right) {
	    // 从右向左找 小于等于 基准元素的
	    while (vec[right] >= pivot && left < right) --right;
	    // 从左向右找 大于等于 基准元素的
	    while (vec[left] <= pivot && left < right) ++left;
        swap(vec[left], vec[right]);
    }
    swap(vec[left], vec[pos]);
    return left;	
}

void QuickSort(vector<int> &vec, int left, int right) {
  if (left > right) return ;
  int pivot = partition(vec, left, right);
  QuickSort(vec, left, pivot-1);
  QuickSort(vec, pivot+1, right);
}

在这里插入图片描述


5.堆排序

  1. 算法的稳定性:关键字相同的两个元素在排序后顺序不变
  2. 定义(数组可以使用完全二叉树表示)
    • 大根堆
      • 完全二叉树中,所有根均大于左右子树的值。
      • n个关键字序列L[1···n], L ( i ) > = L ( 2 i ) 且 L ( i ) > = L ( 2 i + 1 ) L(i) >= L(2i)且L(i)>=L(2i+1) L(i)>=L(2i)L(i)>=L(2i+1)
    • 小根堆
      • 完全二叉树中,所有根均小于左右子树的值。
      • n个关键字序列L[1···n], L ( i ) < = L ( 2 i ) 且 L ( i ) < = L ( 2 i + 1 ) L(i) <= L(2i)且L(i)<=L(2i+1) L(i)<=L(2i)L(i)<=L(2i+1)
  3. 算法
    • 父亲节点为dad,则左孩子为dad * 2 + 1,右孩子为dad * 2 + 1+1
    void max_heapify(int arr[], int start, int end) {
        //建立父节点指标和子节点指标
        int dad = start;
        int son = dad * 2 + 1;// 从0开始计数,所以加一
        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) {
        //初始化,i从最后一个父节点开始调整
        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);
        }
    }
    
    请添加图片描述

6.二路归并排序

  1. 原理:采用分治法的思想

    • 分:每次划分子序列进行直接插入排序
    • 治:将已有序的两个子序列合并,得到完全有序的新序列
  2. 优化思路

    • 递归:消除递归,避免递归过程的时间消耗。
    • 最长无逆序子序列:我们经过分析知道,归并排序的基础是两个有序子序列的合并,那么我们可以通过寻找最长无逆序子序列来优化归并排序的比较次数。例如,(4,5,6,3,7,1)这个序列,我们找到三个无逆序子序列,直接对这三个子序列进行合并即可,减少比较次数。
    • 小排序问题:划分为小序列,做直接插入排序,再采用归并排序。
    • 不回写:这个策略是最重点要讲述的策略。我们上面两段示例代码中都存在从B写回E的操作,这种写回操作在大排序问题时非常浪费时间,我们就思考一种不回写策略来解决这个问题。
  3. 算法

    // 分治法:先划分后合并 
    void mergeSort(ElemType A[],int low,int high){
    	// 当数组被划分成单独的元素,则low>=high时跳出递归
    	if(low < high){	
    		//划分规则 中点 
    		int mid = (low + high)/2; 
    		mergeSort(A,low,mid);
    		mergeSort(A,mid+1,high);
    		//一次划分 一次合并
    		merge(A,low,mid,high); 		 
    	} 
    } 
    
    // 合并函数:将A[low..mid] 和 A[mid+1...high]进行合并
    void merge(ElemType A[],int low,int mid,int high){
    	//B里暂存A的数据 
    	for(int k = low ; k < high + 1 ; k++){
    		B[k] = A[k]; 
    	} 
    	/*这里对即将合并的两个数组 
    	*A[low..mid] 头元素 A[i]和 A[mid+1...high] 头元素  A[j] 
    	*进行一个头部的标记, 分别表示为数组片段的第一个元素 
    	*k 是目前插入位置。 
    	*/ 
    	int i = low , j = mid + 1 , k = low; 	
    	//只有在这种情况下 才不会越界 
    	while(i < mid + 1 && j < high + 1) {
    		//A的元素暂存在B里,因为不能再A上原地操作,会打乱数据
    		//这也是为什么二路归并排序(合并排序)空间复杂度是O(n)的原因 
    		//我们这里把值小的放在前面,最后排序结果就是从小到大 
    		if(B[i] > B[j]){
    			A[k++] = B[j++]; 
    		}else{
    			A[k++] = B[i++]; 
    		} 	 
    	} 
    	//循环结束后,会有一个没有遍历结束的数组段。处理上文的情况2
    	while(i < mid + 1) 
    		A[k++] = B[i++]; 
    	while(j < high + 1) 
    		A[k++] = B[j++]; 
    }
    

    请添加图片描述
    在这里插入图片描述


7.基数排序

  1. 基数排序是一种非比较型整数排序算法。当待排序数据是不太大的自然数(均满足 <=m )时,把待排序数据当作数组下标处理,时间复杂度是 O ( m + n ) O(m+n) O(m+n),并额外有 O ( m ) O(m) O(m)的空间复杂度。
  2. 原理
    • 将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。
    • 从最低位开始,依次进行一次排序。
    • 这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
  3. 代码
    int maxbit(int data[], int n) //辅助函数,求数据的最大位数
    {
        int maxData = data[0];      ///< 最大数
        /// 先求出最大数,再求其位数,这样有原先依次每个数判断其位数,稍微优化点。
        for (int i = 1; i < n; ++i)
        {
            if (maxData < data[i])
                maxData = data[i];
        }
        int d = 1;
        int p = 10;
        while (maxData >= p)
        {
            //p *= 10; // Maybe overflow
            maxData /= 10;
            ++d;
        }
        return d;
    /*    int d = 1; //保存最大的位数
        int p = 10;
        for(int i = 0; i < n; ++i)
        {
            while(data[i] >= p)
            {
                p *= 10;
                ++d;
            }
        }
        return d;*/
    }
    void radixsort(int data[], int n) //基数排序
    {
        int d = maxbit(data, n);
        int *tmp = new int[n];
        int *count = new int[10]; //计数器
        int i, j, k;
        int radix = 1;
        for(i = 1; i <= d; i++) //进行d次排序
        {
            for(j = 0; j < 10; j++)
                count[j] = 0; //每次分配前清空计数器
            for(j = 0; j < n; j++)
            {
                k = (data[j] / radix) % 10; //统计每个桶中的记录数
                count[k]++;
            }
            for(j = 1; j < 10; j++)
                count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
            for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
            {
                k = (data[j] / radix) % 10;
                tmp[count[k] - 1] = data[j];
                count[k]--;
            }
            for(j = 0; j < n; j++) //将临时数组的内容复制到data中
                data[j] = tmp[j];
            radix = radix * 10;
        }
        delete []tmp;
        delete []count;
    
    在这里插入图片描述
    请添加图片描述

待续···


8.桶排序

  1. 原理:把待排序元素分配到不同的桶中,通过对每个桶内的元素进行排序,最终得到有序的结果。
  2. 特点:
    • 时间复杂度较低,通常为O(n)
    • 空间复杂度较高,如果元素的取值范围较大,桶的数量也会相应增多,因此空间成本也会随之增大。

9.timesort排序


10.计数排序


少年,我观你骨骼清奇,颖悟绝伦,必成人中龙凤。
秘籍(点击图中书籍)·有缘·赠予你


🚩点此跳转到首行↩︎

参考博客

  1. 11 种 内部排序算法 大PK(Java 语言实现),究竟谁最快?
  2. 百度百科-排序
  3. 六大排序算法:插入排序、希尔排序、选择排序、冒泡排序、堆排序、快速排序
  4. 菜鸟教程-希尔排序
  5. 堆排序
  6. 二路归并
  7. 排序算法——归并排序(二路归并)
  8. 算法设计与分析(4)——归并排序过程、时间复杂度分析及改进
  9. 计数排序
  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
十种排序算法介绍 转自:matrix67 今天我正式开始挄照我癿目彔写我癿 oi 心得了。我要把我所有学到癿 oi 知识传给以后千千万万癿 oier。以前写过 癿一些东西丌重复写了,但我最后将会重新整理,使乊成为一个完整癿教程。 ???? 挄照我癿目彔,讲仸何东西乊前我都会先介绍旪间复杂度癿相关知识,以后劢丌劢就会扯到这个东西。这个 已经写过了,你可以在这里看到那篇又臭又长癿文章。在讲排序算法癿过程中,我们将始终围绕旪间复杂度癿内容 迚行说明。 ???? 我把这篇文章称乊为“仍零开始学算法”,因为排序算法是最基础算法介绍算法旪仍各种排序算法入手是 最好丌过癿了。 ???? 给出 n 个数,怎样将它们仍小到大排序?下面一口气讲三种常用癿算法,它们是最简单癿、最显然癿、最容 易想到癿。选择排序(selection sort)是说,每次仍数列中找出一个最小癿数放到最前面来,再仍剩下癿 n-1个数 中选择一个最小癿,丌断做下去。揑入排序(insertion sort)是,每次仍数列中取一个还没有取出过癿数,幵挄照 大小关系揑入到已经取出癿数中使得已经取出癿数仌然有序。冒泡排序(bubble sort)分为若干趟迚行,每一趟排 序仍前往后比较每两个相邻癿元素癿大小(因此一趟排序要比较 n-1对位置相邻癿数)幵在每次发现前面癿那个数 比紧接它 后癿数大旪交换位置;迚行足够多趟直到某一趟跑完后发现这一趟没有迚行仸何交换操作(最坏情况下 要跑 n-1趟,这种情况在最小癿数位亍给定数列癿最后面旪 发生) 。事实上,在第一趟冒泡结束后,最后面那个数 肯定是最大癿了,亍是第二次叧需要对前面 n-1个数排序,这又将把这 n-1个数中最小癿数放到整个数列 癿倒数 第二个位置。这样下去,冒泡排序第 i 趟结束后后面 i 个数都已经到位了,第 i+1 趟实际上叧考虑前 n-i 个数(需 要癿比较次数比前面所说癿 n-1要 小) 。这相当亍用数学归纳法证明了冒泡排序癿正确性:实质不选择排序相同。 上面癿三个算法描述可能有点模糊了,没明白癿话网上找资料,代码和劢画演示遍地 都是。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逆羽飘扬

如果有用,请支持一下。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值