内部排序归纳(原理+实现)

插入排序

每次将一个待排序的数据按其关键字的大小插入到一个已经完成排序的有序序列中,直到所有记录排序结束。

  • 直接插入排序

    通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,从而找到相应的位置插入。

    在从后向前扫描过程中,需要反复把已排序的元素逐步向后挪位,为待插入的新元素提供插入空间。

    // 直接插入排序
    void SInsertSort(vector<int> &L){
        for (int i=1;i<L.size();i++)
        {
            int currentNumber=L[i];
            int j=i-1;
            // 依次后挪
            while(j>=0 && currentNumber<L[j])
            {
                L[j+1]=L[j];
                j--;
            }
            // 插入当前待排序元素
            L[j+1]=currentNumber;
        }
    }
    

    时间复杂度:\(O(n^2)\)

    空间复杂度:\(O(1)\)

    稳定

  • 折半插入排序

    通过二分有序表确定插入位置可以减少扫描次数。但插入次数与直接插入排序相同。

    //折半插入排序
    void BInsertSort(vector<int> &L)
    {
        int low,high,mid;
        for(int i=1;i<L.size();i++)
        {
            int currentNumber=L[i];
            low=0;
            high=i-1;
            while(low<=high)
            {
                mid=low+(high-low)/2; //折半
                if(currentNumber<L[mid]) high=mid-1;
                else low=mid+1;
            }
            for(int j=i-1;j>=low;j--)
            {
                L[j+1]=L[j];
            }
            L[low]=currentNumber;
        }
    }
    

    时间复杂度:\(O(n^2)\)

    空间复杂度:\(O(1)\)

    稳定(⚠️:与细节有关,若条件变为currentNumber<=L[mid],则算法不稳定,可用{2,1,1}测试。

  • 表插入排序

    以上两种插入排序算法都需要大量移动记录,表插入排序不需要移动记录而是通过改变存储结构来进行排序。

    通过链接指针、按关键字的大小实现从小到大的链接过程,需要增设指针项。

  • 希尔排序

    Shell Sort,又称缩小增量排序,是对直接插入排序的一种改进。

    将待排序记录跳跃式分为若干子序列,分别进行直接插入排序。初期选用大增量间隔,然后逐步缩小增量,最后为1。

    //希尔排序
    void ShellSort(vector<int> &L)
    {
        // 间隔序列,在希尔排序中我们称之为增量序列(希尔增量序列)
        for (int gap = L.size()/ 2; gap > 0; gap /= 2) {
            // 分组
            for (int groupStartIndex = 0; groupStartIndex < gap; groupStartIndex++) {
                // 插入排序
                for (int currentIndex = groupStartIndex + gap; currentIndex < L.size(); currentIndex += gap) {
                    // currentNumber 站起来,开始找位置
                    int currentNumber = L[currentIndex];
                    int preIndex = currentIndex - gap;
                    while (preIndex >= groupStartIndex && currentNumber < L[preIndex]) {
                        // 向后挪位置
                        L[preIndex + gap] = L[preIndex];
                        preIndex -= gap;
                    }
                    // currentNumber 找到了自己的位置,坐下
                    L[preIndex + gap] = currentNumber;
                }
            }
        }
    }
    

    时间复杂度:在\(O(n^2)\)\(O(n\log_2n)\)之间,普遍认为最好的时间复杂度为\(O(n^{1.3})\)

    空间复杂度:\(O(1)\)

    不稳定

选择排序

每一趟从待排序序列中选取一个关键字最小的记录,直到全部元素排序完毕。

由于选择排序每一趟总是从待排序序列中选取最值,所以选择排序适用于从大量的元素中选择一部分排序元素的应用。

  • 简单选择排序

    \(i\)趟从第\(i\)个记录开始的\(n-i+1\)个记录中选出关键字最小的记录与第\(i\)个记录交换,直到整个序列按关键字有序。

    //简单选择排序
    void SelectSort(vector<int> &L, int n)
    {
        for(int i=0;i<n;i++)
        {
            int minindex=i;
            for(int j=i+1;j<n;j++)
            {
                if(L[j]<L[minindex])
                {
                    minindex=j;
                }
            }
            //交换
            int tmp=L[i];
            L[i]=L[minindex];
            L[minindex]=tmp;
        }
    }
    

    时间复杂度:\(O(n^2)\)

    空间复杂度:\(O(1)\)

    稳定

  • 堆排序

    堆的定义:n个元素的序列\(\{k_1,k_2,\cdots,k_n\}\),当且仅当任一\(k_i\)满足以下关系时,称之为堆。

    \[\left\{ \begin{array}{lr} { k_i\leq k_{2i} \\ k_i \leq k_{2i+1} } \end{array} \right. 或 \ \left\{ \begin{array}{lr} { k_i \geq k_{2i} \\ k_i \geq k_{2i+1} } \end{array} \right. \]

    其中,\(i=1,2,\cdots,\lfloor n/2\rfloor\),分别称为小顶堆和大顶堆。

    根据堆的定义,它也是完全二叉树,具有如下性质之一:

    (1)每个结点的值都小于等于其左右孩子结点的值,称之为小顶堆

    (2)每个结点的值都大于等于其左右孩子结点的值,称之为大顶堆

    堆排序的基本思想:首先用待排序的记录构造一个堆,此时选出堆中所有记录的最小者作为堆顶,随后将它从堆中移走(通常是将堆顶记录和堆中最后一个记录交换),并将剩余的记录再调整成堆,以此类推,直到堆中只有一个记录为止。

    //堆排序
    /////////////////////////////子函数/////////////////////////////////
    //交换元素
    void exchange(vector<int> &L,int i, int j)
    {
        int tmp=L[i];
        L[i]=L[j];
        L[j]=tmp;
    }
    //调整大顶堆,第三个参数表示尚未排序元素的数量,即剩余堆的大小
    void maxHeapify(vector<int> &L, int i, int heapsize)
    {
        // 左、右子结点下标
        int left=2*i+1;
        int right=left+1;
        // 记录根结点、左子树结点、右子树结点三者中的最大值下标
        int largest=i;
        //与左子树比较
        if(left<heapsize && L[left]>L[largest]) largest=left;
        //与右子树比较
        if(right<heapsize && L[right]>L[largest]) largest=right;
        if(largest!=i)
        {
            //将最大值与跟结点交换
            exchange(L,i,largest);
            //再次调整交换数字后的大顶堆
            maxHeapify(L,largest,heapsize);
        }
    
    }
    void buildMaxHeap(vector<int> &L)
    {
        //从最后一个非叶子结点开始调整大顶堆,最后一个非叶子结点下标为L.size()/2-1
        for(int i=L.size()/2-1;i>=0;i--)
        {
            maxHeapify(L,i,L.size());
        }
    }
    /////////////////////////////主函数/////////////////////////////////
    void HeapSort(vector<int> &L)
    {
        //构建初始大顶堆
        buildMaxHeap(L);
        for(int i=L.size()-1;i>0;i--)
        {
            //将最大值放在数组最后
            exchange(L,0,i);
            //调整剩余数组,使其满足大顶堆
            maxHeapify(L,0,i);
        }
    }
    

    时间复杂度:\(O(n\log_2n)\)

    空间复杂度:\(O(1)\)

    不稳定

交换排序

借助比较和交换进行排序的方法。其中交换是指对序列中两个记录的关键字进行比较,如果排序顺序不对则对换两个记录在序列中的位置。

  • 冒泡排序

    通过对待排序元素中相邻元素间关键字的比较和交换,使关键字最大的元素如气泡一样逐渐“上浮”。

    //冒泡排序
    void BubbleSort(vector<int> &L)
    {
        bool swapped=true; // 初始置true以启动冒泡排序
        for(int i=0;i<L.size()-1;i++)
        {
            if(!swapped) break; //如上轮未发生交换,则说明剩余元素有序,无需再比较
            swapped=false;
            for(int j=0;j<L.size()-1-i;j++)
            {
                if(L[j]>L[j+1])
                {
                    //未引入第三个临时变量即完成两个数字的交换
                    L[j+1]=L[j+1]+L[j];
                    L[j]=L[j+1]-L[j];
                    L[j+1]=L[j+1]-L[j];
                    swapped=true; //交换元素置true
                }
            }
        }
    }
    

    时间复杂度:\(O(n^2)\)

    空间复杂度:\(O(1)\)

    稳定

  • 快速排序

    也称为分区交换排序

    通过对关键字的比较和交换,以待排序列中的某个数据为支点(或称枢轴量),将待排序列分为两个部分,其中左半部分数据小于等于支点,右半部分数据大于等于支点。然后,对左右两部分分别进行快速排序的递归处理,直到整个序列按关键字有序为止。

    //快速排序
    /////////////////////////////子函数/////////////////////////////////
    int partition(vector<int> &L, int low, int high)
    {
        int pivot=L[low];
        int left=low+1;
        int right=high;
        while(left<right)
        {
            //找到右边第一个小于基数的位置
            while(left<right&&L[right]>=pivot) right--;
            //找到左边第一个大于基数的位置
            while(left<right&&L[left]<=pivot) left++;
            //交换这两个数,使得左边分区都小于等于基数,右边分区都大于等于基数
            if(left<right)
            {
                exchange(L,left,right);
                left++;
                right--;
            }
        }
        //分情形讨论:
        //左指针==右指针:需要判断该元素值是否大于基数,若大于,则新支点前移
        if(left==right&&L[right]>pivot) right--;
        //左指针>右指针:需要以小于基数的值和基数做交换(因为原基数在左边),因此以右指针为新支点,能保证左右分区all clear
        exchange(L,low,right);
        return right;
    }
    void quickSort(vector<int> &L, int low, int high)
    {
        if (low < high)
        {
            //将数组分区,获取支点下标
            int mid = partition(L, low, high);
            //对左边区域快速排序
            quickSort(L, low, mid - 1);
            //对右边区域快速排序
            quickSort(L, mid + 1, high);
        }
    }
    /////////////////////////////主函数/////////////////////////////////
    void QuickSort(vector<int> &L)
    {
        quickSort(L, 0, L.size() - 1);
    }
    
    // 精简版快排
    void QuickSort2(vector<int> &L, int low, int high)
    {
        if (low < high)
        {
            int pivot = L[low];
            int left = low , right = high;
            while (left < right)
            {
                while (L[right] >= pivot && left < right)
                    right--;
                while (L[left] <= pivot && left < right)
                    left++;
                swap(L[left], L[right]);
            }
            swap(L[left], L[low]);
            QuickSort2(L, low, left - 1);
            QuickSort2(L, left + 1, high);
        }
        else return;
    }
    

    时间复杂度:\(O(n\log_2 n)\)

    ​ 最好情况:当每次支点都将待排序列划分为两个长度相等的子列时,此时:

    \[\begin{aligned} T(n)\leq n+2T(n/2)\leq n+2(n/2+2T(n/4))=2n+4T(n/4)\\ \leq 2n+4(n/4+2T(n/8))=3n+8T(n/8)\\ \leq \cdots\leq n\log _2n+nT(1)=O(n\log_2n) \end{aligned} \]

    ​ 最坏情况:当每次划分都只得到一个字列时,快速排序的执行过程类似于冒泡排序,此时快排效率最低,时间复杂度 为\(O(n^2)\)

    空间复杂度:由于快排是一个递归的过程,每层递归时都需要用栈来存放指针和相应的参数,且递归的层数与其二叉树的深度一致。因此其空间复杂度为:\(O(\log_2n)\)

    不稳定

归并排序

归并的含义是将两个或两个以上的有序序列归并成一个有序序列的过程。

  • 二路归并排序

    基本思想是:将待排序的n个元素看成是n个有序的子序列,每个子序列长度为1,然后两两归并,得到\(\lfloor \frac{n}{2}\rfloor\)个长度为2或1(最后一个有序序列可能为1)的有序子序列;再两两归并,得到\(\lfloor\frac{n}{4}\rfloor\)个长度为4或小于4(最后一个有序序列的长度可能小于4)的有序子序列;再两两归并,……直至得到一个长度为n的有序序列。

    //二路归并排序
    /////////////////////////////子函数/////////////////////////////////
    //将result的[start,middle]和[middle+1,end]合并
    void Merge(vector<int> &L, int start, int end, vector<int> &result)
    {
        int end1=(start+end)/2;
        int start2=end1+1;
        //用来遍历数组的指针
        int index1=start;
        int index2=start2;
        while(index1<=end1&&index2<=end)
        {
            if(L[index1]<=L[index2])
            {
                result[index1+index2-start2]=L[index1];
                index1++;
            }
            else
            {
                result[index1+index2-start2]=L[index2];
                index2++;
            }
        }
        //将剩余数字补在result后面
        while(index1<=end1)
        {
            result[index1+index2-start2]=L[index1];
            index1++;
        }
        while(index2<=end)
        {
            result[index1+index2-start2]=L[index2];
            index2++;
        }
        //将result操作区间的数字拷贝到L数组中,以便下次比较
        while(start<=end)
        {
            L[start]=result[start];
            start++;
        }
    }
    // 对L的[start,end]区间归并排序
    void mergesort_st(vector<int> &L, int start,int end, vector<int> &result)
    {
        //只剩下一个数字时,停止拆分
        if(start==end) return;
        int middle=(start+end)/2;
        //拆分出左边区域,将归并排序结果放在result[start,middle]中
        mergesort_st(L,start,middle,result);
        //拆分出右边区域,将归并排序结果放在result[middle+1,end]中
        mergesort_st(L,middle+1,end,result);
        //合并左右有序序列至result的[start,end]区间
        Merge(L,start,end,result);
    }
    /////////////////////////////主函数/////////////////////////////////
    void MergeSort(vector<int> &L)
    {
        if(L.size()==0) return;
        vector<int> result(L.size());
        mergesort_st(L,0,L.size()-1,result);
    }
    

    时间复杂度:\(O(n\log n)\),数组拆分\(\log n\)次,每次拆分需要比较次数约等于n

    空间复杂度:\(O(n)\),主要来自于排序前创建的长度为n的result数组

    稳定

附C++源码 链接

posted @ 2021-04-11 20:56  Joxyz  阅读(195)  评论(0)    收藏  举报