C++ 数据结构(查找与排序)

冒泡排序 Bubble Sort


简明解释

通过依次比较、交换相邻的元素大小(按照由小到大的顺序,如果符合这个顺序就不用交换)。1 次这样的循环可以得到一个最大值,n - 1 次这样的循环可以排序完毕

属性

  • 稳定
  • 时间复杂度 O(n²)
  • 交换 O(n²)
  • 对即将排序完成的数组进行排序 O(n)(但是这种情况下不如插入排序块,请继续看下文)

核心概念

  • 利用交换,将最大的数冒泡到最后

基本实现

首先定义一个数组,并求出数组长度,将这两个以参数形式传入函数实现

int arr[]     = {1, 5, 4, 2, 7, 8, 9, 3, 6};
size_t length = end(arr) - begin(arr);
cout << "冒泡排序:" << endl;

BubbleSort(arr, length);
for (auto iter : arr) {
    cout << iter << " ";
}

实现:

void BubbleSort(int *p, size_t length) {
    for (auto i = 0; i < length; ++i) {
        for (auto j = 0; j < length - i - 1; ++j) {
            swap(p[j], p[j + 1]);
        }
    }
}

加入我有一个需求,以一个函数指针为参数,用以实现升序或降序的需求呢

BubbleSort(arr, length, [](int a, int b) { return b - a; });
void BubbleSort(int *p, size_t length, function<int(int lhs, int rhs)> compareFunc) {
    for (auto i = 0; i < length; ++i) {
        for (auto j = 0; j < length - i - 1; ++j) {
            if (compareFunc(p[j], p[j + 1]) > 0) {
                swap(p[j], p[j + 1]);
            }
        }
    }
}

调试输出:

 

选择排序 Selection Sort


简明解释

每一次内循环遍历寻找最小的数,记录下 minIndex,并在这次内循环结束后交换 minIndex 和 i 的位置

重复这样的循环 n - 1 次即得到结果。

属性

  • 不稳定
  • Θ(n²) 无论什么输入,均为 Θ(n²)
  • Θ(n) 交换注意,这里只有 n 次的交换,选择排序的唯一优点*

核心概念

  • “可预测”的时间复杂度,什么进来都是 O(n²),但不稳定,唯一的优点是减少了 swap 次数

基本实现

void SelectionSort(int *p, size_t length) {
    for (auto i = 0; i < length - 1; ++i) {
        int min = i;
        for (auto j = i + 1; j < length; ++j) {
            if (p[min] > p[j]) {
                min = j;
            }
        }
        if (min != i) {
            swap(p[i], p[min]);
        }
    }
}

 

插入排序 Insertion Sort


默认 a[0] 为已排序数组中的元素arr[1] 开始逐渐往已排序数组中插入元素从后往前一个个比较,如果待插入元素小于已排序元素,则已排序元素往后移动一位,直到待插入元素找到合适的位置并插入已排序数组。

经过 n - 1 次这样的循环插入后排序完毕。

属性

  • 稳定
  • 适合场景:对快要排序完成的数组时间复杂度为 O(n)
  • 非常低的开销
  • 时间复杂度 O(n²)

由于它的优点(自适应,低开销,稳定,几乎排序时的O(n)时间),插入排序通常用作递归基本情况(当问题规模较小时)针对较高开销分而治之排序算法, 如希尔排序快速排序

核心概念

  • 高性能(特别是接近排序完毕时的数组),低开销,且稳定
  • 利用二分查找来优化

基本实现

void InsertSort(int *p, int *begin, int *end) {
    for (auto iter = begin + 1; iter != end; ++iter) {
        const int tmp = *iter;
        auto index    = iter - begin;
        auto j        = index;
        while (j > 0 && tmp > p[j - 1]) {
            p[j] = p[j - 1];
            --j;
        }
        if (j != index) {
            p[j] = tmp;
        }
    }
}

代码可能比for i循环类型的难理解,原理一样,也可以利用其它语言的语法糖。

 

希尔排序 Shell Sort


简明解释

希尔排序是插入排序的改进版,它克服了插入排序只能移动一个相邻位置的缺陷(希尔排序可以一次移动 gap 个距离),利用了插入排序在排序几乎已经排序好的数组的非常快的优点

使用可以动态定义的 gap 来渐进式排序,先排序距离较远的元素,再逐渐递进,而实际上排序中元素最终位置距离初始位置远的概率是很大的,所以希尔排序大大提升了性能(尤其是 reverse 的时候非常快,想象一下这时候冒泡排序和插入排序的速度)。

而且希尔排序不仅效率较高(比冒泡和插入高),它的代码相对要简短,低开销(继承插入排序的优点),追求这些特点(效率要求过得去就好,代码简短,开销低,且数据量较小)的时候希尔排序是好的 O(n·log(n)) 算法的替代品

总而言之:希尔排序的性能优化来自增量队列的输入gap 的设定

属性

  • 不稳定
  • 在快要排序完成的数组有 O(n·log(n)) 的时间复杂度(并且它对于反转数组的速度非常快)
  • O(n^3/2) time as shown 

关于不稳定:

我们知道, 单次直接插入排序是稳定的,它不会改变相同元素之间的相对顺序,但在多次不同的插入排序过程中, 相同的元素可能在各自的插入排序中移动,可能导致相同元素相对顺序发生变化。因此, 希尔排序并不稳定

关于 worse-case time 有一点复杂:

The worse-case time complexity of shell sort depends on the increment sequence. For the increments 1 4 13 40 121…, which is what is used here, the time complexity is O(n3/2). For other increments, time complexity is known to be O(n4/3) and even O(n·log2(n)).

核心概念

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  1. 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到 O(n) 的效率
  2. 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位 ;

其中 gap(增量)的选择是希尔排序的重要部分。只要最终 gap 为 1 任何 gap 序列都可以工作。算法最开始以一定的 gap 进行排序。然后会继续以一定 gap 进行排序,直到 gap = 1 时,算法变为插入排序

Donald Shell 最初建议 gap 选择为 n / 2 并且对 gap 取半直到 gap 达到 1 。虽然这样取可以比 O(n²) 类的算法(插入排序、冒泡排序)更好,但这样仍然有减少平均时间和最差时间的余地。

void ShellSort(int *arr, size_t len) {
    int gap = 1;
    while (gap < len) {
        gap = gap * 3 + 1;
    }
    while (gap > 0) {
        for (auto i = gap; i < len; ++i) {
            auto idx = i - gap;
            auto tmp = arr[i];
            while (idx >= 0 && tmp < arr[idx]) {
                arr[idx + gap] = arr[idx];
                idx -= gap;
            }
            arr[idx + gap] = tmp;
        }
        gap = (int)floor(gap / 3);
    }
}

快速排序 Quick Sort


简明解释

  1. 从数列中挑出一个元素,称为"基准"(pivot),
  2. 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任何一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作
  3. 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。

属性

  • 不稳定
  • O(n²) time, 但是通常都是 O(n·log(n)) time (或者更快)
  • O(log(n)) extra space

When implemented well, it can be about two or three times faster than its main competitors, merge sort and heap sort

核心概念

  • 使用了分而治之的思想

基本实现

    cout << "\n快速排序:" << endl;
    QuickSort(arr, 0, end(arr) - begin(arr) - 1);
    for (auto iter : arr) {
        cout << iter << " ";
    }
int Partition(int *p, int l, int r) {
    auto x = p[l];
    while (l < r) {
        while (l < r && p[r] >= x) // 从右向左找第一个小于x的数
            r--;
        if (l < r)
            p[l++] = p[r];
        while (l < r && p[l] < x) // 从左向右找第一个大于等于x的数
            l++;
        if (l < r)
            p[r--] = p[l];
    }
    p[l] = x;
    return l;
}

void QuickSort(int *p, int l, int r) {
    auto position = 0;
    if (l < r) {
        position = Partition(p, l, r); //返回划分元素的最终位置
        QuickSort(p, l, position - 1); //划分左边递归
        QuickSort(p, position + 1, r); //划分右边递归
    }
}

二分查找 BinarySearch


这里补充一下二分查找的算法的实现。

核心概念是:折半

//调用处
cout << "\n二分查找:" << endl;
auto index = BinarySearch(arr, length, 9);
cout << "\n二分查找结果:" << index << endl;
//定义

int BinarySearch(int p[] /*此处int *p 也可以*/, size_t len, int target) {
    int min = 0;
    int max = static_cast<int>(len - 1);
    while (min <= max) {
        const auto mid = (min + max) / 2;
        cout << "  find index: " << p[mid] << endl;
        if (target == p[mid]) {
            cout << "  target finded ! " << p[mid] << endl;
            return mid;
        } else if (target < p[mid]) {
            max = mid - 1;
        } else if (target > p[mid]) {
            min = mid + 1;
        }
    }
    cout << "  target not found" << endl;
    return 0;
}

总结


  • 数据几乎快排序完成时?

插入排序不解释

  • 数据量小,对效率要求不高,代码简单时?

性能大小:希尔排序 > 插入排序 > 冒泡排序 > 选择排序

  • 数据量大,要求稳定的效率(不会像快速排序一样有 O(n²) 的情况)(如数据库中)?

堆排序

  • 数据量大,要求效率高,而且要稳定?

归并排序

  • 数据量大,要求最好的平均效率?

性能大小:快速排序 > 堆排序 > 归并排序

因为虽然堆排序做到了 O(n·log(n),而快速排序的最差情况是 O(n²),但是快速排序的绝大部分时间的效率比 O(n·log(n) 还要快,所以快速排序真的无愧于它的名字。(十分快速)

  • 选择排序绝对没用吗?

选择排序只需要 O(n) 次交换,这一点它完爆冒泡排序。

 

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值