七大经典排序

七大经典排序

插入排序

插入排序开始先把第一个数字作为一个有序子数组,然后从第二个数字开始。

既然前面是一个有序的数组,那么当前这个数组只要逐个跟前面的有序子数组比较就行。

如果当前数字比前面的数字小,就把前面的数字往后挪一个位置。

直到当前的数字比比较的数字( 下标为:j )大为止,把当前数字填入到 j + 1 的位置。

即可表示当前数字插入成功。

// 插入排序
//
// 时间复杂度
// a. 最好:O(N)    已经有序的情况
// b. 平均:O(N^2)  
// c. 最坏:O(N^2)  已经有序是逆序
//
// 空间复杂度
// O(1)
//
// 稳定性:稳定
void InsertSort(std::vector<int>& arr)
{
    // 外循环实现减治过程
    // 一次取一个数,插入到前面的有序区间里
    for (size_t i = 1; i < arr.size(); i++)
    {
        int key = arr[i];
        // 内部的循环实现的是插入的过程
        // j 从 [i - 1, 0]
        // 如果 arr[j] > key, 往后搬
        // 如果 arr[j] == key, 跳出循环(保证了稳定性)
        // 如果 arr[j] < key, 跳出循环
        int j;
        for (j = i - 1; j >= 0 && arr[j] > key; j--)
        {
            arr[j + 1] = arr[j];
        }

        // j + 1 就是要插入的位置
        arr[j + 1] = key;
    }
}

希尔排序

希尔排序是在插入排序的基础上做了一个优化。

希尔排序是把一个数组先分成若干组,先对这几组的数据,每组之间进行插入排序。

然后,缩小间隔(gap),也就是说,减少组的数量,再次进行插入排序。

这样做的目的就是为了在最后一次,只有一组的情况下,尽可能的让这个数组里的数字已经有序。

gap 的变化是 :

先让 gap == arr.size()

然后 gap = gap / 3 + 1

再进行排序

直到 gap == 1 的情况排完,说明,这个数组已经完成了排序。(gap == 1, 说明它把整个数组分成了一个组,进行插入排序,排完一定有序),就可以跳出了。

// 希尔排序
// 希尔排序是插入排序的优化版本
// 在插入排序之前,尽可能的让数据有序
// 分组插排
// 通过维护一个 gap 来实现分组
// gap 初始化 为 gap = arr.size() / 3 + 1
// 后面 gap = gap / 3 + 1
//
// 时间复杂度
// a. 最好: O(N)
// b. 平均: O(N^1.2) ~ O(N^1.3)
// c. 最坏: O(N^2) 这里虽然跟插入排序的最坏时间复杂度一样
// 可是它把遇到最坏情况的概率变小了
//
// 空间复杂度
// O(1)
//
// 稳定性:不稳定。
// 一旦分组,很难保证相同的数在一个组里

// 可以设置 gap 间隔的插入排序
void InsertSortWithGap(std::vector<int>& arr, int gap)
{
    for (size_t i = gap; i < arr.size(); i++)
    {
        int key = arr[i];
        int j;
        for (j = i - gap; j >= 0 && arr[j] > key; j -= gap)
        {
            arr[j + 1] = arr[j];
        }

        // j + 1 就是要插入的位置
        arr[j + gap] = key;
    }
}

// 希尔排序
void ShellSort(std::vector<int>& arr)
{
    int gap = arr.size();
    while (true)
    {
        gap = gap / 3 + 1;

        InsertSortWithGap(arr, gap);

        // 如果 gap == 1 ,说明上次排序已经排完了整个数组
        if (gap == 1)
            break;
    }
}

堆排序

堆排序比较好理解。

如果排升序,首先建一个大堆。

然后每次把对顶元素(数组里最大的数字)和最后一个元素交换。这个时候向下调整的时候,size - 1 。也就是说,把最后一个元素进行向下调整,忽略已经在数组最后的堆顶元素。

以此类推,直到 size = 1 已经把所有数字排完。(就是依次把当前 size 个数组元素里最大的数字放到最后)

// 堆排序--》升序建大堆
//
// 时间复杂度
// 最好 = 平均 = 最坏 = O(N * LogN)
// 向下调整时间复杂度是 O(LogN)
// 建堆的时间复杂度是 O(N * LogN)
// 排序的时间复杂是 O(O * LogN)
//
// 空间复杂度 O(1)
//
// 稳定性:不稳定
// 向下调整过程中,无法保证相等数的前后关系
//
// 向下调整
void AdJustDown(std::vector<int>& arr, int size, int root)
{
    // 如果此时 root 是叶子结点,调整结束退出
    if (root * 2 + 1 >= size)
        return;

    // 找到最大的孩子
    int left = (root * 2) + 1;
    int max = left;
    int right = (root * 2) + 2;
    if (right < size && arr[right] > arr[left])
    {
        max = right;
    }

    // 判断当前 root 是否小于叶子结点
    if (arr[root] < arr[max])
    {
        std::swap(arr[root], arr[max]);
        AdJustDown(arr, size, max);
    }
    
    return;
}
// 建堆
void CreateHeap(std::vector<int>& arr)
{
    // 从第一个双亲结点开始向下调整
    // 逐渐向上走,直到根结点向下调整结束
    for (int i = (int)(arr.size() - 2) / 2; i >= 0; i--)
    {
        AdJustDown(arr, (int)arr.size(), i);
    }
}
// 堆排序
void HeapSort(std::vector<int>& arr)
{
    // 先建堆
    CreateHeap(arr);

    // 然后每次将堆顶元素和最后一个元素交换
    // size - 1
    // 向下调整
    for (int i = 0; i < (int)arr.size(); i++)
    {
        std::swap(arr[0], arr[arr.size() - i - 1]);

        AdJustDown(arr, arr.size() - i - 1, 0);

    }
}

选择排序

这个排序也很简单。

就是每次把当前数组里最大的元素放到最后。然后进行减治运算就行。

(优化版本也就是每次选两个,一个最大元素的下标,一个最小元素的下标。然后把最大的放到最后,最小的放到最前面,再进行减治)

// 选择排序
//
// 每次遍历无序区间,找到最大数的下标
// 1. 交换最大的数和无序区间的最后一个数
// 2. 区间 size - 1
// 3. 重新循环

// 时间复杂度
// 最好 = 最坏 = 平均 = O(N^2)
//
// 空间复杂度
// O(1)
//
// 稳定性:不稳定。 
// {9, 4, 3, 5a, 5b} 无法保证5a在5b前
void SelectSort(std::vector<int>& arr)
{
    for (int i = 0; i < (int)arr.size() - 1; i++)
    {
        int max = 0;
        for (int j = 0; j < (int)arr.size() - i; j++)
        {
            if (arr[j] > arr[max])
                max = j;
        }

        std::swap(arr[max], arr[arr.size() - 1 - i]);
    }
}

冒泡排序

这个跟选择排序差不多,但是冒泡排序是每次通过交换的方式,把最大的放到最后面。

然后进行减治。

// 冒泡排序
//
// 时间复杂度
// 最好 :O(N)
// 最坏|平均:O(N^2) 逆序
//
// 空间复杂度
// O(1)
//
// 稳定性:稳定

void BubbleSort(std::vector<int>& arr)
{
    for (int i = 0; i < (int)arr.size(); i++)
    {
        int sorted = 0;
        for (int j = 0; j < (int)arr.size() - 1 - i; j++)
        {
            if (arr[j] > arr[j + 1])
            {
                std::swap(arr[j], arr[j + 1]);
                sorted = 1;
            }
        }

        if (sorted == 0)
            break;
    }
}

快速排序

快速排序是先确定一个基准值(一般取数组的最左边的数或者最右边的数)我这里取数组最右边的数,然后确定两个下标left right。

如果基准值取最右边的数,那么先让左侧的下标从左往右找,找到第一个比基准值大的数字停下来,然后右侧的下标开始从右往左遍历数组,找到第一个比基准值小的值停下来。交换 left right 所对应的值。

直到两个下标相遇,把相遇的数字和基准值交换就可以了。

然后以基准值为中心,把数组分成两个小数组,再次在小数组内进行刚才的操作。

直到小数组的元素个数小于等于1,就可以停下来了。

三种分割方法

1. hoare 法

// hoare 版本
int Parition(std::vector<int>& arr, int left, int right)
{
    int div = right;

    while (left < right)
    {
        // 比较的时候要加 = 的情况
        // 比如说一个例子 1 1 1 1 1 
        // 如果不加等于 就成死循环了
        
        // 先从左边找到一个比基准值大的数字
        while (left < right && arr[left] <= arr[div])
            left++;

        // 从右边开始找一个比基准大小的数字
        while (left < right && arr[right] >= arr[div])
            right--;

        if (left < right)
            std::swap(arr[left], arr[right]);
    }

    std::swap(arr[left], arr[div]);

    return left;
}

2. 挖坑法

// 挖坑法
int ParitionDiggintPit(std::vector<int>& arr, int left, int right)
{
    // 把基准值获取到,然后这个位置成为了一个坑
    // 下次找到满足条件的数,就把那个数填进去
    // 那么被填进去的数原本的位置就成了新的坑
    int base_value = arr[right];
    int pit = right;
    int div = right;

    while (left < right)
    {
        while (left < right && arr[left] <= arr[div])
        {
            left++;
        }
        // 找到一个比基准值大的值,将这个值填入坑中
        arr[pit] = arr[left];
        // 更新坑的位置
        pit = left;

        while (left < right && arr[right] >= arr[div])
        {
            right--;
        }
        // 找到了一个比基准值小的值,将这个值填入坑中
        arr[pit] = arr[right];
        // 更新坑的位置
        pit = right;
    }

    arr[pit] = base_value;

    return pit;
}

3. 拉窗帘法

// 拉窗帘法
int ParitionSlideWindow(std::vector<int>& arr, int left, int right)
{
    int div = right;
    int d = 0;
    int c = 0;

    while (c < div)
    {
        // 始终让滑动窗口内部的值大于基准值
        if (arr[c] >= arr[div])
        {
            c++;
        }
        else
        {
            if (d < c)
                std::swap(arr[d], arr[c]);
            
            d++;
            c++;
        }
    }

    // 走到这表示 d 之前的值都比基准值小
    // 把基准值和 arr[d] 交换即可
    std::swap(arr[d], arr[div]);

    return d;
}

整体代码:

// 快速排序
// [left, right]
// 1. 在要排序的区间内选择一个基准值
//  具体方法:
//  1)选择区间最右边这个数 arr[right]
//
// 2. 遍历整个区间,做一些数据交换,达到效果:
// 比基准值小的数,放到基准值左侧
// 比基准值大的数,放到基准值右侧
//
// 3. 分治算法:把一个问题变成两个同样的小问题
//
// 4. 用递归或非递归
// 终止条件:
//  1)分出来的小区间没有数了:分出的区间 size == 0
//  2)分出来的小区间已经有序了:区间的 size == 1

// 时间复杂度
// 最好|平均:O(N * LogN)
// 遍历一遍数组:O(N)
// 高度: Log N
//
// 最差:O(N^2) 
// 如果已经有序,就是一个单支树。
//
// 空间复杂度
// O(LogN) ~ O(N)  
// 递归调用的深度(二叉树的高度)
//
// 稳定性:不稳定

// Parition 三种方式
// hoare 版本
int Parition(std::vector<int>& arr, int left, int right)
{
    int div = right;

    while (left < right)
    {
        // 比较的时候要加 = 的情况
        // 比如说一个例子 1 1 1 1 1 
        // 如果不加等于 就成死循环了
        
        // 先从左边找到一个比基准值大的数字
        while (left < right && arr[left] <= arr[div])
            left++;

        // 从右边开始找一个比基准大小的数字
        while (left < right && arr[right] >= arr[div])
            right--;

        if (left < right)
            std::swap(arr[left], arr[right]);
    }

    std::swap(arr[left], arr[div]);

    return left;
}

// 挖坑法
int ParitionDiggintPit(std::vector<int>& arr, int left, int right)
{
    int base_value = arr[right];
    int pit = right;
    int div = right;

    while (left < right)
    {
        while (left < right && arr[left] <= arr[div])
        {
            left++;
        }
        // 找到一个比基准值大的值,将这个值填入坑中
        arr[pit] = arr[left];
        // 更新坑的位置
        pit = left;

        while (left < right && arr[right] >= arr[div])
        {
            right--;
        }
        // 找到了一个比基准值小的值,将这个值填入坑中
        arr[pit] = arr[right];
        // 更新坑的位置
        pit = right;
    }

    arr[pit] = base_value;

    return pit;
}

// 拉窗帘法
int ParitionSlideWindow(std::vector<int>& arr, int left, int right)
{
    int div = right;
    int d = 0;
    int c = 0;

    while (c < div)
    {
        // 始终让滑动窗口内部的值大于基准值
        if (arr[c] >= arr[div])
        {
            c++;
        }
        else
        {
            if (d < c)
                std::swap(arr[d], arr[c]);
            
            d++;
            c++;
        }
    }

    // 走到这表示 d 之前的值都比基准值小
    // 把基准值和 arr[d] 交换即可
    std::swap(arr[d], arr[div]);

    return d;
}

void _QuickSort(std::vector<int>& arr, int left, int right)
{
    // 终止条件
    if (left >= right)
        return;

    int div; // 基准值取最右边的值
    div = ParitionSlideWindow(arr, left, right); // 遍历 arr[left, right],把小的放左,大的放右

    _QuickSort(arr, left, div - 1);     
    _QuickSort(arr, div + 1, right);
}

void QuickSort(std::vector<int>& arr)
{
    _QuickSort(arr, 0, (int)arr.size() - 1);
}

归并排序

这个排序比较有意思。

现在说一个特殊的例子。如何给两个有序的数组进行排序呢?就是先开辟一个空间足以容纳两个数组的数组,然后分别从两个数组的开头遍历,谁小就往里放就行了。

那么归并也是这意思。它是先把一个数组一直二分。直到分成最小数组(指的是数组里只有一个或者两个元素),然后两个两个的进行合并。

然后逐渐向上回溯,两个两个的数组越来越大的而已。最后进行的就是合并两个有序数组。(这两个数组各占一半需要排序的数组)。

// 归并排序
// 对一个大数组进行排序的问题
// 变成了对左右两个小数组进行排序的问题o
//
// 时间复杂度
// 最好|平均|最坏:O(N * LogN)
//
// 空间复杂度
// O(N)
//
// 稳定性:稳定
//
// 优点:可以对硬盘上的数据进行排序

// 合并两个有序数组
// arr[left, mid)
// arr[mid, right)
// 该操作的
// 时间复杂度 
// O(N)
//
// 空间复杂度
// O(N)
void Merge(std::vector<int>& arr, int left, int mid, int right, 
{

    int left_index = left;
    int right_index = mid;
    int index = 0;

    while (left_index < mid && right_index < right)
    {
        if (arr[left_index] <= arr[right_index]) // <= 之所以加 
        {
            tmp[index++] = arr[left_index++];
        }
        else
        {
            tmp[index++] = arr[right_index++];
        }
    }

    while (left_index < mid)
    {
        tmp[index++] = arr[left_index++];
    }

    while (right_index < right)
    {
        tmp[index++] = arr[right_index++];
    }
    
    for (int i = 0; i < right - left; i++)
    {
        arr[left + i] = tmp[i];
    }
}

void _MergeSort(std::vector<int>& arr, int left, int right, std:
{
    if (right == left + 1)// 区间内还剩一个数
    {
        return;
    }
    if (left >= right)// 区间内没有数了
    {
        return;
    }

    int mid = left + (right - left) / 2;
    //  区间被分成左右两个小区间
    //  [left, mid)
    //  [mid, right)
    //  先把左右两个小区间进行排序,分治算法,递归解决
    
    _MergeSort(arr, 0, mid, tmp);
    _MergeSort(arr, mid, right, tmp);

    // 左右两个小区间已经有序
    // 合并两个有序数组
    Merge(arr, left, mid, right, tmp);
}

void MergeSort(std::vector<int>& arr)
{
    std::vector<int> tmp(arr.size());
    _MergeSort(arr, 0, arr.size(), tmp);
}

总结:

插入排序:

时间复杂度(平均):O(N^2)

空间复杂度:O(1)

稳定性:稳定

希尔排序:

时间复杂度:O(N^1.2) ~O(N^1.3)

空间复杂度:O(1)

稳定性:不稳定

选择排序:

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

空间复杂度:O(1)

稳定性:不稳定

冒泡排序:

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

空间复杂度:O(1)

稳定性:稳定

堆排序:

时间复杂度:O(N*LogN)

空间复杂度:O(1)

稳定性:不稳定

快速排序:

时间复杂度:O(N*LogN)

空间复杂度:O(LogN) ~ O(N)

稳定性:不稳定

归并排序:

时间复杂度:O(N*LogN)

空间复杂度:O(N)

稳定性:稳定

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值