各种常见算法比较

在这里插入图片描述

1.插入排序(Insertion Sort)

在这里插入图片描述
插入排序其实就是借助这样的思想,首先我们将数组中的数据分为两个区间,一个是已排序区间,另一个是未排序区间,同时这两个区间都是动态的。开始时,假设最左侧的元素已被排序,即为已排序区间,每一次将未排序区间的首个数据放入排序好的区间中,直达未排序空间为空。
在这里插入图片描述


#include<iostream>
#include<vector>

using namespace std;

void InsertionSort(vector<int>&, int);

int main() {
  vector<int> test = { 3, 7, 6, 4, 5, 1, 2, 8 };
  InsertionSort(test, test.size());

  for (auto x : test)
    cout << x << " ";

  return 0;
}

void InsertionSort(vector<int>& arr, int len) {
  for (int i = 1; i < len; ++i) {   //注意i从1开始
    int key = arr[i];    //需要插入的元素  
    int j = i - 1;   //已排序区间 
    while ((j >= 0) && (arr[j] > key)) {
      arr[j + 1] = arr[j];    //元素向后移动
      j--;
    }
    arr[j + 1] = key;
  }
}

算法分析
最好情况: 即该数据已经有序,我们不需要移动任何元素。于是我们需要从头到尾遍历整个数组中的元素O(n).

最坏情况: 即数组中的元素刚好是倒序的,每次插入时都需要和已排序区间中所有元素进行比较,并移动元素。因此最坏情况下的时间复杂度是O(n^2).

平均时间复杂度:类似我们在一个数组中插入一个元素那样,该算法的平均时间复杂度为O(n^2).
稳定性:其实,我们在插入的过程中,如果遇到相同的元素,我们可以选择将其插入到之前元素的前面也可以选择插入到后面。所以,插入排序可以是稳定的也可能是不稳定的。

2.选择排序(Selection Sort)

选择排序和插入排序类似,也将数组分为已排序和未排序两个区间。但是在选择排序的实现过程中,不会发生元素的移动,而是直接进行元素的交换。
选择排序的实现过程: 在不断未排序的区间中找到最小的元素,将其放入已排序区间的尾部。
在这里插入图片描述

#include<iostream>
#include<vector>

using namespace std;

void SelectionSort(vector<int>&);

int main() {
  vector<int> test = { 3, 7, 6, 4, 5, 1, 2, 8 };
  SelectionSort(test);

  for (auto x : test)
    cout << x << " ";

  return 0;
}

void SelectionSort(vector<int>& arr) {
  for (int i = 0; i < arr.size()-1; i++) {
    int min = i;
    for (int j = i + 1; j < arr.size(); j++)
      if (arr[j] < arr[min]) min = j;

    swap(arr[i], arr[min]);
  }
}

最好情况,最坏情况:都需要遍历未排序区间,找到最小元素。所以都为O(n2).因此,平均复杂度也为O(n2).
稳定性:答案是否定的,因为每次都要在未排序区间找到最小的值和前面的元素进行交换,这样如果遇到相同的元素,会使他们的顺序发生交换。
在这里插入图片描述

3.冒泡排序(Bubble Sort)

冒泡排序和插入排序和选择排序不太一样。冒泡排序每次只对相邻两个元素进行操作。每次冒泡操作,都会比较相邻两个元素的大小,若不满足排序要求,就将它俩交换。每一次冒泡,会将一个元素移动到它相应的位置,该元素就是未排序元素中最大的元素。
在这里插入图片描述


#include<iostream>
#include<vector>

using namespace std;

void BubbleSort(vector<int>&);

int main() {
  vector<int> test = { 3, 7, 6, 4, 5, 1, 2, 8 };
  BubbleSort(test);

  for (auto x : test)
    cout << x << " ";

  return 0;
}

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

冒泡排序的递归实现
如果我们仔细观察冒泡排序算法,我们会注意到在第一次冒泡中,我们已经将最大的元素移到末尾。在第二次冒泡中,我们将第二大元素移至倒数第二个位置,然后以此类推,所以很容易想到利用递归来实现冒泡排序。

递归思路:

如果数组大小为1,则直接返回。
每进行一次冒泡排序,可修复当前数组的最后一个元素。
每完成一次冒泡操作,对剩下未排序所有元素进行递归冒泡。
冒泡排序递归代码实现:


#include<iostream>
#include<vector>

using namespace std;

void Recursive_BubbleSort(vector<int>&, int);

int main() {
  vector<int> test = { 3, 7, 6, 4, 5, 1, 2, 8 };
  Recursive_BubbleSort(test,test.size());

  for (auto x : test)
    cout << x << " ";

  return 0;
}

void Recursive_BubbleSort(vector<int>& arr, int n) {
  if (n == 1) return;

  for (int i = 0; i < arr.size() - 1; i++) {
    if (arr[i] > arr[i + 1])
      swap(arr[i], arr[i + 1]);
  }

  Recursive_BubbleSort(arr, n - 1);
}

算法分析:

最好情况:我们只需要进行一次冒泡操作,没有任何元素发生交换,此时就可以结束程序,所以最好情况时间复杂度是O(n).

最坏情况: 要排序的数据完全倒序排列的,我们需要进行n次冒泡操作,每次冒泡时间复杂度为O(n),所以最坏情况时间复杂度为O(n^2)。

平均复杂度:O(n^2)
稳定性:在冒泡排序的过程中,只有每一次冒泡操作才会交换两个元素的顺序。所以我们为了冒泡排序的稳定性,在元素相等的情况下,我们不予交换,此时冒泡排序即为稳定的排序算法。

4.归并排序(Merge Sort)-分治的思想

运用递归法实现归并操作的主要步骤:

  • 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列。
  • 设定两个指针,最初位置分别为两个已经排序序列的起始位置。
  • 比较两个指针所指向的元素,选择较小的元素放入到合并空间,并将指针移动到下一位置。
  • 重复步骤3直到某一指针到达序列尾,然后将另一序列剩下的所有元素直接复制到合并序列尾
    在这里插入图片描述
#include <stdio.h>
#include <stdlib.h>
long long int a[100005], b[100005];
long long int sum = 0;
void sort(long long int l1, long long int r1, long long int l2, long long int r2)
{
    long long int top = 0;
    long long int i = l1, j = l2;
    while(i <= r1 && j <= r2)
    {
        if(a[i] > a[j])
        {
            sum+=(r1-i+1);//求逆序对
            b[top++] = a[j++];
        }
        else
        {
            b[top++] = a[i++];
        }

    }
    while(i <= r1)b[top++] = a[i++];
    while(j <= r2)b[top++] = a[j++];
    for(i = l1, j = 0; j < top; j++, i++)
    {
        a[i] = b[j];
    }
    return;
}
void de(long long int l, long long int r)
{
    if(l >= r)return;
    long long int m = (l + r) / 2;
    de(l, m);
    de(m + 1, r);
    sort(l, m, m + 1, r);
    return;
}
int main()
{
    long long int n, i;
    scanf("%lld", &n);
    for(i = 0; i < n; i++)scanf("%lld", &a[i]);
    de(0, n - 1);
    printf("%lld\n",sum);
    return 0;
}

算法分析:
时间复杂度为O(nlongn)
归并排序是原地排序吗?

从原理中可以看出,在归并排序过程中我们需要分配临时数组temp,所以不是原地排序算法,空间复杂度为O(n).

归并排序是稳定的排序算法吗?

当我们遇到左右数组中的元素相同时,我们可以先把左边的元素放入temp数组中,再放入右边数组的元素,这样就保证了相同元素的前后顺序不发生改变。所以,归并排序是一个稳定的排序算法。

5.快速排序(Quicksort)

快速排序,也就是我们常说的“快排”。其实,快排也是利用的分治思想。它具体的做法是在数组中取一个基准pivot,pivot位置可以随机选择(一般我们选择数组中的最后一个元素)。选择完pivot之后,将小于pivot的所有元素放在pivot左边,将大于pivot的所有元素放在右边。最终,pivot左侧元素都将小于右侧元素。接下来我们递归将左侧的子数组和右侧子数组进行快速排序。如果左右两侧的数组都是有序的话,那么我们的整个数组就处于有序的状态了。
在这里插入图片描述

//快速排序(从小到大)
void quickSort(int left, int right, vector<int>& arr)
{
	if(left >= right)
		return;
	int i, j, base, temp;
	i = left, j = right;
	base = arr[left];  //取最左边的数为基准数
	while (i < j)
	{
		while (arr[j] >= base && i < j)
			j--;
		while (arr[i] <= base && i < j)
			i++;
		if(i < j)
		{
			temp = arr[i];
			arr[i] = arr[j];
			arr[j] = temp;
		}
	}
	//基准数归位
	arr[left] = arr[i];
	arr[i] = base;
	quickSort(left, i - 1, arr);//递归左边
	quickSort(i + 1, right, arr);//递归右边
}

算法分析:
快速排序的时间复杂度?
快排的时间复杂度也可以像归并排序那样用递推公式计算出来。如果每次分区都刚好把数组分成两个大小一样的区间,那么它的时间复杂度也为O(nlogn).但是如果遇到最坏情况下,该算法可能退化成O(n^2).

快速排序是原地排序吗?
根据上述原理可以知道,快速排序也没有额外的内存消耗,故也是一种原地排序算法。

快速排序是稳定的排序算法吗?
因为分区操作涉及元素之间的交换(如下图),当遍历到第一个小于2的元素1时,会交换1与前面的3,因此两个相等3的顺序就发生了改变。所以快速排序不是一个稳定的排序算法。
在这里插入图片描述

6.计数排序(Counting Sort)

7.桶排序(Bucket Sort)

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int n,x,i,a[101];
    scanf("%d",&n);
    for(i=0; i<101; i++)a[i]=0;
    for(i=0; i<n; i++)
    {
        scanf("%d",&x);
        if(x>=100)a[100]++;
        else a[x]++;
    }
    for(i=0; i<101; i++)
    {
        if(a[i]!=0)printf("%d %d\n",i,a[i]);
    }
    return 0;
}

桶排序思路比较简单,如果桶的数量等于数组元素的数量,那么桶排序就变成了计数排序。所以在代码中可以看到与计数排序相似的地方。

假设我们需要排序的数组元素有n个,同时用m个桶来存储我们的数据。那么平均每个桶的元素个数为k = n/m个.如果在桶内我们使用快速排序,那么时间复杂度为klogk,总的时间复杂度即为nlog(n/m).如果桶的数量接近元素的数量,桶排序的时间复杂度就是O(n) 了。但是如果运气不好,所有的元素都到了一个桶了,那么它的时间复杂度就退化成 O(nlogn) 了。

8.基数排序(Radix sort)

基数排序其实也是一个非比较型的整数排序算法,其原理是将整数按位切割成不同的数字,然后按每个位数分别比较。但是在计算机中字符串和浮点数也可以用整数表示,所以也可以用基数排序。

由此可见,基数排序是基于位数的比较,所以再处理一些位数较多的数字时基数排序就有明显的优势了。例如在给手机号排序,或者给一些较长的英语专业名词排序等。

基数排序的主要步骤:

找出数组中的最大值并求其位数bit
初始化一个数组count[],长度与当前数组所包含数字的进制相同。例如对于整数排序,数组长度应该是10。
运用计数排序的思想对数组进行排序,循环bit次
基数排序算法图解如下:
在这里插入图片描述

#include<iostream>
#include<vector>

using namespace std;

void radixsort(vector<int>&);
int maxbit(vector<int>);

int main() {
    vector<int> test = { 77, 15, 31, 50, 8, 100, 24, 3, 43, 65 };
    radixsort(test);
    for (auto x : test)
        cout << x << " ";
    return 0;
}

int maxbit(vector<int> arr) //求数据的最大位数
{
    int max = arr[0];
    for (auto x : arr)
        if (x > max)
            max = x;
            
    int bit = 1;

    while (max >= 10)
    {
        max /= 10;
        ++bit;
    }
    return bit;
}

void radixsort(vector<int>& arr) //基数排序
{
    int bit = maxbit(arr);
    vector<int> tmp(arr.size());
    vector<int> count(10); //0-9计数器
    int i, j, k;
    int radix = 1;
    for (i = 1; i <= bit; i++) //进行bit次排序
    {
        for (j = 0; j < 10; j++)
            count[j] = 0; //每次分配前清空计数器
        for (j = 0; j < arr.size(); j++)
        {
            k = (arr[j] / radix) % 10; 
            count[k]++;
        }
        for (j = 1; j < 10; j++)
            count[j] = count[j - 1] + count[j]; 

        for (j = arr.size() - 1; j >= 0; j--) 
        {
            k = (arr[j] / radix) % 10;
            tmp[count[k] - 1] = arr[j];
            count[k]--;
        }

        for (j = 0; j < arr.size(); j++) 
            arr[j] = tmp[j];
        radix = radix * 10;
    }
}

根据上面的讲解大家也可以很容易地看出,基数排序的时间复杂度是O(k*n),其中n是排序的元素个数,k是元素中最大元素的位数。因此,基数算法也是线性的时间复杂度,但是由于k取决于数字的位数,所以在某些情况下该算法不一定优于O(nlogn).

9.希尔排序(Shellsort)

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

所以在具体讲解希尔排序之前,我们还是先来回顾一下插入排序的整个实现过程:

首先我们将数组中的数据分为两个区间,一个是已排序区间,另一个是未排序区间,同时这两个区间都是动态的,需要添加和移动元素。开始时,假设最左侧的一个元素已被排序,即为已排序区间,每一次将未排序区间的首个数据插入排序好的区间中,直达未排序空间为空
在这里插入图片描述
那么插入排序有哪些不足的地方呢?

在插入排序中,我们每次只交换两个相邻元素,当一个元素需要向前移动至它的正确位置时,只能一步一步地移动。因此插入排序的平均时间复杂度为O(n^2).

而希尔排序的想法是实现具有一定间隔元素之间的交换,即首先排序有一定间隔的元素,同时按顺序依次减小间隔,这样就可以让一个元素一次性地朝最终位置前进一大步,当间隔为1时就是插入排序了。

希尔排序算法步骤:

计算步长间隔值gap
将数组划分为这些子数组
按照插入排序思想进行排序
重复此过程,直到间隔为1,进行普通的插入排序。
希尔算法图解如下:
在这里插入图片描述

#include<iostream>
#include<vector>
#include<algorithm>
#include<queue>
using namespace std;

void shell_sort(vector<int>&);

int main() {
    vector<int> test = { 40, 8, 2, 15, 37, 42, 11, 29, 24, 7 };
    shell_sort(test);
    for (auto x : test)
        cout << x << " ";
    return 0;
}

void shell_sort(vector<int>& arr) {

    for (int gap = arr.size() / 2; gap > 0; gap /= 2) {
        for (int i = gap; i < arr.size(); i++) {
            int temp = arr[i];
            int j;
            for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
                arr[j] = arr[j - gap];
            }

            arr[j] = temp;
        }
    }
}

在希尔算法中,步长的选择尤其重要。在上面的代码中,最初的步长选择为n/2,并且不断对步长取半,直到最后为1.虽然这样可以比普通的插入排序O(n^2)更好,但是根据步长的选择不同,希尔排序的平均时间复杂度还可以更优。下图为不同步长序列选择下的最坏时间复杂度。
因为希尔排序在实现过程中没有分配额外的内存空间,所以是一种原地排序算法。但是由于希尔排序将数组分组并存在元素的交换,所以并不是一个稳定的排序算法。
在这里插入图片描述

10.堆排序(Heap sort)

堆一般具有下面两个性质:
堆总是一棵完全二叉树。(完全二叉树是指即除了最底层,其他层的节点都必须被元素填满,同时最底层的叶子节点必须全部。堆中的任意节点的值都必须大于等于或小于等于它的子节点。其中,将每个节点的值都大于等于其子节点值的堆称为“大顶堆”(max heap),将每个节点的值小于等于子节点值的堆称为“小顶堆”(min heap).
在这里插入图片描述
因为我们常用数组来存储完全二叉树,所以我们也可以用数组来存储堆。

堆在数组中的存储图如下:
在这里插入图片描述

  • List item2*i+1为它的左子节点
  • 2*i+2为它的右子节点
  • i/2是它的前继节点
    在这里插入图片描述
#include <stdio.h>
#include <stdlib.h>
int a[15];
void down(int a[],int i,int n)
{
int l=2*i;
int r=2*i+1;
int min=i;
if(l<=n)
{
if(l<=n&&a[l]<=a[min])min=l;
if(r<=n&&a[r]<=a[min])min=r;
if(i!=min)
{
int t=a[i];
a[i]=a[min];
a[min]=t;
down(a,min,n);
}
}
return ;
}
void creat(int a[],int n)
{
int i;
for(i=n/2;i>=1;i--)down(a,i,n);
return ;
}
void sort(int a[],int n)
{
if(n<1)return ;
creat(a,n);
int t=a[1];
a[1]=a[n];
a[n]=t;
sort(a,n-1);
return ;
}
int main()
{
    int n,m;
    while(scanf("%d%d",&n,&m)!=EOF)
    {
    int i,k;
    for(i=1;i<=m;i++)scanf("%d",&a[i]);
    creat(a,m);
    for(i=m+1;i<=n;i++)
    {
    scanf("%d",&k);
    if(a[1]<k)a[1]=k;
    creat(a,m);
    }
    sort(a,m);
    for(i=1;i<=m;i++)
    {
    if(i==1)printf("%d",a[i]);
    else printf(" %d",a[i]);
    }
    printf("\n");
    }
    return 0;
}

最好最坏以及平均情况下的时间复杂度均为O(nlogn).

参考:https://segmentfault.com/a/1190000022049588?utm_source=tag-newest

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值