【基础算法】(02) 十二种排序算法(第二篇)

2024-06-06 17:58

本文主要是介绍【基础算法】(02) 十二种排序算法(第二篇),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

【基础算法】(02) 十二种排序算法(第二篇)

Auther: Thomas Shen
E-mail: Thomas.shen3904@qq.com
Date: 2017/10/20
All Copyrights reserved !

      • 基础算法02 十二种排序算法第二篇
        • 篇述
        • 直接插入排序 Straight Insertion Sort
        • 二分插入排序 Binary insert sort
        • 希尔排序 Shells Sort
        • 选择排序简单选择排序Simple Selection Sort
        • 选择排序堆排序Heap Sort
        • 交换排序冒泡排序Bubble Sort
        • 鸡尾酒排序双向冒泡排序
        • 交换排序快速排序Quick Sort
        • 归并排序Merge Sort
        • 桶排序 Bucket sort
        • 计数排序 Counting sort
        • 基数排序 Radix Sort
        • 总结
        • References


1. 篇述:

本系列总结了常用的十二种排序算法,每个算法都包括算法原理, 代码实现, 面试例题 三部分。

其中本文是排序算法系列的第二篇,介绍了:

  • 5. 选择排序—简单选择排序(Simple Selection Sort),
  • 6. 选择排序—堆排序(Heap Sort),
  • 7. 交换排序—冒泡排序(Bubble Sort) ,
  • 8. 鸡尾酒排序/双向冒泡排序

2. 直接插入排序 (Straight Insertion Sort):
3. 二分插入排序 (Binary insert sort):
4. 希尔排序 (Shell’s Sort):

参见第一篇;


5. 选择排序—简单选择排序(Simple Selection Sort):

5.1 原理简介:

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

  1. 第一趟,从n 个记录中找出关键码最小的记录与第一个记录交换;
  2. 第二趟,从第二个记录开始的n-1 个记录中再选出关键码最小的记录与第二个记录交换;
  3. 第i 趟,则从第i 个记录开始的n-i+1 个记录中选出关键码最小的记录与第i 个记录交换;

直到整个序列按关键码有序。

简单选择排序

最差时间复杂度:О(n²)
最优时间复杂度:О(n²)
平均时间复杂度:О(n²)
最差空间复杂度:О(n) total, O(1)

5.2 代码实现:

void selection_sort(int *a, int len)  
{  register int i, j, min, t;  for(i = 0; i < len - 1; i ++)  {  min = i;   //查找最小值  for(j = i + 1; j < len; j ++)  if(a[min] > a[j])  min = j;   //交换  if(min != i)  {  t = a[min];  a[min] = a[i];  a[i] = t;  }  }  
}  

5.3 面试例题:

例题1:
在插入和选择排序中,若初始数据基本正序,则选用 插入排序(到尾部);
若初始数据基本反序,则选用 选择排序。

例题2:
下述几种排序方法中,平均查找长度(ASL)最小的是 ( B )
A. 插入排序 B.快速排序 C. 归并排序 D. 选择排序


6. 选择排序—堆排序(Heap Sort):

6.1 原理简介:

堆排序(Heap sort)堆排序是一种树形选择排序,是对直接选择排序的有效改进。

堆排序利用堆数据结构。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

6.1.1 什么是堆?
我们这里提到的堆一般都指的是二叉堆,它满足二个特性:

  1. 父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值;
  2. 每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。

如下为一个最小堆(父结点的键值总是小于任何一个子节点的键值):
最小堆

初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使之成为一个堆,将堆顶元素输出,得到n 个元素中最小(或最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。称这个过程为堆排序。

因此,实现堆排序需解决两个问题:

  1. 如何将n 个待排序的数建成堆;
  2. 输出堆顶元素后,怎样调整剩余n-1 个元素,使其成为一个新堆。

6.1.2 首先讨论第二个问题:
输出堆顶元素后,对剩余n-1元素重新建成堆的调整过程。

调整小顶堆的方法:

  1. 设有m 个元素的堆,输出堆顶元素后,剩下m-1 个元素。将堆底元素送入堆顶((最后一个元素与堆顶进行交换),堆被破坏,其原因仅是根结点不满足堆的性质。
  2. 将根结点与左、右子树中较小元素的进行交换。
  3. 若与左子树交换:如果左子树堆被破坏,即左子树的根结点不满足堆的性质,则重复方法(2).
  4. 若与右子树交换,如果右子树堆被破坏,即右子树的根结点不满足堆的性质。则重复方法 (2).
  5. 继续对不满足堆性质的子树进行上述交换操作,直到叶子结点,堆被建成。

称这个自根结点到叶子结点的调整过程为筛选。如图:
这里写图片描述

6.1.3 再讨论对n 个元素初始建堆的过程:
建堆方法:对初始序列建堆的过程,就是一个反复进行筛选的过程。

  1. n 个结点的完全二叉树,则最后一个结点是第[ n/2 ]个结点的子树。
  2. 筛选从第[ n/2 ]个结点为根的子树开始,该子树成为堆。
  3. 之后向前依次对各结点为根的子树进行筛选,使之成为堆,直到根结点。

如图建堆初始过程:无序序列:(49,38,65,97,76,13,27,49)
这里写图片描述

最差时间复杂度O(n log n)
最优时间复杂度O(n log n)
平均时间复杂度O(n log n)
最差空间复杂度 O(n)

6.2 代码实现:

void print(int a[], int n){  for(int j= 0; j<n; j++){  cout<<a[j] <<"  ";  }  cout<<endl;  
}  
/** * 已知H[s…m]除了H[s] 外均满足堆的定义 * 调整H[s],使其成为大顶堆.即将对第s个结点为根的子树筛选,  * @param H是待调整的堆数组 * @param s是待调整的数组元素的位置 * @param length是数组的长度 */  
void HeapAdjust(int H[],int s, int length)  
{  int tmp  = H[s];  int child = 2*s+1; //左孩子结点的位置。(i+1 为当前调整结点的右孩子结点的位置)  while (child < length) {  if(child+1 <length && H[child]<H[child+1]) { // 如果右孩子大于左孩子(找到比当前待调整结点大的孩子结点)  ++child ;  }  if(H[s]<H[child]) {  // 如果较大的子结点大于父结点  H[s] = H[child]; // 那么把较大的子结点往上移动,替换它的父结点  s = child;       // 重新设置s ,即待调整的下一个结点的位置  child = 2*s+1;  }  else {            // 如果当前待调整结点大于它的左右孩子,则不需要调整,直接退出  break;  }  H[s] = tmp;         // 当前待调整的结点放到比其大的孩子结点位置上  }  print(H,length);  
}  /** * 初始堆进行调整 * 将H[0..length-1]建成堆 * 调整完之后第一个元素是序列的最小的元素 */  
void BuildingHeap(int H[], int length)  
{   //最后一个有孩子的节点的位置 i=  (length -1) / 2  for (int i = (length -1) / 2 ; i >= 0; --i)  HeapAdjust(H,i,length);  
}  
/** * 堆排序算法 */  
void HeapSort(int H[],int length)  
{  //初始堆  BuildingHeap(H, length);  //从最后一个元素开始对序列进行调整  for (int i = length - 1; i > 0; --i)  {  //交换堆顶元素H[0]和堆中最后一个元素  int temp = H[i]; H[i] = H[0]; H[0] = temp;  //每次交换堆顶元素和堆中最后一个元素之后,都要对堆进行调整  HeapAdjust(H,0,i);  }  
}   int main(){  int H[10] = {3,1,5,7,2,4,9,6,10,8};  cout<<"初始值:";  print(H,10);  HeapSort(H,10);  //selectSort(a, 8);  cout<<"结果:";  print(H,10);  
}  

6.3 面试例题:

例题1:
编写算法,从10亿个浮点数当中,选出其中最大的10000个。

典型的Top K问题,用堆是最典型的思路。建10000个数的小顶堆,然后将10亿个数依次读取,大于堆顶,则替换堆顶,做一次堆调整。结束之后,小顶堆中存放的数即为所求。代码如下(为了方便,这里直接使用了STL容器):
#include "stdafx.h"  
#include <vector>  
#include <iostream>  
#include <algorithm>  
#include <functional> // for greater<>  
using namespace std;  
int _tmain(int argc, _TCHAR* argv[])  
{  vector<float> bigs(10000,0);  vector<float>::iterator it;  // Init vector data  for (it = bigs.begin(); it != bigs.end(); it++)  {  *it = (float)rand()/7; // random values;  }  cout << bigs.size() << endl;  make_heap(bigs.begin(),bigs.end(), greater<float>()); // The first one is the smallest one!  float ff;  for (int i = 0; i < 1000000000; i++)  {  ff = (float) rand() / 7;  if (ff > bigs.front()) // replace the first one ?  {  // set the smallest one to the end!  pop_heap(bigs.begin(), bigs.end(), greater<float>());   // remove the last/smallest one  bigs.pop_back();   // add to the last one  bigs.push_back(ff);   // mask heap again, the first one is still the smallest one  push_heap(bigs.begin(),bigs.end(),greater<float>());  }  }  // sort by ascent  sort_heap(bigs.begin(), bigs.end(), greater<float>());   return 0;  
}  

7. 交换排序—冒泡排序(Bubble Sort):

7.1 原理简介:

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。

算法描述:

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

这里写图片描述

最差时间复杂度: O(n^2)
最优时间复杂度: O(n)
平均时间复杂度: O(n^2)
最差空间复杂度: 总共O(n),需要辅助空间O(1)

冒泡排序是与插入排序拥有相等的执行时间,但是两种法在需要的交换次数却很大地不同。在最坏的情况,冒泡排序需要O(n^2)次交换,而插入排序只要最多O(n)交换。冒泡排序的实现(类似下面)通常会对已经排序好的数列拙劣地执行(O(n^2)),而插入排序在这个例子只需要O(n)个运算。因此很多现代的算法教科书避免使用冒泡排序,而用插入排序取代之。冒泡排序如果能在内部循环第一次执行时,使用一个旗标来表示有无需要交换的可能,也有可能把最好的复杂度降低到O(n)。在这个情况,在已经排序好的数列就无交换的需要。若在每次走访数列时,把走访顺序和比较大小反过来,也可以稍微地改进效率。有时候称为往返排序,因为算法会从数列的一端到另一端之间穿梭往返。

7.2 代码实现:

#include <stdio.h>  
void bubbleSort(int arr[], int count)  {  int i = count, j;  int temp;  while(i > 0)  {  for(j = 0; j < i - 1; j++)  {  if(arr[j] > arr[j + 1])  {   temp = arr[j];  arr[j] = arr[j + 1];  arr[j + 1] = temp;  }  }  i--;  }  }  int main()  {  //测试数据  int arr[] = {5, 4, 1, 3, 6};  //冒泡排序  bubbleSort(arr, 5);  //打印排序结果  int i;  for(i = 0; i < 5; i++)  printf("%4d", arr[i]);  
}  

7.3 面试例题:

把一个字符串的大写字母放到字符串的后面,各个字符的相对位置不变,不能申请额外的空间。

事实上,这道题放到冒泡排序这里不知道是不是特别合适,只是有一种解法是类似冒泡的思想,如下解法一

解法一:
每次遇到大写字母就往后冒,最后结果即为所求:

#include <stdio.h>  
#include <string.h>  
//题目以及要求:把一个字符串的大写字母放到字符串的后面,  
//各个字符的相对位置不变,不能申请额外的空间。   
//判断是不是大写字母   
int isUpperAlpha(char c){  if(c >= 'A' && c <= 'Z'){  return 1;  }  return 0;   
}  
//交换两个字母   
void swap(char *a, char *b){  char temp = *a;  *a = *b;  *b = temp;  
}   
char * mySort(char *arr, int len){  if(arr == NULL || len <= 0){  return NULL;  }  int i = 0, j = 0, k = 0;  for(i = 0; i < len; i++){  for(j = len - 1 - i; j >= 0; j--){  if(isUpperAlpha(arr[j])){  for(k = j; k < len - i - 1; k++){  swap(&arr[k], &arr[k + 1]);  }  break;  }   //遍历完了字符数组,但是没发现大写字母,所以没必要再遍历下去  if(j == 0 && !isUpperAlpha(arr[j])){   //结束;  return arr;  }  }  }   //over:   return arr;  
}  
int main(){  char arr[] = "aaaaaaaaaaaaaaaaaaaaaaaAbcAdeBbDc";  printf("%s\n", mySort(arr, strlen(arr)));  return 0;  
}  

解法二:
步骤如下

  1. 两个指针p1和p2,从后往前扫描;
  2. p1遇到一个小写字母时停下, p2遇到大写字母时停下,两者所指向的char交换;
  3. p1, p2同时往前一格。

代码如下:

#include <stdio.h>  
#include <string.h>  
//判断是不是大写字母   
int isUpperAlpha(char c){  if(c >= 'A' && c <= 'Z'){  return 1;  }  return 0;   
}  
//交换两个字母   
void swap(char *a, char *b){  char temp = *a;  *a = *b;  *b = temp;  
}   
char * Reorder(char *arr, int len){  if(arr == NULL || len <= 0){  return NULL;  }  int *p1 = arr;  int *p2 = arr;  While(p1<arr+len && p2<arr+len){  While( isUpperAlpha(*p1) ){  P1++;  }  While( !isUpperAlpha(*p2) ){  P2++;  }  swap(p1, p2);}  //结束  return arr;  
}  
int main(){  char arr[] = "aaaaaaaaaaaaaaaaaaaaaaaAbcAdeBbDc";  printf("%s\n", Reorder(arr, strlen(arr)));  return 0;  
}  

7.4 冒泡排序算法的改进:

对冒泡排序常见的改进方法是加入一标志性变量exchange,用于标志某一趟排序过程中是否有数据交换,如果进行某一趟排序时并没有进行数据交换,则说明数据已经按要求排列好,可立即结束排序,避免不必要的比较过程。本文再提供以下两种改进算法:

  1. 设置一标志性变量pos,用于记录每趟排序中最后一次进行交换的位置。由于pos位置之后的记录均已交换到位,故在进行下一趟排序时只要扫描到pos位置即可。
void Bubble_1 ( int r[], int n) {  int i= n -1;  //初始时,最后位置保持不变  while ( i> 0) {   int pos= 0; //每趟开始时,无记录交换  for (int j= 0; j< i; j++)  if (r[j]> r[j+1]) {  pos= j; //记录交换的位置   int tmp = r[j]; r[j]=r[j+1];r[j+1]=tmp;  }   i= pos; //为下一趟排序作准备  }   
}    
  1. 第二种优化算法参见 [ 8. 鸡尾酒排序/双向冒泡排序 ]。

8. 鸡尾酒排序/双向冒泡排序:

8.1 原理简介:

鸡尾酒排序等于是冒泡排序的轻微变形。不同的地方在于从低到高然后从高到低,而冒泡排序则仅从低到高去比较序列里的每个元素。他可以得到比冒泡排序稍微好一点的效能,原因是冒泡排序只从一个方向进行比对(由低到高),每次循环只移动一个项目。

算法描述和分析:

  1. 依次比较相邻的两个数,将小数放在前面,大数放在后面;
  2. 第一趟可得到:将最大数放到最后一位。
  3. 第二趟可得到:将第二大的数放到倒数第二位。
  4. 如此下去,重复以上过程,直至最终完成排序。

这里写图片描述

鸡尾酒排序最糟或是平均所花费的次数都是O(n^2),但如果序列在一开始已经大部分排序过的话,会接近O(n)。

最差时间复杂度 O(n^2)
最优时间复杂度 O(n)
平均时间复杂度 O(n^2)

8.2 代码实现:

void CocktailSort(int *a,int nsize)  
{  int tail=nsize-1;  for (int i=0;i<tail;)  {  for (int j=tail;j>i;--j) //第一轮,先将最小的数据排到前面  {  if (a[j]<a[j-1])  {  int temp=a[j];  a[j]=a[j-1];  a[j-1]=temp;  }  }  ++i;                    //原来i处数据已排好序,加1  for (j=i;j<tail;++j)    //第二轮,将最大的数据排到后面  {  if (a[j]>a[j+1])  {  int temp=a[j];  a[j]=a[j+1];  a[j+1]=temp;  }      }  tail--; //原tail处数据也已排好序,将其减1  }  
}  

9. 交换排序—快速排序(Quick Sort):
10. 归并排序(Merge Sort):
11. 桶排序 (Bucket sort):
12. 计数排序 (Counting sort):
13. 基数排序 (Radix Sort):

参见第三、四篇;


14. 总结:

这里写图片描述


References. :
  • [ 1 ]. Coursera | Java程序设计 | PKU
  • [ 2 ]. 转载自:八大排序算法
  • [ 3 ]. 转载自:12种排序算法详解

这篇关于【基础算法】(02) 十二种排序算法(第二篇)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1036821

相关文章

0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeek R1模型的操作流程

《0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeekR1模型的操作流程》DeepSeekR1模型凭借其强大的自然语言处理能力,在未来具有广阔的应用前景,有望在多个领域发... 目录0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeek R1模型,3步搞定一个应

Spring排序机制之接口与注解的使用方法

《Spring排序机制之接口与注解的使用方法》本文介绍了Spring中多种排序机制,包括Ordered接口、PriorityOrdered接口、@Order注解和@Priority注解,提供了详细示例... 目录一、Spring 排序的需求场景二、Spring 中的排序机制1、Ordered 接口2、Pri

大数据小内存排序问题如何巧妙解决

《大数据小内存排序问题如何巧妙解决》文章介绍了大数据小内存排序的三种方法:数据库排序、分治法和位图法,数据库排序简单但速度慢,对设备要求高;分治法高效但实现复杂;位图法可读性差,但存储空间受限... 目录三种方法:方法概要数据库排序(http://www.chinasem.cn对数据库设备要求较高)分治法(常

Python中的随机森林算法与实战

《Python中的随机森林算法与实战》本文详细介绍了随机森林算法,包括其原理、实现步骤、分类和回归案例,并讨论了其优点和缺点,通过面向对象编程实现了一个简单的随机森林模型,并应用于鸢尾花分类和波士顿房... 目录1、随机森林算法概述2、随机森林的原理3、实现步骤4、分类案例:使用随机森林预测鸢尾花品种4.1

Python中lambda排序的六种方法

《Python中lambda排序的六种方法》本文主要介绍了Python中使用lambda函数进行排序的六种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们... 目录1.对单个变量进行排序2. 对多个变量进行排序3. 降序排列4. 单独降序1.对单个变量进行排序

MySQL中my.ini文件的基础配置和优化配置方式

《MySQL中my.ini文件的基础配置和优化配置方式》文章讨论了数据库异步同步的优化思路,包括三个主要方面:幂等性、时序和延迟,作者还分享了MySQL配置文件的优化经验,并鼓励读者提供支持... 目录mysql my.ini文件的配置和优化配置优化思路MySQL配置文件优化总结MySQL my.ini文件

关于Java内存访问重排序的研究

《关于Java内存访问重排序的研究》文章主要介绍了重排序现象及其在多线程编程中的影响,包括内存可见性问题和Java内存模型中对重排序的规则... 目录什么是重排序重排序图解重排序实验as-if-serial语义内存访问重排序与内存可见性内存访问重排序与Java内存模型重排序示意表内存屏障内存屏障示意表Int

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

康拓展开(hash算法中会用到)

康拓展开是一个全排列到一个自然数的双射(也就是某个全排列与某个自然数一一对应) 公式: X=a[n]*(n-1)!+a[n-1]*(n-2)!+...+a[i]*(i-1)!+...+a[1]*0! 其中,a[i]为整数,并且0<=a[i]<i,1<=i<=n。(a[i]在不同应用中的含义不同); 典型应用: 计算当前排列在所有由小到大全排列中的顺序,也就是说求当前排列是第

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个