必知之C++算法之七大排序算法相关万字究极集合

排序算法简介

排序的概念

概念

排序是计算机内经常进行的一种操作,其目的是将一组无序的数据元素调整为有序的数据元素的过程。

操作

比较:任意两个数据元素通过比较操作确定先后次序。

//比较数组中两个数据
if (arr[i] > arr[j])
	//arr[i]大于arr[j]
else
	//arr[i]小于arr[j]

交换:数据元素之间需要交换才能得到预期结果。

//数据交换函数
void swap(int *a, int *b)
{
        int tmp = *a;
        *a = *b;
        *b = tmp;
}

数据量分析

内部排序:

若整个排序过程不需要访问外存,仅在内存中完成数据的调整,则称此类排序问题为内部排序。

外部排序:

若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。

稳定性分析

前提:一组数据中出现多个相同的数据

//一组数据中出现多个相同的数据
int arr[]={9,1,5,6,4,10,5,8,7,3};

若在原始记录序列中, ai 和 aj 的关键字相同, ai 出现在 aj 之前,经过某种方法排序后,ai的位置仍在 aj之前,则称这种排序方法是稳定的;

反之,若经过该方法排序后, ai的位置在 aj 之后,即相同关键字记录的领先关系发生变化,则称这种排序方法是不稳定的。

算法复杂度

算法复杂度

指算法在编写成可执行程序后,运行时所需要的资源,资源包括时间资源和内存资源。应用于数学和计算机导论。

同一问题可用不同算法解决,而一个算法的质量优劣将影响到算法乃至程序的效率。算法分析的目的在于选择合适算法和改进算法。

一个算法的评价主要从[时间复杂度]和[空间复杂度]来考虑。

时间复杂度

一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。

n称为问题的规模,当n不断变化时,时间频度T(n)也会不断变化。

算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数,记作T(n)=O(f(n)),它称为算法的渐进时间复杂度,简称时间复杂度

大O表示法

用O( )来体现算法时间复杂度的记法,称为大O表示法。

算法复杂度可以从最理想情况、平均情况和最坏情况三个角度来评估。

由于平均情况大多和最坏情况持平,而且评估最坏情况也可以避免后顾之忧,因此一般情况下,设计算法时都要直接估算最坏情况的复杂度。

复杂度的阶

常数阶

void test()
{
        //算法的时间复杂度是O(1),称为常数阶。
        printf("hello");//执行1次
}
线性阶
void test(int n)
{
    for(int i = 0;i < n; i++)
    {
		//时间复杂度为O(n)的算法,称为线性阶
	 	printf("hello");//执行n次
	}
}

对数阶

void test(int n)
{
   	int num = 1;
	while(num < n)
    	{
        	//假设循环的次数为X,则由2^x=n得出x=log₂n,因此得出算法的时间复杂度为O(logn)
		num = num * 2;
		//时间复杂度为O(logn)的算法,称为对数阶
		printf("hello");//执行logn次
	}
}

平方阶

void test(int n)
{
       //嵌套循环,外层控制行,内层控制列,执行次数为 外层*内层(n*n)次
      for(int i = 0; i < n; i++)
      {   
          for(int j = 0; j < n; i++)
          {
              //时间复杂度为O(n^2)的算法,称为平方阶
             printf("hello");//执行n^2次
       	}
   	}
}
算法效率的度量

只关注最高次项

时间复杂度是指最坏时间复杂度

只有常数项记做1

常见的时间复杂度

O(1) < O(logn) < O(n) < O(nlogn) <O(n²) < O(n³) < O(2ⁿ) < O(n!) < O(nⁿ)

执行次数函数非正式术语
12O(1)常数阶
2n+3O(n)线性阶
3n^2+2n+1O(n²)平方阶
5log2n+20O(logn)对数阶
2n+3nlog2^n+19O(nlogn)nlogn阶
6n3+2n2+3n+1O(n³)立方阶
2^nO(2ⁿ)指数阶

空间复杂度

空间复杂度是度量算法所需存储空间的大小。

算法的空间复杂度并不是计算实际占用的空间,而是计算整个算法的辅助空间单元的个数。记做S(n)=O(f(n))。

空间复杂度比较常用的有:O(1)、O(n)、O(n²)。

空间复杂度 O(1)

void test()
{
    //创建一个数据大小的空间
    //所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S(n) = O(1)
    int a = 1;
}

空间复杂度 O(n)

void test(int n)
{
    //开辟n个数据的堆空间,因此它的空间复杂度 S(n) = O(n)
    int* p = (int*)malloc(sizeof(int) * n);
}

空间复杂度 O(n²)

void test(int n)
{
    //开辟二级指针对应的堆空间
    int** p = (int**)malloc(sizeof(int*) * n);
    //开辟一级指针对应的堆空间
    for(int i = 0;i<n;i++)
    {
        p[i] = (int*)malloc(sizeof(int) * n);
    }
    //开辟堆空间的数据为n*n,因此它的空间复杂度 S(n) = O(n²)
}

推荐学习书籍

书名作者
算法导论Thomas H. Cormen Charles E. Leiserson
数据结构与算法分析Mark Allen Weiss
数据结构严蔚敏 吴伟民
算法Robert Sedgewick

冒泡排序

原理

**冒泡排序(Bubble Sort)**排列的序列,较大(或较小)的数据会“浮”到序列的顶端(或底部)。

冒泡排序原则:

比较两个相邻的数组元素,使起满足条件交换元素位置,直到n-1轮循环操作结束。

实现

  1. 从头部开始,比较相邻的两个元素arr[j]和arr[j+1],如果前一个元素比后一个元素大,进行数据交换。
  2. 下标向后移动,即使j=j+1,再次比较元素arr[j]和arr[j+1],判断是否需要交换数据。
  3. 针对序列中每一对两两相邻的数据重复以上步骤,直到下标指向最后一个位置。
  4. 在每一轮循环中重复以上步骤(1)(2)(3),直到len-1轮循环执行完毕。

代码

//冒泡排序
void BubbleSort(int* arr, int len)
{
	//外层控制行 表示执行次数
	for (int i = 0; i < 10 - 1; i++)
	{
		//内层控制列 表示比较次数
		for (int j = 0; j < 10 - 1 - i; j++)
		{
			//比较数据
			if (arr[j] > arr[j + 1])
			{
				//交换数据
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
			}
		}
	}
}

测试代码

int main()
{
	//定义数组
	int arr[10] = { 9,1,5,6,10,8,3,7,2,4 };
	//数组元素个数
	int len = sizeof(arr) / sizeof(arr[0]);
    //冒泡排序
    BubbleSort(arr,len);

	//打印数据
	for (int i = 0; i < 10; i++)
		printf("%d\n", arr[i]);

	return 0;
}

优化

假定外层循环执行n次(n<元素个数),数组中数据元素已经变成有序数据,则循环可以退出。

可使用flag标志位记录是否可以终止循环。

    //定义数组
    int arr[10] = { 9,1,5,6,10,8,3,7,2,4 };
    //数组元素个数
    int len = sizeof(arr)/sizeof(arr[0]);
	//设置标志位
	int flag = 1;
    //外层控制行 表示执行次数 将flag标志位作为循环判断条件
    for (int i = 0; i < len - 1 && flag; i++)
    {
        //如果没有进入if判断 表示数据有序,可以终止循环
        flag = 0;
        //内层控制列 表示比较次数
        for (int j = 0; j < len - 1 - i; j++)
        {
            //比较数据
            if (arr[j] > arr[j + 1])
            {
                //表示数据可以继续进行交换
                flag = 1;
                //交换数据
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }

复杂度分析

时间复杂度:

平均时间复杂度:O(n²)

最好时间复杂度:O(n) 数组中数据有序,遍历一次,不需要交换。

最坏时间复杂度:O(n²)

空间复杂度:

O(1),只需要一个额外空间用于交换。

稳定性:

稳定排序

选择排序

原理

**选择排序(Selection Sort)**是从待排序的序列中选出最大值(或最小值),交换该元素与待排序序列头部元素,直到所有待排序的数据元素排序完毕为止。

实现

  1. 第一趟从len个元素的数据序列中选出关键字最小(或最大)的元素并放到最前(或最后)位置。
  2. 下一趟再从len-1个元素中选出最小(大)的元素并放到次前(后)位置。
  3. 以此类推,经过len-1趟完成排序。

代码

	//定义数组
	int arr[10] = { 9,1,5,6,10,8,3,7,2,4 };
	//数组元素个数
	int len = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i < len; i++)
	{
		int max = 0;//最大值下标
		for (int j = 1; j < len - i; j++)
		{
			if (arr[max] < arr[j])
			{
				max = j;//记录最大值下标
			}
		}
		//满足条件 交换数据
		if (max != len - i - 1)
		{
			int temp = arr[max];
			arr[max] = arr[len - i - 1];
			arr[len - i - 1] = temp;
		}
	}

复杂度分析

时间复杂度:

平均时间复杂度:O(n²)

最好时间复杂度:O(n²)

最坏时间复杂度:O(n²)

选择排序最大的特点就是交换移动数据次数比较少,尽管与冒泡排序同为O(n²),但性能上略优于冒泡排序。

空间复杂度:

O(1),只需要一个额外空间用于交换。

稳定性:

不稳定排序

插入排序

原理

直接插入排序(Straight Insertion Sort) 基本操作是:将一个记录插入到已经排好序的有序数据中,从而得到一个新的、记录数增加1的有序表。

实现

把待排序序列视为两部分:

  1. 一部分为有序序列,通常在排序开始之时将序列中的第一个数据视为一个有序序列;
  2. 另一部分为待排序序列,有序序列之后的数据视为待排序序列。
  3. 在排序开始之时,从序列头部到尾部逐个选取数据,与有序序列中的数据,按照从尾部到头部的顺序逐个比较,直到找到合适的位置,将数据插入其中。

代码

	//定义数组
	int arr[10] = { 9,1,5,6,10,8,3,7,2,4 };
	//数组元素个数
	int len = sizeof(arr) / sizeof(arr[0]);
	
	for (int i = 1; i < len; i++)
	{
		//将无序数据插入到有序数据中
		int temp = arr[i];//设置哨兵
		if (temp < arr[i - 1])
		{
			//依次移动数据到指定位置
			for (int j = i - 1; j >= 0 && temp < arr[j]; j--)
			{
				//移动数据
				arr[j + 1] = arr[j];
				arr[j] = temp;
			}
		}
	}

复杂度分析

时间复杂度:

平均时间复杂度:O(n²)

最好时间复杂度:O(n)

最坏时间复杂度:O(n²)

如果排序的数据是随机的,根据概率相同原则,平均比较和移动的次数应为n²/4次,得出直接插入排序的时间复杂度为O(n²)。在同样的时间复杂度中直接插入排序要优于选择排序和冒泡排序。

空间复杂度:

O(1),只需要一个额外空间用于交换。

稳定性:

稳定排序

希尔排序

原理

**希尔排序(Shell Sort)**的基本思想是:先取定一个小于序列元素个数的整数作为增量,把序列的全部元素分成增量个组,所有相互之间距离为增量整数倍的元素放在同一个组中,在各组内进行直接插入排序。

实现

  1. 将一个数据序列按照增量进行分组。
  2. 将各个分组的数据进行直接插入排序。
  3. 更新增量,同时增量大于零在进行分组并排序。

代码

	//定义数组
	int arr[10] = { 9,1,5,6,10,8,3,7,2,4 };
	//数组元素个数
	int len = sizeof(arr) / sizeof(arr[0]);
	int inc = len / 2;//increment 增量
	//增量大于0 为循环条件
	while (inc > 0)
	{
		//对所有相隔增量(inc)的数组元素进行直接插入排序
		for (int i = inc; i < len; i++)
		{
			//定义临时变量存储需要插入的数据
			int temp = arr[i];
			int j = i - inc;
			//对分组的数据进行插入排序
			while (j >= 0 && temp < arr[j]) {
				arr[j + inc] = arr[j];
				j = j - inc;
			}
			arr[j + inc] = temp;
		}
		//更新增量
		inc = inc / 2;
	}

复杂度分析

时间复杂度:

平均时间复杂度:O(n log n)

最好时间复杂度:O(n log² n)

最坏时间复杂度:O(n log² n)

空间复杂度:

O(1),只需要一个额外空间用于交换。

稳定性:

不稳定排序

堆排序

原理

**堆排序(Heaps Sort)**是指利用堆这种数据结构所设计的一种排序算法。

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

堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

  • 大顶堆(大根堆):每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
  • 小顶堆(小根堆):每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;

实现

  1. 创建一个堆,将数据放在堆中存储。
  2. 按大顶堆构建堆,其中大顶堆的一个特性是数据将被从大到小取出,将取出的数据元素按照相反的顺序进行排列,数据元素就完成了排序。
  3. 然后从左到右,从上到下进行调整,构造出大顶堆。
  4. 入堆完成之后,将堆顶元素取出,将末尾元素置于堆顶,重新调整结构,使其满足堆定义。

代码

构建对数据结构

//构建堆数据结构
void HeapAdjust(int arr[], int i, int n)
{
	for (int child; 2 * i + 1 < n; i = child)
	{
		//子结点的位置=2*(父结点位置)+1
		child = 2 * i + 1;
		//得到子结点中较大的结点
		if (child<n - 1 && arr[child + 1]>arr[child])
			child++;
		//如果较大的子结点大于父结点,那么把较大的子结点往上移动,替换它的父结点
		if (arr[i] < arr[child])
		{
			int temp = arr[i];
			arr[i] = arr[child];
			arr[child] = temp;
		}
		else break;		//否则退出循环
	}
}

使用堆进行排序

void HeapSort(int* arr, int len)
{
	//对序列中的每个非叶子结点执行调整算法,使该序列成为一个堆
	for (int i = (len - 1) / 2; i >= 0; i--)
		HeapAdjust(arr, i, len);

	//从最后一个元素开始对序列进行调整,不断缩小调整的范围直到第一个元素
	for (int i = len - 1; i > 0; i--)
	{
		//把第一个元素和当前的最后一个元素交换
		//保证当前最后一个位置存放的是现在这个序列中最大的元素
		int temp = arr[0];
		arr[0] = arr[i];
		arr[i] = temp;
		//不断缩小调整heap的范围,每一次调整完毕保证第一个元素是当前序列的最大值
		HeapAdjust(arr, 0, i);
	}
}

测试代码

int main()
{
	//定义数组
	int arr[10] = { 9,1,5,6,10,8,3,7,2,4 };
	//数组元素个数
	int len = sizeof(arr) / sizeof(arr[0]);

	//堆排序
	HeapSort(arr, len);
    
	//打印数据
	for (int i = 0; i < 10; i++)
		printf("%d\n", arr[i]);
    
	return 0;
}

堆排的另一种实现方式

在这里插入图片描述

复杂度分析

时间复杂度:

平均时间复杂度:O(n log n)

最好时间复杂度:O(n log n)

最坏时间复杂度:O(n log n)

空间复杂度:

O(1),只需要一个额外空间用于交换。

稳定性:

不稳定排序

递归排序

原理

**归并排序(Merge Sort)**的基本思想是:将两个序列合并在一起,并且使之有序。

该算法是采用**分治法(Divide-and-Conquer)**的经典的应用。

归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并

实现

  1. 把长度为n的输入序列分成两个长度为n/2的子序列;
  2. 对这两个子序列分别采用归并排序;
  3. 将两个排序好的子序列合并成一个最终的排序序列。
  4. 在2-路归并排序算法中,由于需要进行递归调用,为了保证递归的顺利执行,按照一定的方法划分序列,直到子序列成为单个的元素,才开始对相邻的序列进行排序与归并。

代码

归并两个序列的算法

//归并两个序列的算法
void Merge(int* arr, int* temp, int start, int mid, int end)
{
	int i = start, j = mid + 1, k = start;
	//比较排序并将值赋给中间变量temp
	while (i != mid + 1 && j != end + 1)
	{
		if (arr[i] >= arr[j])
			temp[k++] = arr[j++];
		else
			temp[k++] = arr[i++];
	}
	//若一个序列指针走到最后,另一个指针为走到最后,直接复制
	while (i != mid + 1)
		temp[k++] = arr[i++];
	while (j != end + 1)
		temp[k++] = arr[j++];
	//将中间变量数组中存储的值赋给原始数组
	for (i = start; i <= end; i++)
		arr[i] = temp[i];
}

递归调用归并算法

//递归调用归并算法
void MergeSort(int* arr, int* temp, int start, int end)
{
	int mid;
	if (start < end)
	{
		//取中间值将原序列分为两组
		mid = (start + end) / 2;
		MergeSort(arr, temp, start, mid);
		MergeSort(arr, temp, mid + 1, end);
		Merge(arr, temp, start, mid, end);
	}
}

测试代码

int main()
{
	//定义数组
	int arr[10] = { 9,1,5,6,10,8,3,7,2,4 };
	//数组元素个数
	int len = sizeof(arr) / sizeof(arr[0]);

	int temp[10] = { 0 };
	//归并排序
	MergeSort(arr, temp, 0, len - 1);

	//打印数据
	for (int i = 0; i < 10; i++)
		printf("%d\n", arr[i]);
	return 0;
}

复杂度分析

时间复杂度:

平均时间复杂度:O(n log n)

最好时间复杂度:O(n log n)

最坏时间复杂度:O(n log n)

空间复杂度:

O(n),需要数据元素大小的额外空间用于交换。

稳定性:

稳定排序

快速排序

原理

**快速排序(Quick Sort)**是对冒泡排序的改进。

快速排序的基本思想是:通过一趟排序,将序列中的数据分割为两部分,其中一部分的所有数值都比另一部分的小;然后按照此种方法,对两部分数据分别进行快速排序,直到参与排序的两部分都有序为止。

实现

将序列划分为如上所述的两部分:

  1. 需要在开始的时置一个参考值,通过与参考值的比较来划分数据;
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面;
  3. 递归地把小于基准值元素的子数列和大于基准值元素的子数列排序。

代码

//快速排序
void QuickSort(int arr[], int left, int right)
{
	if (left >= right)	//如果左边索引大于或等于右边索引,说明该组序列整理完毕
	{
		return;
	}
	int i = left;
	int j = right;
	int key = arr[i];	//使用key来保存作为键值的数据,将arr[i]空出来
						//本轮排序开始,当i=j时本轮排序结束,将键值赋给arr[i]
	while (i < j)
	{
		while ((i < j) && (key <= arr[j]))
		{
			//不符合条件,继续向前寻找
			j--;
		}
		arr[i] = arr[j];//从后往前找到一个小于当前键值的数据arr[j],将其赋给arr[i]
						//赋值之后arr[j]相当于一个空的、待赋值的空间
						//从前往后找一个大于当前键值的数据
		while ((i < j) && (key >= arr[i]))
		{
			//不符合条件,继续向后寻找
			i++;
		}
		//找到或i<j不成立(即序列查找完毕时)while循环结束,进行赋值
		arr[j] = arr[i];
	}
	arr[i] = key;
	//递归调用排序函数对键值两边的子序列进行排序操作
	QuickSort(arr, left, i - 1);
	QuickSort(arr, i + 1, right);
}

测试代码

int main()
{
	//定义数组
	int arr[10] = { 9,1,5,6,10,8,3,7,2,4 };
	//数组元素个数
	int len = sizeof(arr) / sizeof(arr[0]);

	//快速排序
	QuickSort(arr, 0, len - 1);

	//打印数据
	for (int i = 0; i < 10; i++)
		printf("%d\n", arr[i]);

	return 0;
}

复杂度分析

时间复杂度:

平均时间复杂度:O(n log n)

最好时间复杂度:O(n log n)

最坏时间复杂度:O(n²)

空间复杂度:

O(log n)

稳定性:

不稳定排序

归并排序

原理

1.将序列中带排序数字分为若干组,每个数字分为一组

2.将若干个组两两合并,保证合并后的组是有序的。

3.重复第二步操作直到只剩下一组,排序完成。

代码

在这里插入图片描述

2020一些基本排序算法总结

1.冒泡排序、选择排序、插入排序都是时间复杂度O(n2)

2.归并排序、快速排序、堆排序、希尔排序(升级版插入排序,步长很关键)时间复杂度为O(N*logN)

3.计数排序(分组进桶出桶排序)、基数排序(个十百位进桶出通排序)、时间复杂度为O(N),不是基于比较的,思想来自桶排序

空间复杂度:
O(1) :冒泡排序、选择排序、插入排序、堆排序(用递归实现时为O(logN))、希尔排序
O(logN)~O(N):快速排序
O(N):归并排序
O(M):计数排序、基数排序
稳定性:
稳定的排序算法:冒泡排序、插入排序、归并排序、计数排序、基数排序、桶排序
不稳定的排序算法:选择排序、快速排序、希尔排序、堆排序  【原始相同元素顺序可能会变化】

工程上使用排序:

1.综合排序

2.数组小就插入排序

3.数组大,插入排序或者其他O(N*logN)的排序

例子:两个有序数组合并成一个(关键:从后往前覆盖)

//两个有序数组合并成一个(关键:从后往前覆盖)
void MergeSort(int* arr, int* arr2,int len1,int len2) {
	int temp = 0;
	for (int i = 0; i < len1; ++i) {
		if (arr[i] == 0) {
			temp = i - 1;
			break;
		}
	}
	for (int i = len1 - 1; i >= 0; --i) {
		if (arr2[len2 - 1] > arr[temp] && len2 >= 1) {
			arr[i] = arr2[len2 - 1];
			len2--;
		}
		else {
			arr[i] = arr[temp];
			temp--;
		}
	}
}

eg:
    int arr[9] = { 2,4,5 };
	int arr2[] = { 1,3,6,9,11,12};
	int len1 = sizeof(arr) / sizeof(int);
	int len2 = sizeof(arr2) / sizeof(int);

例子:行列都有序矩阵找数(右上角开始找起)

//行列都有序矩阵找数(右上角开始找起)
bool FindNumInSequentialMatrix(int arr[][4],int row,int col,int num) {
	int i, j = col - 1;
	for (i =0; i <= row-1; i++) {
		if (j != col - 1) {
			if (arr[i][j] == num)
				return true;
			if (arr[i][j] < num)
				continue;
		}
		for (; j >= 0; j--) {
			if (arr[i][j] == num) {
				return true;
			}
			if (arr[i][j] > num) {
				continue;
			}
			else {
				break;
			}
		}
		if (j == -1) {
			return false;
		}
	}
	return false;
}
eg:
    int arr[][4] = { 0,1,2,5,2,3,4,7,4,4,4,8,5,7,7,9 };
	int row = sizeof(arr) / sizeof(arr[0]);
	int col = sizeof(arr[0]) / sizeof(int);

附带图:

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值