美文网首页
排序算法

排序算法

作者: China_ly | 来源:发表于2018-07-21 09:55 被阅读0次
    • 冒泡排序
    • 选择排序
    • 插入排序
    • 归并排序
    • 堆排序
    • 快速排序

    排序算法大体可分为两种:
      一种是比较排序,时间复杂度O(nlogn) ~ O(n^2),主要有:冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序等。
      另一种是非比较排序,时间复杂度可以达到O(n),主要有:计数排序,基数排序,桶排序等。

    常见比较排序算法的性能:

    739525-20160503202729044-614991035.jpg
    1. 冒泡排序(Bubble Sort)

      冒泡排序是一种极其简单的排序算法,也是我所学的第一个排序算法。它重复地走访过要排序的元素,依次比较相邻两个元素,如果他们的顺序错误就把他们调换过来,直到没有元素再需要交换,排序完成。这个算法的名字由来是因为越小(或越大)的元素会经由交换慢慢“浮”到数列的顶端。

      冒泡排序算法的运作如下:

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

    冒泡排序的代码如下:

    #include <stdio.h>
    
    // 分类 -------------- 内部比较排序
    // 数据结构 ---------- 数组
    // 最差时间复杂度 ---- O(n^2)
    // 最优时间复杂度 ---- 如果能在内部循环第一次运行时,使用一个旗标来表示有无需要交换的可能,可以把最优时间复杂度降低到O(n)
    // 平均时间复杂度 ---- O(n^2)
    // 所需辅助空间 ------ O(1)
    // 稳定性 ------------ 稳定
    
    void Swap(int A[], int i, int j)
    {
        int temp = A[i];
        A[i] = A[j];
        A[j] = temp;
    }
    
    void BubbleSort(int A[], int n)
    {
        for (int j = 0; j < n - 1; j++)         // 每次最大元素就像气泡一样"浮"到数组的最后
        {
            for (int i = 0; i < n - 1 - j; i++) // 依次比较相邻的两个元素,使较大的那个向后移
            {
                if (A[i] > A[i + 1])            // 如果条件改成A[i] >= A[i + 1],则变为不稳定的排序算法
                {
                    Swap(A, i, i + 1);
                }
            }
        }
    }
    
    int main()
    {
        int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 };    // 从小到大冒泡排序
        int n = sizeof(A) / sizeof(int);
        BubbleSort(A, n);
        printf("冒泡排序结果:");
        for (int i = 0; i < n; i++)
        {
            printf("%d ", A[i]);
        }
        printf("\n");
        return 0;
    }
    

    上述代码对序列{ 6, 5, 3, 1, 8, 7, 2, 4 }进行冒泡排序的实现过程如下:

    739525-20160329100443676-1647340243.gif
    2. 选择排序(Selection Sort)

      选择排序也是一种简单直观的排序算法。它的工作原理很容易理解:初始时在序列中找到最小(大)元素,放到序列的起始位置作为已排序序列;然后,再从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
      注意选择排序与冒泡排序的区别:冒泡排序通过依次交换相邻两个顺序不合法的元素位置,从而将当前最小(大)元素放到合适的位置;而选择排序每遍历一次都记住了当前最小(大)元素的位置,最后仅需一次交换操作即可将其放到合适的位置。

    选择排序的代码如下:

     #include <stdio.h>
    
    // 分类 -------------- 内部比较排序
    // 数据结构 ---------- 数组
    // 最差时间复杂度 ---- O(n^2)
    // 最优时间复杂度 ---- O(n^2)
    // 平均时间复杂度 ---- O(n^2)
    // 所需辅助空间 ------ O(1)
    // 稳定性 ------------ 不稳定
    
    void Swap(int A[], int i, int j)
    {
        int temp = A[i];
        A[i] = A[j];
        A[j] = temp;
    }
    
    void SelectionSort(int A[], int n)
    {
        for (int i = 0; i < n - 1; i++)         // i为已排序序列的末尾
        {
            int min = i;
            for (int j = i + 1; j < n; j++)     // 未排序序列
            {
                if (A[j] < A[min])              // 找出未排序序列中的最小值
                {
                    min = j;
                }
            }
            if (min != i)
            {
                Swap(A, min, i);    // 放到已排序序列的末尾,该操作很有可能把稳定性打乱,所以选择排序是不稳定的排序算法
            }
        }
    }
    
    int main()
    {
        int A[] = { 8, 5, 2, 6, 9, 3, 1, 4, 0, 7 }; // 从小到大选择排序
        int n = sizeof(A) / sizeof(int);
        SelectionSort(A, n);
        printf("选择排序结果:");
        for (int i = 0; i < n; i++)
        {
            printf("%d ", A[i]);
        }
        printf("\n");
        return 0;
    }
    

    选择排序过程如下图:


    739525-20160329102006082-273282321.gif

      选择排序是不稳定的排序算法,不稳定发生在最小元素与A[i]交换的时刻。
      比如序列:{ 5, 8, 5, 2, 9 },一次选择的最小元素是2,然后把2和第一个5进行交换,从而改变了两个元素5的相对次序。

    3. 插入排序(Insertion Sort)

      插入排序是一种简单直观的排序算法。它的工作原理非常类似于我们抓扑克牌

      对于未排序数据(右手抓到的牌),在已排序序列(左手已经排好序的手牌)中从后向前扫描,找到相应位置并插入。

      插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

    具体算法描述如下:

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

    插入排序的代码如下:

    #include <stdio.h>
    
    // 分类 ------------- 内部比较排序
    // 数据结构 ---------- 数组
    // 最差时间复杂度 ---- 最坏情况为输入序列是降序排列的,此时时间复杂度O(n^2)
    // 最优时间复杂度 ---- 最好情况为输入序列是升序排列的,此时时间复杂度O(n)
    // 平均时间复杂度 ---- O(n^2)
    // 所需辅助空间 ------ O(1)
    // 稳定性 ------------ 稳定
    
    void InsertionSort(int A[], int n)
    {
        for (int i = 1; i < n; i++)         // 类似抓扑克牌排序
        {
            int get = A[i];                 // 右手抓到一张扑克牌
            int j = i - 1;                  // 拿在左手上的牌总是排序好的
            while (j >= 0 && A[j] > get)    // 将抓到的牌与手牌从右向左进行比较
            {
                A[j + 1] = A[j];            // 如果该手牌比抓到的牌大,就将其右移
                j--;
            }
            A[j + 1] = get; // 直到该手牌比抓到的牌小(或二者相等),将抓到的牌插入到该手牌右边(相等元素的相对次序未变,所以插入排序是稳定的)
        }
    }
    
    int main()
    {
        int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 };// 从小到大插入排序
        int n = sizeof(A) / sizeof(int);
        InsertionSort(A, n);
        printf("插入排序结果:");
        for (int i = 0; i < n; i++)
        {
            printf("%d ", A[i]);
        }
        printf("\n");
        return 0;
    }
    

    上述代码对序列{ 6, 5, 3, 1, 8, 7, 2, 4 }进行插入排序的实现过程如下:


    739525-20160329095145504-1018443290.gif

      插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,比如量级小于千,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。

    4. 归并排序(Merge Sort)

      归并排序是创建在归并操作上的一种有效的排序算法,效率为O(nlogn),1945年由冯·诺伊曼首次提出。

     归并排序的实现分为递归实现与非递归(迭代)实现。递归实现的归并排序是算法设计中分治策略的典型应用,我们将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决整个大问题。非递归(迭代)实现的归并排序首先进行是两两归并,然后四四归并,然后是八八归并,一直下去直到归并了整个数组。

      归并排序算法主要依赖归并(Merge)操作。归并操作指的是将两个已经排序的序列合并成一个序列的操作,归并操作步骤如下:

    1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
    2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
    3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
    4. 重复步骤3直到某一指针到达序列尾
    5. 将另一序列剩下的所有元素直接复制到合并序列尾
        归并排序的代码如下
    #include <stdio.h>
    #include <limits.h>
    
    // 分类 -------------- 内部比较排序
    // 数据结构 ---------- 数组
    // 最差时间复杂度 ---- O(nlogn)
    // 最优时间复杂度 ---- O(nlogn)
    // 平均时间复杂度 ---- O(nlogn)
    // 所需辅助空间 ------ O(n)
    // 稳定性 ------------ 稳定
    
    
    void Merge(int A[], int left, int mid, int right)// 合并两个已排好序的数组A[left...mid]和A[mid+1...right]
    {
        int len = right - left + 1;
        int *temp = new int[len];       // 辅助空间O(n)
        int index = 0;
        int i = left;                   // 前一数组的起始元素
        int j = mid + 1;                // 后一数组的起始元素
        while (i <= mid && j <= right)
        {
            temp[index++] = A[i] <= A[j] ? A[i++] : A[j++];  // 带等号保证归并排序的稳定性
        }
        while (i <= mid)
        {
            temp[index++] = A[i++];
        }
        while (j <= right)
        {
            temp[index++] = A[j++];
        }
        for (int k = 0; k < len; k++)
        {
            A[left++] = temp[k];
        }
    }
    
    void MergeSortRecursion(int A[], int left, int right)    // 递归实现的归并排序(自顶向下)
    {
        if (left == right)    // 当待排序的序列长度为1时,递归开始回溯,进行merge操作
            return;
        int mid = (left + right) / 2;
        MergeSortRecursion(A, left, mid);
        MergeSortRecursion(A, mid + 1, right);
        Merge(A, left, mid, right);
    }
    
    void MergeSortIteration(int A[], int len)    // 非递归(迭代)实现的归并排序(自底向上)
    {
        int left, mid, right;// 子数组索引,前一个为A[left...mid],后一个子数组为A[mid+1...right]
        for (int i = 1; i < len; i *= 2)        // 子数组的大小i初始为1,每轮翻倍
        {
            left = 0;
            while (left + i < len)              // 后一个子数组存在(需要归并)
            {
                mid = left + i - 1;
                right = mid + i < len ? mid + i : len - 1;// 后一个子数组大小可能不够
                Merge(A, left, mid, right);
                left = right + 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);
        MergeSortRecursion(A1, 0, n1 - 1);          // 递归实现
        MergeSortIteration(A2, n2);                 // 非递归实现
        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;
    }
    

    上述代码对序列{ 6, 5, 3, 1, 8, 7, 2, 4 }进行归并排序的实例如下:

    739525-20160328211743473-909317024.gif
    5. 堆排序(Heap Sort)

      堆排序是指利用堆这种数据结构所设计的一种选择排序算法。堆是一种近似完全二叉树的结构(通常堆是通过一维数组来实现的),并满足性质:以最大堆(也叫大根堆、大顶堆)为例,其中父结点的值总是大于它的孩子节点。

      我们可以很容易的定义堆排序的过程:

    1. 由输入的无序数组构造一个最大堆,作为初始的无序区
    2. 把堆顶元素(最大值)和堆尾元素互换
    3. 把堆(无序区)的尺寸缩小1,并调用heapify(A, 0)从新的堆顶元素开始进行堆调整
    4. 重复步骤2,直到堆的尺寸为1
        堆排序的代码如下:
    #include <stdio.h>
    
    // 分类 -------------- 内部比较排序
    // 数据结构 ---------- 数组
    // 最差时间复杂度 ---- O(nlogn)
    // 最优时间复杂度 ---- O(nlogn)
    // 平均时间复杂度 ---- O(nlogn)
    // 所需辅助空间 ------ O(1)
    // 稳定性 ------------ 不稳定
    
    
    void Swap(int A[], int i, int j)
    {
        int temp = A[i];
        A[i] = A[j];
        A[j] = temp;
    }
    
    void Heapify(int A[], int i, int size)  // 从A[i]向下进行堆调整
    {
        int left_child = 2 * i + 1;         // 左孩子索引
        int right_child = 2 * i + 2;        // 右孩子索引
        int max = i;                        // 选出当前结点与其左右孩子三者之中的最大值
        if (left_child < size && A[left_child] > A[max])
            max = left_child;
        if (right_child < size && A[right_child] > A[max])
            max = right_child;
        if (max != i)
        {
            Swap(A, i, max);                // 把当前结点和它的最大(直接)子节点进行交换
            Heapify(A, max, size);          // 递归调用,继续从当前结点向下进行堆调整
        }
    }
    
    int BuildHeap(int A[], int n)           // 建堆,时间复杂度O(n)
    {
        int heap_size = n;
        for (int i = heap_size / 2 - 1; i >= 0; i--) // 从每一个非叶结点开始向下进行堆调整
            Heapify(A, i, heap_size);
        return heap_size;
    }
    
    void HeapSort(int A[], int n)
    {
        int heap_size = BuildHeap(A, n);    // 建立一个最大堆
        while (heap_size > 1)           // 堆(无序区)元素个数大于1,未完成排序
        {
            // 将堆顶元素与堆的最后一个元素互换,并从堆中去掉最后一个元素
            // 此处交换操作很有可能把后面元素的稳定性打乱,所以堆排序是不稳定的排序算法
            Swap(A, 0, --heap_size);
            Heapify(A, 0, heap_size);     // 从新的堆顶元素开始向下进行堆调整,时间复杂度O(logn)
        }
    }
    
    int main()
    {
        int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 };// 从小到大堆排序
        int n = sizeof(A) / sizeof(int);
        HeapSort(A, n);
        printf("堆排序结果:");
        for (int i = 0; i < n; i++)
        {
            printf("%d ", A[i]);
        }
        printf("\n");
        return 0;
    }
    

    堆排序算法的演示:


    739525-20160328213839160-2037856208.gif

      堆排序是不稳定的排序算法,不稳定发生在堆顶元素与A[i]交换的时刻。

      比如序列:{ 9, 5, 7, 5 },堆顶元素是9,堆排序下一步将9和第二个5进行交换,得到序列 { 5, 5, 7, 9 },再进行堆调整得到{ 7, 5, 5, 9 },重复之前的操作最后得到{ 5, 5, 7, 9 }从而改变了两个5的相对次序。

    5.快速排序(Quick Sort)

      快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序n个元素要O(nlogn)次比较。在最坏状况下则需要O(n^2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他O(nlogn)算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。

      快速排序使用分治策略(Divide and Conquer)来把一个序列分为两个子序列。步骤为:

    1.从序列中挑出一个元素,作为"基准"(pivot).
    2.把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。

    1. 对每个分区递归地进行步骤1~2,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。
        
      快速排序的代码如下:
    #include <stdio.h>
    
    // 分类 ------------ 内部比较排序
    // 数据结构 --------- 数组
    // 最差时间复杂度 ---- 每次选取的基准都是最大(或最小)的元素,导致每次只划分出了一个分区,需要进行n-1次划分才能结束递归,时间复杂度为O(n^2)
    // 最优时间复杂度 ---- 每次选取的基准都是中位数,这样每次都均匀的划分出两个分区,只需要logn次划分就能结束递归,时间复杂度为O(nlogn)
    // 平均时间复杂度 ---- O(nlogn)
    // 所需辅助空间 ------ 主要是递归造成的栈空间的使用(用来保存left和right等局部变量),取决于递归树的深度,一般为O(logn),最差为O(n)       
    // 稳定性 ---------- 不稳定
    
    void Swap(int A[], int i, int 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)              // 把小于等于基准的元素放到前一个子数组末尾
            {
                Swap(A, ++tail, i);
            }
        }
        Swap(A, tail + 1, right);           // 最后把基准放到前一个子数组的后边,剩下的子数组既是大于基准的子数组
                                            // 该操作很有可能把后面元素的稳定性打乱,所以快速排序是不稳定的排序算法
        return tail + 1;                    // 返回基准的索引
    }
    
    void QuickSort(int A[], int left, int right)
    {
        if (left >= right)
            return;
        int 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;
    }
    

    使用快速排序法对一列数字进行排序的过程:

    739525-20160328215109269-23458370.gif

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

    相关文章

      网友评论

          本文标题:排序算法

          本文链接:https://www.haomeiwen.com/subject/rugcmftx.html