排序算法总结

选择排序

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

C

#include <stdio.h>

int main()
{
    int A[] = { 8, 5, 2, 6, 9, 3, 1, 4, 0, 7 }; // 从小到大选择排序
    int n = 10;
    int i, j, temp;
    for (i = 0; i <= n - 2; i++)//已排序序列的末尾(需要n-1轮,最后一次只剩1个数据未排序,直接加到前面序列末尾)
    {
        for (j = i + 1; j <= n - 1; j++)
        //未排序序列,第1轮(i=0)需要(n-1)次比较,...,第(n-1)轮(i=n-2)需要1次比较 
        {
            if (A[j] < A[i])// 依次找出未排序序列中的最小值,存放到已排序序列的末尾
            {
                temp = A[i];
                A[i] = A[j];
                A[j] = temp;
                //该操作很有可能把稳定性打乱,所以选择排序是不稳定的排序算法         
            }
        }
    }
    printf("选择排序结果:");
    for (i = 0; i < n; i++)
    {
        printf("%d ",A[i]);
    }
    printf("\n");
    return 0;
}

// 分类 --------------内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ----  O(n^2)
// 最优时间复杂度 ----  O(n^2)
// 平均时间复杂度 ----  O(n^2)
// 所需辅助空间 ------  O(1)
// 稳定性 ------------ 不稳定

C++

#include <iostream>
using namespace std;

//交换data1和data2所指向的整形
void DataSwap(int* data1, int* data2)
{
    int temp = *data1;
    *data1 = *data2;
    *data2 = temp;
}

/********************************************************
*函数名称:SelectionSort
*参数说明:pDataArray 无序数组;
*          iDataNum为无序数据个数
*说明:    选择排序
*********************************************************/
void SelectionSort(int* pDataArray, int iDataNum)
{
    for (int i = 0; i < iDataNum - 1; i++)    //从第一个位置开始
    {
        int index = i;
        for (int j = i + 1; j < iDataNum; j++)    //寻找最小的数据索引 
            if (pDataArray[j] < pDataArray[index])
                index = j;

        if (index != i)    //如果最小数位置变化则交换
            DataSwap(&pDataArray[index], &pDataArray[i]);
    }
}

int main(){
    int A[] = { 8, 5, 2, 6, 9, 3, 1, 4, 0, 7 }; // 从小到大选择排序

    SelectionSort(A,10);
    for(int i=0; i<10; i++) {
        cout<<A[i]<<" ";
    }
    cout<<endl;

    return 0;
}

//平均时间复杂度:O(n2)
//空间复杂度:O(1)  (用于交换和记录索引)
//稳定性:不稳定 (比如序列【5, 5, 3】第一趟就将第一个[5]与[3]交换,导致第一个5挪动到第二个5后面)

插入排序

//    插入排序是一种简单直观的排序算法。它的工作原理非常类似于我们抓扑克牌。原理如下: 
//   对于未排序数据(右手抓到的牌),在已排序序列(左手已经排好序的手牌)中从后向前扫描,找到相应位置并插入。
//  插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,
//    要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
//
//  具体算法描述如下:
//    1.从第一个元素开始,该元素可以认为已经被排序
//    2.取出下一个元素,在已经排序的元素序列中从后向前扫描
//    3.如果该元素(已排序)大于新元素,将该元素移到下一位置
//    4.重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
//    5.将新元素插入到该位置后
//    重复步骤2~5

C

#include <stdio.h>

int main()
{
    int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 };// 从小到大插入排序
    int n = sizeof(A) / sizeof(int);
    int i, j, get;

    for (i = 1; i < n; i++)             // 类似抓扑克牌排序
    {
        get = A[i];                     // 右手抓到一张扑克牌
        j = i - 1;                      // 拿在左手上的牌总是排序好的
        while (get < A[j] && j >= 0)    // 将抓到的牌与手牌从右向左进行比较
        {
            A[j + 1] = A[j];            // 如果抓到的牌比手里最右边的牌小,就将其最右的牌右移一位 
            j--;
        }
        A[j + 1] = get;
        //直到抓到的牌比该手牌大(或二者相等),将抓到的牌插入到该手牌右边(相等元素的相对次序未变,所以插入排序是稳定的)
    }   

    printf("插入排序结果:");
    for (i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    return 0;
}

// 分类 ------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- 最坏情况为输入序列是降序排列的,此时时间复杂度O(n^2)
// 最优时间复杂度 ---- 最好情况为输入序列是升序排列的,此时时间复杂度O(n)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定

快速排序

//快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序n个元素要O(nlogn)次比较。
//在最坏状况下则需要O(n^2)次比较,但这种状况并不常见。
//事实上,快速排序通常明显比其他O(nlogn)算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。
//
//  快速排序使用分治策略(Divide and Conquer)来把一个序列分为两个子序列。步骤为:
//
//    1.从序列中挑出一个元素,作为"基准"(pivot).
//    2.把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。
//    3.对每个分区递归地进行步骤1~3,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。

C


#include <stdio.h>

void exchange(int A[], int i, int j)        // 交换A[i]和A[j]
{
    int temp = A[i];
    A[i] = A[j];
    A[j] = temp;
}

int partition(int A[], int left, int right)  // 划分函数
{
    int pivot = A[right];                    // 选择最后一个元素作为基准
    int tail = left - 1;                     // tail为小于基准的子数组最后一个元素的索引
    for (int i = left; i < right; i++)       // 遍历基准以外的其他元素
    {
        if (A[i] <= pivot)                   // 遇到大的不管,把小于等于基准的元素放到前一个子数组中
        {
            tail++;
            exchange(A, tail, i);//不管大的,遇到小的,就把小的和第一个大的交换,保证两段序列前面的序列都小于等于基准,后面的序列都大于基准
            //这儿是关键 
            /*******************************
            这里可以优化,如果tail==i,就不做交换。
              if(tail!=i){
                 exchange(A, tail, i);
              }
            *********************************/ 
        }
    }
    exchange(A, tail + 1, right);            // 最后把基准放到前一个子数组的后边,剩下的子数组既是大于基准的子数组
    //该操作很有可能把后面元素的稳定性打乱,所以快速排序是不稳定的排序算法
    return tail + 1;                         // 返回基准的索引
}

void quicksort(int A[], int left, int right)
{
    int pivot_index;                        // 基准的索引
    if (left < right)
    {
        pivot_index = partition(A, left, right);
        quicksort(A, left, pivot_index-1);
        quicksort(A, pivot_index+1, right);
    }
}

int main()
{
    int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 };// 从小到大快速排序
    int n = sizeof(A) / sizeof(int);
    quicksort(A, 0, n - 1);
    printf("快速排序结果:");
    for (int i = 0; i < n; i++)
    {
        printf("%d ",A[i]);
    }
    printf("\n");
    return 0;
}

// 分类 ------------ 内部比较排序
// 数据结构 --------- 数组
// 最差时间复杂度 ---- 每次选取的基准都是最大的元素(或者每次都是最小),导致每次只划分出了一个子序列,需要进行n-1次划分才能结束递归,时间复杂度为O(n^2)
// 最优时间复杂度 ---- 每次选取的基准都能使划分均匀,只需要logn次划分就能结束递归,时间复杂度为O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ O(logn)~O(n),主要是递归造成的栈空间的使用(用来保存left和right等局部变量),取决于递归树的深度
//                   一般为O(logn),最差为O(n)(基本有序的情况)
// 稳定性 ---------- 不稳定

//快速排序是不稳定的排序算法,不稳定发生在基准元素与A[tail+1]交换的时刻。
//    比如序列:{ 1, 3, 4, 2, 8, 9, 8, 7, 5 },基准元素是5,一次划分操作后5要和第一个8进行交换,从而改变了两个元素8的相对次序。

图示过程

这里写图片描述


归并排序

//************************************************************************************************  
//    归并排序是创建在归并操作上的一种有效的排序算法,效率为O(nlogn),1945年由冯·诺伊曼首次提出。
//
//  归并排序的实现分为递归实现与非递归(迭代)实现。
//    递归实现的归并排序是算法设计中分治策略的典型应用,
//    我们将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决整个大问题。
//    非递归(迭代)实现的归并排序首先进行是两两归并,然后四四归并,然后是八八归并,一直下去直到归并了整个数组。

//************************************************************************************************  
//  归并排序算法主要依赖归并(Merge)操作。归并操作指的是将两个已经排序的序列合并成一个序列的操作,归并操作步骤如下:
//
//    1.申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
//    2.设定两个指针,最初位置分别为两个已经排序序列的起始位置
//    3.比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
//    4.重复步骤3直到某一指针到达序列尾
//    5.将另一序列剩下的所有元素直接复制到合并序列尾

C


#include <stdio.h>
#include <limits.h> //包含极限值的头文件,这里用到了无穷大INT_MAX

// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(nlogn)
// 最优时间复杂度 ---- O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ O(n)
// 稳定性 ------------ 稳定

int L[10];    // 两个子数组定义成全局变量(辅助存储空间,大小正比于元素的个数)
int R[10];

void merge(int A[], int left, int middle, int right)// 合并两个已排好序的数组A[left...middle]和A[middle+1...right]
{
    int n1 = middle - left + 1;     // 两个数组的大小
    int n2 = right - middle;
    for (int i = 0; i < n1; i++)    // 把两部分分别拷贝到两个数组中
        L[i] = A[left + i];
    for (int j = 0; j < n2; j++)
        R[j] = A[middle + j + 1];
    L[n1] = INT_MAX;                // 使用无穷大作为哨兵值放在子数组的末尾
    R[n2] = INT_MAX;                // 这样可以免去检查某个子数组是否已读完的步骤
    int i = 0;
    int j = 0;
    for (int k = left; k <= right; k++) // 依次比较两个子数组中的值,每次取出更小的那一个放入原数组
    {
        if (L[i] <= R[j]) 
        {
            A[k] = L[i];
            i++;
        }
        else
        {
            A[k] = R[j];
            j++;
        }
    }

}

void mergesort_recursion(int A[], int left, int right) // 递归实现的归并排序(自顶向下)
{
    int middle = (left + right) / 2;
    if (left < right)          // 当待排序的序列长度为1时(left == right),递归“开始回升”
    {
        mergesort_recursion(A, left, middle);
        mergesort_recursion(A, middle + 1, right);
        merge(A, left, middle, right);
    }
}

void mergesort_iteration(int A[], int left, int right)  // 非递归(迭代)实现的归并排序(自底向上)
{
    int low, middle, high;    // 子数组索引,前一个为A[low...middle],后一个子数组为A[middle+1...high]
    for (int size = 1; size <= right - left; size *= 2) // 子数组的大小初始为1,每轮翻倍
    {
        low = left;
        while (low + size - 1 <= right - 1 )// 后一个子数组存在(需要归并)
        {
            middle = low + size - 1;    
            high = middle + size;        
            if (high > right)// 后一个子数组大小不足size
                high = right;
            merge(A, low, middle, high);
            low = high + 1;// 前一个子数组索引向后移动
        }
    }
}

int main()
{
    int A1[] = { 6, 5, 3, 1, 8, 7, 2, 4 };    // 从小到大归并排序
    int A2[] = { 6, 5, 3, 1, 8, 7, 2, 4 };    
    int n1 = sizeof(A1) / sizeof(int);
    int n2 = sizeof(A2) / sizeof(int);
    mergesort_recursion(A1, 0, n1 - 1);       // 递归实现
    mergesort_iteration(A2, 0, n2 - 1);       // 非递归实现
    printf("递归实现的归并排序结果:");
    for (int i = 0; i < n1; i++)
    {
        printf("%d ",A1[i]);
    }
    printf("\n");
    printf("非递归实现的归并排序结果:");
    for (int i = 0; i < n2; i++)
    {
        printf("%d ", A2[i]);
    }
    printf("\n");
    return 0;
}

/*
归并排序的效率是比较高的,设数列长为N,将数列分开成小数列一共要logN步,
每步都是一个合并有序数列的过程,时间复杂度可以记为O(N),故一共为O(N*logN)。
因为归并排序每次都是在相邻的数据中进行操作,所以归并排序在O(N*logN)的几种排序
方法(快速排序,归并排序,希尔排序,堆排序)也是效率比较高的。
*/ 

排序算法的稳定性的意义

算法的稳定性是衡量一个算法健壮的标准之一,那算法的稳定性有什么意义呢?

此稳定非彼稳定。这里的稳定性和算法健壮没有一毛钱的关系。特指排序条件相等的两个元素,排序后的顺序是否和排序前一致。有时候我们需要按照多个条件排序,比如举重比赛,按照成绩排序,成绩相同,按照体重的逆序排序。那么如果排序算法是稳定的,我们可以先按照体重逆序排序后再按照成绩排序,则结果就是我们要的。如果是不稳定排序,我们需要额外的步骤保证结果的正确。

1、如果只是简单的进行数字的排序,那么稳定性将毫无意义。
2、如果排序的内容仅仅是一个复杂对象的某一个数字属性,那么稳定性依旧将毫无意义(所谓的交换操作的开销已经算在算法的开销内了,如果嫌弃这种开销,不如换算法好了?)。
3、如果要排序的内容是一个复杂对象的多个数字属性,但是其原本的初始顺序毫无意义,那么稳定性依旧将毫无意义。
4、除非要排序的内容是一个复杂对象的多个数字属性,且其原本的初始顺序存在意义,那么我们需要在二次排序的基础上保持原有排序的意义,才需要使用到稳定性的算法,例如要排序的内容是一组原本按照价格高低排序的对象,如今需要按照销量高低排序,使用稳定性算法,可以使得想同销量的对象依旧保持着价格高低的排序展现,只有销量不同的才会重新排序。(当然,如果需求不需要保持初始的排序意义,那么使用稳定性算法依旧将毫无意义)。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值