各类排序分析汇总


以下代码全部由C++实现 , 如果没学过c++ 可以把cout看成是printf , 是用来输出的函数

①冒泡排序

i是标记左边的数组元素的下标,标记元素A[i] ,(同理j的作用也是如此)

假设A[0]是最大的元素 ,
所以先让j从0 ~ n - 1 遍历一次 , 如果有元素比当前的元素A[i]更小 , 则交换A[i]与A[j]
第一遍遍历:A[0] 与 A[1],A[2] , A[3] 。。。A[ n - 1 ] 全部比较一次
所以第一次 j 从0 ~ n- 1 遍历完时 , 数组首元素一定是最小的==

接下来假设 A[1] 是最大的元素 , j 又从 0 ~ n - 1 遍历一次 ,
当遍历完成时 ,A[1]与A[2] , A[3] 。。。A[ n - 1 ]又再次全部都比较了一次 ,
A[1] 一定时数组里面第二大的元素, 以此类推
数组元素慢慢变得有序

#include <iostream>

using namespace std;
typedef int *Int;//自定义一种指向int数据的指针类型的别名Int
const int MAX = 50;

inline Swap(Int a , Int b)
{
    int tmp = *a;
         *a = *b;
         *b =tmp;
}

void BubbleSort(int A[] , int n)
{
    for(int i = 0 ; i < n - 1 ; i++)
    {
        for(int j = i + 1 ; j < n ; j++)
        {
            if(A[j] < A[i])
            {
                Swap(A + i , A + j);
            }
        }
    }
}
int main()
{
    int A[MAX];
    for(int i = 0 ; i < MAX ; i++)
        A[i] = MAX - i;//数组元素为100 , 99 , 98依次降序排列
    BubbleSort(A , MAX);
    for(int i = 0 ; i < MAX ; i++)
        cout << A[i] << '\n'; // 数组元素将为 1 , 2 , 3依次升序
    return 0;
}

②选择排序

算法分析:

i , j , k 的作用:
i : 假设当前最大元素是A[i]
j 是拿来遍历用的从 0 ~ n - 1
k是拿来标记当前最大元素的下标 ,在遍历j的过程中如果有元素A[j]比A[k]还要小 , 那么就让
k 等于 j , 标记当前最大元素的下标 , 并在j的遍历结束之后检查 k 是否和 i 相等

如果k == i 那么说明在j遍历过的元素中没有元素比最开始假设的A[i]小 , A[i]仍旧是最小的,无须交换
如果 k != i 说明 j 遍历过的元素中有A[k]比A[i]更小 , 那么此时必须要把A[i] 和 A[k]交换
这样就得到了第i大的元素

#include <iostream>

using namespace std;

typedef int *Int;//自定义一种指向int数据的指针类型的别名Int
const int MAX = 50;
inline Swap(Int a , Int b)
{
    int tmp = *a;
         *a = *b;
         *b =tmp;
}

void SelectSort(int A[] , int n)
{
    int i , j , k ;
    for(i = 0 ; i < n - 1 ; i++)
    {
        k = i ; 假设A[i] 是第 i + 1 大的元素 , i == 0 时 , 假设A[0]是目前最大的元素
        for(j = i + 1 ; j < n ; j++)
        {
            if(A[j] < A[k])
                k = j ; 把最小元素的下标 j 记录到 k 中
        }
        if(k != i) 
        {   如果 k != i 说明 j 遍历过的元素中有A[k]比A[i]更小 ,
            那么此时必须要把A[i]  和 A[k]交换   
           Swap(A + i , A + k);
        }
    }
}

int main()
{
    int A[MAX];
    for(int i = 0 ; i < MAX ; i++)
        A[i] = MAX - i;//数组元素为100 , 99 , 98依次降序排列
    SelectSort(A , MAX);
    for(int i = 0 ; i < MAX ; i++)
        cout << A[i] << '\n'; // 数组元素将为 1 , 2 , 3依次升序
    return 0;
}

③插入排序

算法分析:

插入排序相当于是打扑克牌时给扑克牌的大小进行排序(可以想象一下自己的手指等于5张牌,抽出第2张牌)
先从手中抽出第二张牌开始与前面的牌的大小进行比较 ,(tmp = A【1】)(抽牌)
如果左边的牌大于右边的牌 , 那么就把
左边的牌往右边错位( A【 j 】 = A【j - 1】),意思就是把左边的牌放到右边 , 再检查下一张牌(j–)是否比手中的牌大(A【j - 1】> tmp ?, 如果还是 true , 继续错位A【j】 = A【j - 1】,直到没有下一张牌 j - 1 >= 0 为假 ,【防止越界用的】)
如果内循环为假 , 就要把牌插入回去 ,A【j】 = tmp , 接着开始抽第三张牌与前面的比较 , 每次内循环都消除一些逆序对 ,循环结束之后就没有了逆序对 , 也就有序了 ,实现代码如下:

#include <iostream>

using namespace std;
const int MAX = 50;
void InsertSort(int A[] , int n)
{
    int i , j , tmp ;
    for(i = 1; i < n ; i++)
    {
        tmp = A[i]; // 抽牌与前面的牌比较
        for(j = i ; j >= 1 && A[j - 1] >= tmp ; j--)
        // 没有下一张牌 , 或者前面的牌都比手上的牌小就会跳出内循环
        {
            A[j] = A[j - 1]; // 前面的牌大于手上的牌就往后错位 , 左边牌放到右边
        }
        A[j] = tmp; // 把手上的牌插入回正确的位置
    }
}
int main()
{
    int A[MAX];
    for(int i = 0 ; i < MAX ; i++)
        A[i] = MAX - i;//数组元素为50 , 49 , 48依次降序排列
    InsertSort(A , MAX);
    for(int i = 0 ; i < MAX ; i++)
        cout << A[i] << '\n'; // 数组元素将为 1 , 2 , 3依次升序
    return 0;
}

④希尔排序

算法分析:

希尔排序类似于前面的插入排序
先把区间的一些用增量隔开的数进行插入排序 ,接着当增量为1时 , 执行插入排序 , 此时的希尔排序就变成了插入排序 ,其实就只需要把前面插入排序的代码中的 1 全部替换成为 Increment(增量)就可 以啦
具体实现代码如下

#include <iostream>

using namespace std;
const int MAX = 1e1;
void ShellSort(int A[] , int n)
{
    int i , j , tmp , Increament;
    for(Increament = n / 2 ; Increament > 0 ; Increament /= 2)
    {
        for(i = Increament ; i < n ; i++)
        {
            tmp = A[i];
            for(j = i ; A[j - Increament] > tmp && j - Increament >= 0 ; j -= Increament)
            {
                A[j] = A[j - Increament];
            }
            A[j] = tmp;
        }
    }
}

int main()
{
    int A[MAX];
    for(int i = 0 ; i < MAX ; i++)
        A[i] = MAX - i;
    ShellSort(A , MAX);
    for(int i = 0 ; i < MAX ; i++)
        cout << A[i] << "\n";
    return 0;
}

⑤堆排序

算法分析:

如果是最小堆
主要就是插入的时候父节点一定比子节点小 ,这是堆的有序性 , 也是排序的前提
要知道以下结论:
父节点的下标为 i , 那么左儿子的下标一定是 2 * i , 右儿子就是 2 * i + 1

其实堆排序就是对选择排序的一种改进 , 选择排序需要线性地寻找最小元 , 但是堆排序因为其有序性 , 堆顶元素就是最小值 , 直接就找到了 , 所以可以把堆排序看成是选择排序的改进。

堆排序原理

删除的时候就是删除最小元 , 删除最小元的时候会不断上溯 ,维护了堆的有序性 , 使得
删除之后就是仍然会有一个根节点是最小元 , 然后我们可以不断的删除当前堆里面的最小元(可以写一个删除并返回最小元的函数 , 前提是堆不能为空!) ,不断地得到当前数组里面的最小元 不断地维护堆的有序性 , 然后输出那个返回值(如果不输出的话可以考虑存到另外一个数组里面 , 但是就是比较费时间 ,拖慢了堆排序

往堆里面删除和插入的讲解最好就是看机械工业出版社的那本
<<数据结构与算法分析 :C语言描述>>
讲的非常的不错

时间复杂度:

O(N log N)

堆排序实现代码有点长 , 我把它放到头文件里面了。。。。

头文件(类声明)

#ifndef HEAP_H_INCLUDED
#define HEAP_H_INCLUDED
#include<cstdlib>
typedef int *Int;
class Heap
{
private:
    int Size    ;
    int Capacity;
    Int Array   ;
public:
     Heap(int Capacity_) : Size(0) , Capacity(Capacity_) , Array(new int[Capacity_])
     {
        if(!Array)
        {
            FatalError("Out of space.");
        }
        Array[0] = -1e8;
     }
    ~Heap(){delete[] Array;}
    bool isFull();
    bool isEmpty();
    int  DeleteMin() ;
    void AddElement(int Element);
    void HeapSort()  ;
    void Error(const char * str)
    {
        std::cout << str << '\n';
    }
    void FatalError(const char * str)
    {
        std::cout << str << "\n";
        exit(1);
    }
};


#endif // HEAP_H_INCLUDED

类实现

#ifndef IMPLEMENTS_H_INCLUDED
#define IMPLEMENTS_H_INCLUDED
#include "heap.h"
bool Heap::isFull()
{
    return Size >= Capacity;
}
bool Heap::isEmpty()
{
    return Size == 0;
}
void FatalError(const char * str)
{
     std::cout << str << "\n";
     exit(1);
}

void Error(const char * str)
{
    std::cout << str << '\n';
}

int  Heap::DeleteMin()
{
    int Child , Parent;
    int LastElement , Min_Element;
    if(isEmpty())
    {
        Error("The heap is empty.");
        return 0;
    }
    LastElement = Array[Size--];
    Min_Element = Array[1];
    for(Parent = 1 ; Parent * 2 <= Size ; Parent = Child)
    {
        Child = Parent * 2 ;
        if(Child + 1 <= Size && Array[Child + 1] < Array[Child])
        {
            Child++;
        }
        if(LastElement < Array[Child])
        { /**Easy to be forgotten!*/
            break;
        }
        else
        {
            Array[Parent] = Array[Child];
        }
    }
    Array[Parent] = LastElement;
    return Min_Element;
}

void Heap::AddElement(int Element)
{
    int i ;
    if(isFull())
    {
        FatalError("Full Heap.");
    }
    for(i = ++Size ; i >= 1 && Array[i/2] > Element ; i /= 2)
    {   /*Decrease swap times.*/
        Array[i] = Array[i/2];
    }
    Array[i] = Element;
}

void Heap::HeapSort()
{
    while(!isEmpty())
    {
        std::cout << DeleteMin() << '\n';
    }
}

主函数

#include <iostream>
#include"heap.h"
#include "implements.h"
using namespace std;
const int MAX = 1e5;
typedef class Heap *PtrHeap;
int main()
{
    {
        Heap h(MAX + 1);
        for(int i = 0 ; i < MAX ; i++)
            h.AddElement(MAX - i);
        h.HeapSort();
    }
    return 0;
}

⑥归并排序(MergeSort)

算法分析:

分治算法:

这种递归的算法最开始理解的时候就是比较费劲 , 但是我们一定要用分治的思想去理解这种算法
先假装MergeSort的具体实现代码已经实现 , 并可以直接调用
在 Merge_Sort(int A[] , int n)里面调用如下的具体的实际函数
MergeSort( A , tmp , Left , Center) ; // 先处理左边的子数组
MergerSort(A , tmp , Center + 1 , Right); // 后处理右边的子数组
MergeSort(A , tmp , Left , Right); // 合并两个数组
通过以上代码分别处理左右的子数组

时间复杂度分析:

数组都被分成是2个子数组 , T(N) = T(N / 2 ) + T(N / 2 ) + O(N)
接着不断递归直到不能再分 , 因此N / 2 又被分成了N / 4 ,然后就是不断递推啦
得到T(N) = O( N log N )

归排并没有最好最坏平均时间复杂度, 任何情况下都是N log N ,归并排序都是稳定的算法

编码注意:

申请堆上的数组时 , 一定要在归排序Merge_Sort那里申请一个数组就可以了,
不要在递归的函数里面申请 , 否则会不断的申请空间和归还空间 , 会严重拖慢排序 ,
而且容易申请空间失败

#include <iostream>

using namespace std;
const int MAX = 1e3;
void MergeArray(int A[] , int tmpArray[] , int Lpos , int Rpos , int RightEnd)
{ // 到这里才有具体的实现细节
	// Lpos是Left Position 意思是左位置,就是标记左边子数组的下标用的啦 , Rpos也是一样的
    int LeftEnd = Rpos - 1; // 得到左边子数组的最后面的下标
    int TmpC    = Lpos    ; // 动态申请的临时数组的下标为TmpC
    int NumElement = RightEnd - Lpos + 1;
    while(Lpos <= LeftEnd && Rpos <= RightEnd)
    {
        if(A[Lpos] <= A[Rpos])
            tmpArray[TmpC++] = A[Lpos++];
        else
            tmpArray[TmpC++] = A[Rpos++];
    }
    while(Lpos <= LeftEnd) tmpArray[TmpC++] = A[Lpos++];
    while(Rpos <= RightEnd)tmpArray[TmpC++] = A[Rpos++];
    for(int i = 0 ; i < NumElement ; i++ , RightEnd--)
    {
        A[RightEnd] = tmpArray[RightEnd];
    }
}
void MergeSort(int A[] , int tmp[] , int Left , int Right)
{
    if(Left < Right)
    {  
       int Center = (Left + Right) / 2 ; // 假装已经处理好了所有的细节,于是开始分治(&&递归) 
       MergeSort(A , tmp , Left , Center); //先处理左边的子数组
       MergeSort(A , tmp , Center + 1 , Right);// 后处理右边的子数组
       MergeArray(A , tmp , Left , Center + 1 , Right);// 合并两个数组
    }
}
void Merge_Sort(int A[] , int n)
{
    int * tmp = new int[n];
    if(tmp != nullptr)
    {
        MergeSort(A , tmp , 0 , n - 1);
        delete[] tmp;
    }
}
int main()
{
    int A[MAX];
    for(int i = 0 ; i < MAX ; i++)
        A[i] = MAX - i;//数组元素为100 , 99 , 98依次降序排列
    Merge_Sort(A , MAX);
    for(int i = 0 ; i < MAX ; i++)
        cout << A[i] << '\n'; // 数组元素将为 1 , 2 , 3依次升序
    return 0;
}

⑦快速排序(高度优化的快排)

算法分析:

快排比其他算法快的根本原因:
枢纽元在i 和 j 的遍历后被放到了最正确的位置 , 左边所有元素都比枢纽元小
右边所有元素都比枢纽元大, 这是快排比其他排序更快的原因
因为枢纽元被一次性放到了最合适的位置 ,从此之后不需要再交换位置
但是其他的算法都可能需要不断的改变位置

时间复杂度:

跟前面的归并排序类似 , 也是不断的把数组分为2个子数组 , 所以也是O(N log N )

#include <iostream>

using namespace std;
const int CUTOFF = 3;
const int MAX = 1e2;
inline void Swap(int *a , int *b)
{
    int tmp = *a;
         *a = *b;
         *b =tmp;
}
/**插入排序*/
void InsertSort(int A[] , int n)
{
    int i , j , tmp;
    for(i = 1 ; i < n ; i ++)
    {
        tmp = A[i];
        for(j = i ; j >= 1 && A[j - 1] >= tmp ; j--)
        {
            A[j] = A[j - 1];
        }
        A[j] = tmp;
    }
}
/**三数中值分割,哎,其实就是头中尾三个地方取中位数 , 并把枢纽元藏在Right - 1这个位置
   并且返回枢纽元的值*/
int  Medium3(int A[] , int Left , int Right)
{
    int Center = (Right + Left) / 2 ;
    if(A[Left]   > A[Center]) Swap(A + Left  , A + Center);
    if(A[Left]   > A[Right] ) Swap(A + Left  , A + Right );
    if(A[Center] > A[Right])  Swap(A + Center, A + Right );
    Swap(A + Center , A + Right - 1);
    //A[Left]<=A[Center]<=A[Right]
    //枢纽元是A[Center]
    //枢纽元和A[Right-1]交换位置可以防止j越界!!
    return A[Right - 1];
}
/**高度优化的快排*/
void QuickSort(int A[] , int Left , int Right)
{ /**只有当前的数组长度大于等于3时才能用中位数分割 ,否则就用插入排序*/
    if(Left + CUTOFF <= Right)
    {
        int Privot = Medium3(A , Left , Right);
        int i = Left ;
        int j = Right - 1;
        while(true)
        {
          while(A[++i] < Privot){} //如果小于枢纽元就迅速过,直到找到比枢纽元大的
          while(A[--j] > Privot){} //j 是从Right - 2 的位置开始遍历 ,Right - 1的位置是枢纽元的
          if(i < j) // 如果i 和 j 还没交错 , 就直接交换
                Swap(A + i , A + j);
            else
                break;
        }
        Swap(A + i , A + Right - 1); // 把枢纽元和刚刚开始交错的位置 i 交换位置 ,
        							// 此时枢纽元被放到了最正确的位置 , 左边所有元素都比枢纽元小
        							// 右边所有元素都比枢纽元大, 这是快排比其他排序更快的原因
        							// 因为枢纽元被一次性放到了最合适的位置 ,从此之后不需要再交换位置
        							// 但是其他的算法都可能需要不断的改变位置 
        QuickSort(A , Left , i); /**分治 , 分左边和右边分别处理*/
        QuickSort(A , i + 1 , Right);
    }
    else
        InsertSort(A + Left , Right - Left + 1);
        //由于对数组的部分元素进行排序,所以数组是在一定范围之内排序的
        //所以数组排序的起始位置在A+Left处,得到排序的首地址,之后传入元素个数
        //主要就是因为划分子集后,总是只是对部分元素排序,传入的参数就有变化了
}
void Quick_Sort(int A[] , int n)
{
    QuickSort(A , 0 , n - 1);
}
int main()
{
    int A[MAX];
    for(int i = 0 ; i < MAX ; i++)
        A[i] = MAX - i;//数组元素为100 , 99 , 98依次降序排列
    Quick_Sort(A , MAX);
    for(int i = 0 ; i < MAX ; i++)
        cout << A[i] << '\n'; // 数组元素将为 1 , 2 , 3依次升序
    return 0;
}

写在最后

CSDN的博客不太会格式,有的地方看起来会比较奇怪,如有问题可以在评论留言,我会更正的

猜你喜欢

转载自blog.csdn.net/weixin_46028606/article/details/106227397