LeetCode算法题回顾 排序算法之内部排序算法

1.基础知识部分

排序算法分类:平常所说的排序算法大部分是指内部排序算法,其实还有三种常见的外部排序算法(计数、基数、桶排序)

  • 内部排序和外部排序

  1. 内排序是指所有的数据已经读入内存,在内存中进行排序的算法。排序过程中不需要对磁盘进行读写。同时,内排序也一般假定所有用到的辅助空间也可以直接存在于内存中。

  2. 外排序,即内存中无法保存全部数据,需要进行磁盘访问,每次读入部分数据到内存进行排序。

 

下面给出它们的具体分类:

  • 内部排序算法:冒泡排序、快速排序、直接择排序、直接插入排序、希尔排序、归并排序、堆排序

    • 其中冒泡排序和快速排序属于交换排序,直接插入排序和希尔排序属于插入排序

  • 外部排序算法:计数排序、基数排序、桶排序等

LeetCode算法题回顾 排序算法之内部排序算法_第1张图片

 概念讲解

  • 稳定性:如果i=j,排序前i在j的前面,排序后i仍然在j的前面,即相等的两个数字的相对位置在排序前后不变,则该算法是稳定的,否则不稳定。

  1. 举例: 用某一算法对[1,3,2,4,2]进行排序后的结果为[1,2,2,3,4],我们可以看到排序前粗体2在细体2之前,排序之后仍然是,则该算法为稳定的。

  2. 稳定性作用:排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,前一个键排序的结果可以为后一个键排序所用。可能比较难理解,这里再举个例子方便理解,比如在基数排序中,先将低位排序,再逐次按高位排序,稳定的话就可以保证排序后低位元素的顺序在高位相同时是不会改变的。

  • 时间复杂度:指执行算法所需要的工作量,即对待排序数据的总操作次数,我们用它来描述算法的运行时间。

  • 空间复杂度:指执行算法所需的内存空间。

Note:

  • 每个算法的稳定性并不是绝对的,比如冒泡算法若把判断条件由原来的if L(j) > L(j+1)改成if L(j) >= L(j+1),那么冒泡算法的稳定性就由原来的稳定变为不稳定(后面会详细讲解,这里只是举个例子


目录

1.基础知识部分

 概念讲解

1.冒泡排序

1.1 基本思想

1.2 具体步骤

1.3 代码实现 c++

1.4 改进的冒泡排序算法 c++

1.5 性质总结:

2. 快速排序(Quick Sort)

2.1 基本思想   

2.2 具体步骤

2.3 代码实现 c++

2.4 性质总结

2.5算法改进

3.直接选择排序

3.1 基本思想

3.2 具体步骤

3.3 代码实现 c++

3.4 性质总结

4.堆排序

4.1 基本思想

4.2 具体步骤(需要记住k节点的父节点的位置是[k/2])

4.3 代码实现 c++

4.4 性质总结

5.直接插入排序

5.1 基本思想

5.2 具体步骤

5.3 代码实现 c++

5.5 性质总结

6.0 希尔排序(Shell Sort)

6.1 基本思想

6.2 具体步骤

6.3 代码实现

6.4 性质总结

7.归并排序(Merge Sort)

7.1理解归并

7.2 具体步骤

7.3 代码实现

7.4 性质总结

七大排序算法源码

 

*博文参考:



1.冒泡排序

1.1 基本思想

冒泡排序可以算是最简单、最基础的排序算法了,它的基本思想是:重复的遍历待排序的一组数字(通常是列表形式),依次比较两个相邻的元素(数字),若它们的顺序错误则将它们调换一下位置,直至没有元素再需要交换为止。因为每遍历一次列表,最大(或最小)的元素会经过交换一点点”浮“到列表的一端(顶端),所以形象的称这个算法为冒泡算法

1.2 具体步骤

  1. 比较两个相邻元素,如果前一个比后一个大,则交换这两个相邻元素

  2. 从头至尾对每一对相邻元素进行步骤1的操作,完成1次对整个待排序数字列表的遍历后,最大的元素就放在了该列表的最后一个位置上了

  3. 对除最后一个元素的所有元素重复上述步骤,这第二次遍历后第二大的元素就也放在了正确的位置(整个列表的倒数第二位置上)

  4. 不断重复上述步骤,每次遍历都会将一个元素放在正确的位置上,从而下次遍历的元素也会随之减少一个,直至没有任何一对数字需要比较

1.3 代码实现 c++

/* @Description:

冒泡排序算法实现*/ public class BubbleSort { public : void bubbleSort(vector& arr) { if( arr.size() == 0 || arr.size() ==1 ) return ; for(int i=0; ii; j--) { if(arr[j] < arr[j-1]) swap(arr,j-1,j); } } } public: void swap(vector& arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } };

但是上面代码的时间复杂度=O(n^2),那么冒泡算法的最好状况时间复杂度O(n)是从何而来的呢,看下面的改进的冒泡算法

1.4 改进的冒泡排序算法 c++

可以发现,如果一组数字一开始就完全有序的话,上述算法还是会一遍一遍的遍历,对于这种不必要进行改进一下。

在排序后期可能数组已经有序了而算法却还在一趟趟的比较数组元素大小,可以引入一个标记,如果在一趟排序中,数组元素没有发生过交换说明数组已经有序,跳出循环即可。优化后的代码如下

/* @Description:

冒泡排序算法实现

*/ public class BubbleSort {  //************ 冒泡排序算法实现 */ //改进版冒泡排序 public: void bubbleAdSort(vector& arr) { if(arr.size() == 0 || arr.size() ==1 ) return ; bool flag = true; for(int i=0; ii; j--) { if(arr[j] < arr[j-1]) { flag =false; swap(arr, j-1, j); } } if(flag) break; } } public: void swap(vector& arr, int i, int j) {         int temp = arr[i];         arr[i] = arr[j];         arr[j] = temp;         } };

1.5 性质总结

  • 时间复杂度

    • 平均情况:O(n^2)

    • 最好情况:O(n)

    • 最坏情况:O(n^2)

  • 空间复杂度:O(1)

  • 稳定性:稳定 (仅限于本博客代码,前面说过若把判断条件由原来的if L(j) > L(j+1)改成if L(j) >= L(j+1),那么稳定性就由原来的稳定变为不稳定! 


2. 快速排序(Quick Sort)

快速排序采用了一种叫分治的思想。(idea = 分治+二分+冒泡)

分治法的基本思想是:将原问题分解为若干个规模更小但结构与原问题相似的子问题。递归地解这些子问题,然后将这些子问题的解组合为原问题的解

2.1 基本思想   

快速排序基本思想是:通过选取一个基准base,用一趟排序将待排序列表分割成独立的两部分,左边部分的所有元素都比base小,右边部分的所有元素都比base大,然后将左右两部分分别继续重复进行此操作,递归实现,从而达到最终将整个列表排序的目的。

快速排序示意图,图来源网上,侵删

2.2 具体步骤

  1. 从待排序列表(数组)中选择一个元素作为基准(pivot),这里我们选择最后一个元素元素

  2. 遍历列表,将所有小于基准的元素放在其前面,这样就可以将待排序列表分成两部分了

  3. 递归地对每个部分进行1、2操作,这里递归结束的条件是序列的大小为1,此时递归结束,排序就已经完成了

2.3 代码实现 c++

/***@Description:

实现快速排序算法

*/ class QuickSort { public:    void quicksort(vector &vec, int left, int right)     {         if (left>= right) return;         int keyloc = partition (vec,left, right);         quicksort( vec, left, keyloc-1);         quicksort( vec, keyloc+1, right);     }      public: //核心idea在于我们要在两端开始各自存储比key大和比key小的数 //所以选择left/right进行key值赋值后,我们要从另一端开始进行大小判定,才可以保证这个数不论大于KEY或者小于KEY都是在两端位置 int partition (vector &vec, int left, int right ){ int key = vec[left]; while( left < right){ while ( left= key) right--; vec[left] = vec [right]; while ( left &vec, int left, int right ){ int nr=right; int key = vec[right]; while( left < right){ while ( left= key ) right--; //两个指针指向的值的需要调换才满足顺序。(vec[left]>key&& vec[right] a={8,1,2,6,3,2,4,7,9,5,12};//示例数组,可改动或者直接输入     QuickSort sort;     sort.quicksort(a, 0, a.size()-1);          for (int i=0;i

2.4 性质总结

  • 时间复杂度: 

    • 平均情况:O(nlogn)

    • 最好情况:O(nlong)

    • 最坏情况:O(n^2) 快排的最坏情况就已经退化为冒泡排序 

  • 空间复杂度(用辅助数组/就地排序为O(1),如示例代码):

    • 平均情况:O(logn) 

    • 最好情况:O(logn) 每一次都平分数组的情况

    • 最坏情况:O(n) 退化为冒泡排序的情况

  • 稳定性:不稳定 (由于关键字的比较和交换是跳跃进行的,所以快速排序是一种不稳定的排序方法~)

  • 其他拓展:

LeetCode算法题回顾 排序算法之内部排序算法_第2张图片


2.5算法改进

(from*1 算法 第4版-谢路云译完整版)

LeetCode算法题回顾 排序算法之内部排序算法_第3张图片

LeetCode算法题回顾 排序算法之内部排序算法_第4张图片


3.直接选择排序

3.1 基本思想

选择排序的基本思想是:首先,在待排序列表中找到最小的元素并且将它和待排序中的第一个元素进行交换(如果是本身就和自己交换);然后,再从剩余待排序序列中找到最小的元素,重复时间上一过程直至只有一个元素。这样的排序叫做选择排序,因为它在不断地选择剩余元素之中那个最小者。

内循环只比较当前元素和目前已知的最小元素(记得对当前索引+1看是否越界),交换元素的代码写在内循环之外,每次交换排定一个元素,所以交换总次数为N-1,效率取决于比较的次数。

LeetCode算法题回顾 排序算法之内部排序算法_第5张图片 直接选择排序--《算法》第四版

3.2 具体步骤

  1. 初始状态整个待排序序列为无序序列,有序序列为空

  2. 每次遍历无序序列将最小元素交换到有序序列之后

  3. n-1趟遍历后排序完成

3.3 代码实现 c++

/***@Description:

实现直接选择排序算法

*/ #include #include class DCSort{ public: void DirectChooseSort(vector &vec, int left, int right) { if ( left >= right ) return; int min=INT_MAX; int min_index=0; for ( int i=left;i<=right;i++ ){ if( vec[i] &vec, int i, int j) { int temp = vec[i]; vec[i] = vec[j]; vec[j] = temp; } }; int main() { vector a={8,9,5,3,-24,-35}; DCSort sort; sort.DirectChooseSort(a, 0, a.size()-1); for (int i=0;i

3.4 性质总结

  • 时间复杂度:

    • 平均情况 / 最好情况 / 最坏情况:O(n^2)

  • 空间复杂度:O(1)

  • 稳定性:不稳定


4.堆排序

首先,堆的结构相当于一个完全二叉树, 堆排序是一种选择排序。

  • 堆的定义 堆是具有以下性质的完全二叉树:

  1. 每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;

  2. 或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:

 

LeetCode算法题回顾 排序算法之内部排序算法_第6张图片 大顶堆(最大堆)与小顶堆(最小堆)

 

同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子

 

该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:

大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]  

小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]  

 

4.1 基本思想

堆排序可以分为两个阶段:1)堆的构造阶段 + 2)下沉排序。 在堆的构造阶段中,我们将原始数组重新组织安排进一个堆中,然后在下沉排序阶段,从堆中按照递减顺序取出所有元素并得到排序结果。

具体来说:整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了

 

4.2 具体步骤(需要记住k节点的父节点的位置是[k/2])

步骤一 堆的构造( 从右至左使用sink() 比 从左至右使用swim()效率更高)

1)假设给定无序序列结构如下

 

LeetCode算法题回顾 排序算法之内部排序算法_第7张图片

2)此时我们从最后一个非叶子结点开始(叶结点不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整

 

LeetCode算法题回顾 排序算法之内部排序算法_第8张图片

3)找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。

 

LeetCode算法题回顾 排序算法之内部排序算法_第9张图片

4)这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。

 

LeetCode算法题回顾 排序算法之内部排序算法_第10张图片

此时,我们就将一个无需序列构造成了一个大顶堆。

步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。

1).将堆顶元素9和末尾元素4进行交换

 

LeetCode算法题回顾 排序算法之内部排序算法_第11张图片

2).重新调整结构,使其继续满足堆定义(循环不再考虑已经排出的节点值)

LeetCode算法题回顾 排序算法之内部排序算法_第12张图片

 

3).再将堆顶元素8与末尾元素5进行交换,得到第二大元素8.

LeetCode算法题回顾 排序算法之内部排序算法_第13张图片

 

后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序

 

LeetCode算法题回顾 排序算法之内部排序算法_第14张图片

步骤总结:

  1. 将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;

  2. 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;

  3. 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序

  4. 重复上述步骤,直至堆(无序区)的尺寸变为1,此时排序完成

示例2

LeetCode算法题回顾 排序算法之内部排序算法_第15张图片

LeetCode算法题回顾 排序算法之内部排序算法_第16张图片

4.3 代码实现 c++

class Solution {
public: void sink(vector& vec, int start, int N)//每个子树的根节点最大
{
    int temp=vec[start];
 
    for( int child = start*2+1; child=vec[child])  //注意是temp的值
            break;
        vec[start]=vec[child];
        start=child;
    }
    vec[start]=temp;
}

public: 
    void HeapSort(vector &vec){
        if( vec.size() <=1) return ;
        
        //建立大顶堆
        for (int i =vec.size()/2-1; i >= 0; i--)    //i 最大叶子节点
            sink(vec, i, vec.size());
        
        //交换堆顶与末尾元素,重新调整大顶堆
        for (int j = vec.size() - 1; j > 0; j--) {
            swap(vec, 0, j);
            sink(vec, 0, j);
        }
    }
};

int main()
{
    Solution sort;
    vector b={15,2,9,3,1,2,4,2,-6,132,4,22,44,5,12,78,9,2,54,63,32,7,98,90};
 
    sort.HeapSort(b, b.size());
    cout<<"Heap answer is  ";
    sort.Print(b,0,b.size()-1);
    cout<

 

4.4 性质总结

  • 时间复杂度:

    • 平均情况:O(nlogn)

    • 最好情况:O(nlogn)

    • 最坏情况:O(nlogn)

       堆排序效率很好

  • 空间复杂度:O(1)

  • 稳定性:不稳定


5.直接插入排序

5.1 基本思想

直接插入排序就是将未排序元素一个个的插入到已排序列表中,它的基本思想是:对于未排序元素,在已排序序列中从后向前扫描,找到相应位置把它插入进去;在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为新元素提供插入空间。重复直至没有未排序元素

LeetCode算法题回顾 排序算法之内部排序算法_第17张图片

5.2 具体步骤

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

5.3 代码实现 c++

public:
    void DirectInsertSort(vector &vec, int left, int right)
    {
        if ( left >= right ) return;
        int index = left+1;
        int insert = vec[index];
        int i;
        for ( i=left;i>=0;i--){
            if( vec[i]>=insert ) { vec[i+1]=vec[i]; }
            else break;
         }
        vec[i+1] = insert;
        
        left++;
        DirectInsertSort(vec, left,right);
    } 

5.5 性质总结

  • 时间复杂度:
    • 平均情况:O(n^2)
    • 最好情况:O(n)
    • 最坏情况:O(n^2)
  • 空间复杂度:O(1)

  • 稳定性:稳定


6.0 希尔排序(Shell Sort)

希尔排序也是一种插入排序,是改进直接插入排序的更高效版本,也叫做缩小增量排序。它与简单插入排序不同的是,它优先比较距离较远的元素。希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。

6.1 基本思想

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

希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。

一般我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2...1},这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。 

6.2 具体步骤

  1. 选择一个增量序列(定义增量的递减状况,直至最后为1)

  2. 按增量序列的个数k,对序列进行k趟排序

  3. 每趟排序,对各分组进行直接插入排序

示例

LeetCode算法题回顾 排序算法之内部排序算法_第18张图片

 

6.3 代码实现

NOTE : 理解希尔排序时,我们倾向于对于每一个分组,逐组进行处理但在代码实现中,我们可以不用这么按部就班地处理完一组再调转回来处理下一组(这样还得加个for循环去处理分组)比如[5,4,3,2,1,0] ,首次增量设gap=length/2=3,则为3组[5,2] [4,1] [3,0],实现时不用循环按组处理,我们可以从第gap个元素开始,逐个跨组处理。同时,在插入数据时,可以采用元素交换法寻找最终位置,也可以采用数组元素移动法寻觅。

  • 下面是不处理分组,逐个跨组处理,并采用元素交换法寻找最终位置的希尔排序实现。
public: 
    void shellSort(vector& vec ){
        if( vec.size()<=1) return;
        int gap = vec.size()/2;
        while(gap>0){
            for(int i=gap;i=0 && vec[j]& vec, int a, int b)
{
    int temp = vec[a];
    vec[a] = vec[b];
    vec[b] = temp;
}

6.4 性质总结

  • 时间复杂度:

    • 平均情况:O(nlogn~n^2)

    • 最好情况:O(n^1.3)

    • 最坏情况:O(n^2)

  • 空间复杂度:O(1)

  • 稳定性:不稳定


7.归并排序(Merge Sort)

归并排序的递归实现是算法设计中分治策略的典型应用,它的基本思想是:递归的将两个已排序的序列合并成一个序列。

7.1理解归并

归并的含义就是将两个或多个有序序列合并成一个有序序列的过程,归并排序就是将若干有序序列逐步归并,最终形成一个有序序列的过程。以最常见的二路归并为例,就是将两个有序序列归并。归并排序由两个过程完成:有序表的合并和排序的递归实现。

 

LeetCode算法题回顾 排序算法之内部排序算法_第19张图片

有序表的合并

  再来看看阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。

 

7.2 具体步骤

  1. 申请空间,其大小为两个已经排序序列之和,该空间用来存放合并后的序列

  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置

  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置

7.3 代码实现

public: 
    void MergeSort(vector& vec ){
        if( vec.size()<=1) return;
        vector temp(vec.size());
        DivideS( vec,temp, 0,vec.size()-1);
    }
public: 
    void DivideS(vector& vec,vector& temp, int left, int right){
        if( left& vec, int left,int mid, int right,vector& temp)
    {
      
        int i=left;     //左序列指针
        int j=mid+1;    //右序列指针
        int t=0;       //临时数组指针
        
        while( i<=mid && j<=right){
            if(vec[i]

7.4 性质总结

  • 时间复杂度:

    • 平均情况:O(nlogn)

    • 最好情况:O(nlogn)

    • 最坏情况:O(nlogn)

  • 空间复杂度:O(n)

  • 稳定性:稳定

    可以看出,归并排序的效率很好(与堆排序一样),只是它的空间复杂度较高(需要占用一定的内存空间)


七大排序算法源码

 

*博文参考:

  1. 算法 第4版-谢路云译完整版
  2. 十大排序算法和七大查找算法总结(原理讲解和代码实现)-------(一)排序算法篇 from 口天丶木乔 
  3. 七大排序算法 from涛声依旧~
  4. 面试中的排序算法总结 from Pickle

你可能感兴趣的:(Leetcode,代码实现与解析)