七种简单排序算法详解与实现

排序算法详解及实现

前言

排序算法作为基础算法,在面试中出现频率也算低,但是由于我们平时都使用系统自带的sort()函数,而忽视了各种排序的(最佳,平均,最坏)时空复杂度,稳定性,复杂性等问题,一旦面试官问到你不熟悉的排序算法,你可能写不出来,所以再次,自己总结一篇关于排序算法的文章,相信大家都是有基础的,简单排序算法不写过程。

一、排序算法简介

所谓排序算法,即通过特定的算法因式将一组或多组数据按照既定模式进行重新排序。这种新序列遵循着一定的规则,体现出一定的规律,因此,经处理后的数据便于筛选和计算,大大提高了计算效率。对于排序,我们首先要求其具有一定的稳定性,即当两个相同的元素同时出现于某个序列之中,则经过一定的排序算法之后,两者在排序前后的相对位置不发生变化。换言之,即便是两个完全相同的元素,它们在排序过程中也是各有区别的,不允许混淆不清。

排序算法作为基础算法,在实际中有很多用处,比如成绩高低排名,销售额排高低等,都需要排序算法,而排序算法有很多,如:冒泡,插入,选择,希尔,快排,归并,堆等,在此只介绍这几种简单的排序算法。

二、排序算法实现

1.冒泡排序

冒泡排序算法是把较小的元素往前调或者把较大的元素往后调。这种方法主要是通过对相邻两个元素进行大小的比较,根据比较结果和算法规则对该二元素的位置进行交换,这样逐个依次进行比较和交换,就能达到排序目的。冒泡排序的基本思想是,首先将第1个和第2个记录的关键字比较大小,如果是逆序的,就将这两个记录进行交换,再对第2个和第3个记录的关键字进行比较,依次类推,重复进行上述计算,直至完成第(n一1)个和第n个记录的关键字之间的比较,此后,再按照上述过程进行第2次、第3次排序,直至整个序列有序为止。排序过程中要特别注意的是,当相邻两个元素大小一致时,这一步操作就不需要交换位置,因此也说明冒泡排序是一种严格的稳定排序算法,它不改变序列中相同元素之间的相对位置关系。

1.1 算法描述

  • 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  • 针对所有的元素重复以上的步骤,除了最后一个;
  • 重复步骤1~3,直到排序完成。

1.2 动图演示

在这里插入图片描述

1.3 代码实现

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

template<class T>
void popsort(vector<T> &nums)
{
    for (int i = 0; i < nums.size()-1;++i)
    {
        bool flag = true;
        for (int j = nums.size() - 1; j > i;--j)
        {
            if(nums[j]<nums[j-1])
            {
                swap(nums[j], nums[j - 1]);
                flag = false;      
            }
        }
        if(flag)        //若一趟下来没有发生交换,则数据已排好序
            break;
    }
}

int main()
{
    vector<double> nums = {6,2, 5, 3, 9, 2,8, 6};
    popsort(nums);             
    //输出
    for (int i = 0; i < nums.size();++i)
    {
        if(i!=nums.size()-1)
            cout << nums[i]<<" ";
        else
            cout << nums[i] << endl;
    }
    return 0;
}

2.选择排序

选择排序算法的基本思路是为每一个位置选择当前最小的元素。选择排序的基本思想是,基于直接选择排序和堆排序这两种基本的简单排序方法。首先从第1个位置开始对全部元素进行选择,选出全部元素中最小的给该位置,再对第2个位置进行选择,在剩余元素中选择最小的给该位置即可;以此类推,重复进行“最小元素”的选择,直至完成第(n-1)个位置的元素选择,则第n个位置就只剩唯一的最大元素,此时不需再进行选择。使用这种排序时,要注意其中一个不同于冒泡法的细节。举例说明:序列58539.我们知道第一遍选择第1个元素“5”会和元素“3”交换,那么原序列中的两个相同元素“5”之间的前后相对顺序就发生了改变。因此,我们说选择排序不是稳定的排序算法,它在计算过程中会破坏稳定性。

2.1 算法描述

  • 第i趟排序(i=1,2,3,n-1),找到最小(最大元素)放到i-1位置。
  • n-1趟之后数组有序。

2.2 动图演示

在这里插入图片描述

3.3 代码实现

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

template<class T>
void selectsort(vector<T> &nums)
{
    for (int i = 0; i < nums.size() - 1;++i)
    {
        int key = i;
        int j = i + 1;
        for (; j < nums.size();++j)
        {
            if(nums[key]>nums[j])
                key = j;
        }
        swap(nums[i], nums[key]);
    }
}

3.插入排序

插入排序算法是基于某序列已经有序排列的情况下,通过一次插入一个元素的方式按照原有排序方式增加元素。这种比较是从该有序序列的最末端开始执行,即要插入序列中的元素最先和有序序列中最大的元素比较,若其大于该最大元素,则可直接插入最大元素的后面即可,否则再向前一位比较查找直至找到应该插入的位置为止。插入排序的基本思想是,每次将1个待排序的记录按其关键字大小插入到前面已经排好序的子序列中,寻找最适当的位置,直至全部记录插入完毕。执行过程中,若遇到和插入元素相等的位置,则将要插人的元素放在该相等元素的后面,因此插入该元素后并未改变原序列的前后顺序。我们认为插入排序也是一种稳定的排序方法。插入排序分直接插入排序、折半插入排序和希尔排序3类。

3.1算法描述

  • 从第一个元素开始,该元素可以认为已经被排序;
  • 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  • 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  • 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  • 将新元素插入到该位置后;
  • 重复步骤2~5。

3.2 动图演示

在这里插入图片描述

3.3 代码实现

template<class T>
void insertsort(vector<T>&nums)
{
    for (int i = 1; i < nums.size();++i)
    {
        T key = nums[i];
        int j = i-1;
        while(j>=0&&nums[j]>key)
        {
            nums[j + 1] = nums[j];
            j--;
        }
        nums[j + 1] = key;
    }
}

4.希尔排序

希尔排序属于插入排序的一种,希尔排序又叫缩小增量排序,把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。

4.1 算法描述

  • 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;(实际中取 t i t_i ti= t i − 1 / 2 t_{i-1}/2 ti1/2)
  • 按增量序列个数k,对序列进行k 趟排序;
  • 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

4.2 动图演示

在这里插入图片描述

4.3 代码实现

template<class T>
void shellsort(vector<T>&nums)
{
    int n = nums.size();
    int gap = n / 2;
    while(gap)
    {
        for (int i = gap; i < n;++i)
        {
            int j = i - gap;
            T temp = nums[i];
            for ( ; j >= 0 && nums[j] > temp;j-=gap)
            {
                nums[j + gap] = nums[j];
            }
            nums[j + gap] = temp;
        }
        gap /= 2;
    }
}

5.快速排序

通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

5.1算法描述

  • 首先设定一个分界值,通过该分界值将数组分成左右两部分。
  • 将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。
  • 然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
  • 重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。

5.2 动图演示

在这里插入图片描述

5.3 代码实现

//快排一趟
template<class T>
int partition(vector<T>&nums,int low,int high)
{
    T target = nums[low];
    int i = low;
    while(low<high)
    {
        while(high>low&&nums[high]>=target)
            high--;
        while(low<high&&nums[low]<=target)
            low++;
        if(low<high)
        {
            swap(nums[low], nums[high]);
        }
    }
    nums[i] = nums[low];
    nums[low] = target;
    return low;
}
//快速排序(模板)
template<class T>
void quicksort(vector<T>&nums,int low,int high)
{
    if(low>=high)
        return;
    int index = partition(nums, low, high);
    quicksort(nums, low,index - 1);
    quicksort(nums, index + 1, high);
}

6.堆排序

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

6.1 算法描述

  • 将初始待排序关键字序列(R0,R2….Rn-1)构建成大顶堆,此堆为初始的无序区;
  • 将堆顶元素R[0]与最后一个元素R[n-1]交换,此时得到新的无序区(R0,R2,……Rn-2)和新的有序区(Rn-1),且满足R[1,2…n-1]<=R[n];
  • 由于交换后新的堆顶R[0]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-2)调整为新堆,然后再次将R[0]与无序区最后一个元素交换,得到新的无序区(R0,R2….Rn-3)和新的有序区(Rn-2,Rn-1)。
  • 不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

6.2 动图演示

在这里插入图片描述

6.3 代码实现

//堆排序   (大根堆:升序  小根堆:降序)
template<class T>
void heaphelper(vector<T>&nums,int index,int n)
{
    if(index>=n)
        return;
    int largestindex = index;
    if(2*index<n&&nums[2*index]>nums[largestindex])
        largestindex = 2 * index;
    if(2*index+1<n&&nums[2*index+1]>nums[largestindex])
        largestindex = 2 * index + 1;
    if(largestindex!=index)
    {
        swap(nums[index], nums[largestindex]);
        heaphelper(nums, largestindex,n);
    }
}
template<class T>
void heapsort(vector<T>&nums)
{
    int n=nums.size();
    for (int i = n / 2; i >= 0; --i)
    {
        heaphelper(nums, i,n);
    }
    cout << "Construct  largest head finished!" << endl;
    while(n)
    {
        swap(nums[0], nums[n-1]);
        n--;    //尾部数据已排好序不加入排序
        heaphelper(nums, 0,n);

    }
    cout << "heap sort is finished!" << endl;
}

7.归并排序

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

7.1 算法描述

  • 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列。
  • 设定两个指针,最初位置分别为两个已经排序序列的起始位置。
  • 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置。
  • 重复步骤3直到某一指针超出序列尾。
  • 将另一序列剩下的所有元素直接复制到合并序列尾。

7.2 动画演示

在这里插入图片描述

7.3 代码实现

template<class T>
void mergesort(vector<T>&nums,vector<T>& temp,int start,int end)
{
    if(start>=end)
        return;
    int mid = start + ((end - start) >> 1);
    int start1 = start;
    int end1 = mid;
    int start2 = mid + 1;
    int end2 = end;
    mergesort(nums,temp, start1, end1);     //递归左边
    mergesort(nums,temp, start2, end2);    //递归右边
    int k = start;

    while(start1<=end1&&start2<=end2)        //排序
    {
       temp[k++]=nums[start1] < nums[start2] ? nums[start1++] : nums[start2++];
    }
    while(start1<=end1)
    {
        temp[k++] = nums[start1++];
    }
    while(start2<=end2)
    {
        temp[k++] = nums[start2++];
    }
    for (int i = start; i <= end;++i)     //赋值给原数组
    {
        nums[i] = temp[i];
    }
}

三.总结

对七种算法时空复杂性,以及优缺点方面总结。

排序算法时间复杂度(平均)时间复杂度(最佳)时间复杂度(最差)空间复杂度稳定性优缺点
冒泡 O ( n 2 ) O(n^2) O(n2) O ( n ) O(n) O(n) 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)不稳定优点:移动数据的次数已知(n-1次); 缺点:比较次数多。
插入 O ( n 2 ) O(n^2) O(n2) O ( n ) O(n) O(n) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)稳定优点:稳定;缺点:比较次数不一定,比较次数越少,插入点后的数据移动越多,特别是当数据总量庞大的时候,但用链表可以解决这个问题。
希尔 O ( n l o g n ) O(nlogn) O(nlogn) 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)不稳定优点:快,数据移动少; 缺点:不稳定,d的取值是多少,应取多少个不同的值,都无法确切知道,只能凭经验来取。
快排 O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g n ) O(nlogn) O(nlogn) O ( n 2 ) O(n^2) O(n2) O ( n l o g n ) O(nlogn) O(nlogn)不稳定优点:极快,数据移动少; 缺点:不稳定
堆排 O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g n ) O(nlogn) O(nlogn) O ( 1 ) O(1) O(1)不稳定优点:据的有序性不敏感的一种算法,大规模数据性能优越。缺点:小规模的序列中不合适。
归并 O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g n ) O(nlogn) O(nlogn) O ( n ) O(n) O(n)稳定优点:稳定,对两个己有序的序列归并,非常合适对数据的有序性不敏感。缺点:若数据节点数据量大,那将不适合

总结这七种基本排序算法,应该能面对基本面试官的要求。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值