zoukankan      html  css  js  c++  java
  • 各主流排序算法详细介绍(时间/空间复杂度,适用范围和稳定性)

    一,插入排序

    插入排序基本思想:

      在一个已经有序的序列里插入新的元素,直到有序序列包含所有被排序元素。

    例子:

       

    代码实现:

    void InsertSort(vector<int> &v)
    {
        for(int i = 1;i < v.size(); ++i)//i表示有序集合里的元素数目和待插入元素下标
        {
            for(int j = i; j > 0 && v[j-1] > v[j]; --j)
            {
                int temp = v[j-1];
                v[j-1] = v[j];
                v[j] = temp;
            }
        }
    }
    View Code

    时间复杂度为O(N^2)

    空间复杂度为O(1)

    插入排序在小规模数据时或者基本有序时比较高效。

    二,希尔排序

    希尔排序基本思想:

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

    例子:

       

     在取增量d的时候一般有以下要求:

    ①最后一次排序d = 1;

    ②每次取的d并不包含处1以外的公因子;

    ③采用缩小增量法,d的取值逐步缩小。

    代码实现:

    void ShellSort(vector<int> &v)
    {
        int k = 0;
        int t = v.size() - 1;
        int delta = 2*t - k - 1;//有研究表明,delta取2*t-k-1时时间复杂度可达O(N^1.5) 
        for(k = 0;k < v.size(); ++k)
        {
            delta = (2*t-k-1) == t?(2*t-k-1):1;//最后一次delta要取1
            for(int i = 0; i < v.size()-delta; ++i)
            {
                if(v[i] > v[i+delta])
                {
                    int temp = v[i];
                    v[i] = v[i+delta];
                    v[i+delta] = temp;
                }
            }
        }
    }
    View Code

    希尔排序delta开始会很大,所以该排序不稳定。

    希尔排序的复杂度与delta相关:

    {1,2,4,8,...}使用这个增量序列的时间复杂度(最坏情形)是O(N^2)

    代码中所给序列的时间复杂度时O(N^1.5)

    空间复杂度为O(1)

    希尔排序为插入排序的一种优化,其适用范围为:大规模且无序的数据。

    三,冒泡排序

    冒泡排序基本思想:

      将第一个记录的关键字与第二个记录的关键字进行比较,若逆序则交换;然后比较第二个记录与第三个记录;依次类推,直至第n-1个记录和第n个记录比较为止——第一趟冒泡排序,结果关键字最大的记录被安置在最后一个记录上。

      对前n-1个记录进行第二趟冒泡排序,结果使关键字次大的记录被安置在第n-1个记录位置。重复上述过程,直到在一趟排序过程中没有进行过交换操作”为止。

    例子:

      

    代码实现:

    void BubbleSort(vector<int> &v)
    {
        for(int i = 0; i < v.size(); ++i)
        {
            for(int j = 0;j < v.size() - i; ++j)
            {
                if(v[j] < v[j-1])
                {
                    int temp = v[j];
                    v[j] = v[j-1];
                    v[j-1] = temp;
                }
            }
        }
    }
    View Code

    时间复杂度为O(N^2)

    空间复杂度为O(1)

    冒泡排序是稳定的排序算法。

    四,快速排序

    快速排序基本思想:

      快速排序的本质是分治法。从数列中挑出一个元素,称为 “基准”(pivot) 分区(partition):将序列分区,使所有元素比基准值小的放在基准前面,所有元素比基准值大的放在基准的后面(相等的数可以到任一边)(Divide) 排序:递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序(Conquer)。每次递归都会把基准放在正确的位置,即每次递归都排好一个元素,并将小于这个元素的放左边,大于这个元素的放右边。

    例子:

      记low为起始位置,high为末位置。基准一般取R[low],R[high],R[(low+high)/2]处于中间大小的数。这里叙述方便,直接取R[0]为基准。

    ①首先从后半部分开始,如果扫描到的值大于基准数据就让high减1,如果发现有元素比该基准数据的值小,就将high位置的值赋值给low位置 。

      

     ②然后开始从前往后扫描,如果扫描到的值小于基准数据就让low加1,如果发现有元素大于基准数据的值,就再将low位置的值赋值给high位置的值,指针移动并且数据交换后的结果如下:

     

     ③继续进行以上步骤,直到low >= high 停止扫描,此时基准R[0]处于正确的位置上。

     ④然后递归的处理左子序列和右子序列。

    代码实现:

    int Parition(vector<int>&v,int low,int high)
    {
        int ref = v[low];
        if(low < high)
        {
            while(low < high)
            {
                while(low < high && v[high] >= ref)
                {
                    --high;
                }
                v[low] = v[high];
                while(low < high && v[low] <= ref)
                {
                    ++low;
                }
                v[high] = v[low];
            }
            v[low] = ref;
        }
        return low;
    }
    
    void QSort(vector<int>&v,int low,int high)
    {
        if(low < high)
        {
            int index = Parition(v,low,high);
            QSort(v,low,index-1);
            QSort(v,index+1,high);
        }
    }
    
    void QuickSort(vector<int>&v)
    {
        QSort(v,0,v.size()-1);
    }
    View Code

     时间复杂度为O(NlogN)

    空间复杂度为最优的是O(logN),最差的为O(N)

     不稳定。

    五,简单选择排序

    简单选择排序基本思想:

      首先通过n-1次关键字比较,从n个记录中找出关键字最小的记录,将它与第一个记录交换;

      再通过n-2次比较,从剩余的n-1个记录中找出关键字次小的记录,将它与第二个记录交换;

      重复上述操作,共进行n-1趟排序后,排序结束。

    例子:

      

    代码实现:

    void SelectSort(vector<int>& v)
    {
        for(int i = 0; i < v.size(); ++i)
        {
            int MIN = v[i],pos = i;;
            for(int j = i; j < v.size(); ++j)
            {
                if(MIN > v[j])
                {
                    MIN = v[j];
                    pos = j;
                }
            }
            v[pos] = v[i];
            v[i] = MIN;
        }
    }
    View Code

    时间复杂度为O(N^2)

    空间复杂度为O(1)

     不稳定,交换过程中可能发生相同数字的次序颠倒。

    六,堆排序

    堆排序基本思想:

      堆是一个具有这样性质的完全二叉树:每个非终端结点(记录)的关键字大于等于(小于等于)它的孩子结点的关键字。堆排序是利用堆的特性对记录序列进行排序的一种排序方法。将无序序列建成一个堆,得到关键字最小(或最大)的记录;输出堆顶的最小(大)值后,使剩余的n-1个元素重又建成一个堆,则可得到n个元素的次小值;重复执行,得到一个有序序列。因为堆是完全二叉树,所以我们可以用数组来储存。只要将需要排序的数组建立成堆,然后每次取出根结点,就把剩下的结点调整成堆;再取出根结点,如此下去,最后便能得到排好序的数据。

    例子:

     建堆:

    给定需要排序的6个数字:

    ①保证[0,0]范围内是个最大堆,12本身是个堆。

    ②保证[0,1]范围内是个最大堆,因为34 > 12,12在34的父节点上,所以需要调换12和34

    ③再保证[0,3]内是个最大堆,12 54都是34的儿子,所以调换34和54

     ④重复以上步骤,直到[0,n-1]都是个最大堆

    建堆结果是:

    调整堆并排序:

    成为堆以后那么heap[0]一定是该组数的最大值/最小值(这里建的是最大堆,所以heap[0]是最大值)。

    ①交换63和24,已知63是最大值,则63在排序后的正确位置上,我们假装数组只有n-1个数,把63踢出去。

     ②然后对[0,n-2]的所以数据进行调整,挑24左右儿子最大的与其比较,54 > 24,所以交换位置。

    然后12和31是交换位置后的24的左右儿子,又因为31 > 24交换24,31。

     这已经是一个最大堆了,将54和24交换位置(首位交换),54处在正确的位置上。

     继续重复上述过程,直到每个数都排好。

    代码实现:

    void GetHeap(vector<int>& v)//将数组v调整为最大堆
    {
        for(int i = 0; i < v.size(); ++i)
        {
            int CurrentIndex = i;//新插入的结点
            int Father = (CurrentIndex - 1) / 2;
            while(v[CurrentIndex] > v[Father])
            {
                int temp = v[CurrentIndex];
                v[CurrentIndex] = v[Father];
                v[Father] = temp;
                CurrentIndex = Father;
                Father = (CurrentIndex - 1) / 2;
            }
        }
    }
    
    void AdjustHeap(vector<int>& v,int index,int size)//交换首尾元素后调整最大堆
    {
        int right = 2*index + 2;
        int left = 2*index + 1;
        while(left < size)
        {
            int LargestIndex;
            if(v[left] < v[right] && right < size)
            {
                LargestIndex = right;
            }
            else
            {
                LargestIndex = left;
            }
            if(v[index] > v[LargestIndex])
            {
                LargestIndex = index;
            }
            if(index == LargestIndex)
            {
                break;
            }
            int temp = v[LargestIndex];
            v[LargestIndex] = v[index];
            v[index] = temp;
            index = LargestIndex;
            left = 2*index + 1;
            right = 2*index + 2;
        }
    }
    
    void HeapSort(vector<int>& v)
    {
        GetHeap(v);
        int size = v.size();
        while(size > 1)
        {
            int temp = v[0];
            v[0] = v[size - 1];
            v[size - 1] = temp;
            --size;
            AdjustHeap(v,0,size); 
        }
    }
    View Code

    时间复杂度为O(NlogN)

    空间复杂度为O(1)

    不稳定。

    七,归并排序

    归并排序基本思想:

      归并——将两个或两个以上的有序表组合成一个新的有序表,叫归并。

      2-路归并排序:将两个位置相邻的记录有序子序列归并为一个记录的有序序列。

      如果记录无序序列 R[s..t] 的两部分 R[s...(s+t)/2]   和   R[(s+t)/2+1..t] 分别按关键字有序, 则很容易将它们归并成整个记录序列是一个有序序列。具体看例子。

    例子:

      对一个序列不断递归(分治法),然后两两归并并排序。

      

    代码实现:

    void MergeArray(vector<int> &v, int left, int mid, int right, int temp[])//归并并排序
    {
        int i = left,j = mid+1;
        int n = mid,m = right;
        int k = 0;
    
        while(i <= n && j <= m)
        {
            if(v[i] < v[j])
            {
                temp[k++] = v[i++];
            }
            else
            {
                temp[k++] = v[j++];
            }
        }
        while(i <= n)
        {
            temp[k++] = v[i++];
        }
        while(j <= m)
        {
            temp[k++] = v[j++];
        }
        for(int i = 0; i < k; ++i)
        {
            v[left+i] = temp[i];
        }
    }
    
    void Divide(vector<int>& v,int left,int right,int temp[])//分治
    {
        if(left < right)
        {
            int mid = (left+right)/2;
            Divide(v,left,mid,temp);
            Divide(v,mid+1,right,temp);
            MergeArray(v,left,mid,right,temp);
        }
    }
    
    bool MergeSort(vector<int>& v)
    {
        int* temp = new int[v.size()];
        if(temp == NULL)
        {
            return false;
        }
        Divide(v,0,v.size()-1,temp);
        delete [] temp;
        return true;
    }
    View Code

    时间复杂度非常稳定,最好和最差都是O(NlogN)

    空间复杂度为O(N) (new了一个大小为n的数组)

  • 相关阅读:
    bzoj 1176 cdq分治套树状数组
    Codeforces 669E cdq分治
    Codeforces 1101D 点分治
    Codeforces 1100E 拓扑排序
    Codeforces 1188D Make Equal DP
    Codeforces 1188A 构造
    Codeforces 1188B 式子转化
    Codeforces 1188C DP 鸽巢原理
    Codeforces 1179D 树形DP 斜率优化
    git commit -m "XX"报错 pre -commit hook failed (add --no-verify to bypass)问题
  • 原文地址:https://www.cnblogs.com/csuchenzc/p/12940416.html
Copyright © 2011-2022 走看看