编程我只用CPP 发布的文章

一、堆排序原理

通过最大堆的性质可以知道:一个堆中最大的元素总是在堆顶的,即数组下标0的位置。基于这一点,我们可以每次都把堆中的最大值提取出来,放到当前数组的后面。然后重新构建最大堆,重复这个过程,以此来完成一个数组的排序。

例如,一个已知的最大堆为:

堆排序-1

把最大的元素16提取出来,放到最后。然后重新建堆,此时堆中最大的元素为15,更新后的堆为:

堆排序-2

再把15提取出来,重新建堆,得到:

最大堆-3

如此往复,直到最后堆中的元素只有一个,就完成了整个数组的排序。

二、代码实现

堆排序的关键点在于构造堆,如何构造堆可参考数据结构之堆。基于模板的最大堆化函数实现:

template <typename T>
static void max_heapify(T *data, size_t len, size_t idx) {
    size_t largest, lchild, rchild;

    lchild = LCHILD(idx);
    rchild = RCHILD(idx);

    if (lchild < len && data[lchild] > data[idx])
        largest = lchild;
    else
        largest = idx;

    if (rchild < len && data[rchild] > data[largest])
        largest = rchild;

    if (idx != largest) {
        my_swap(data[idx], data[largest]);
        max_heapify(data, len, largest);
    }
}

实现堆排序,堆排序的关键点在于从后往前排:

template <typename T>
static void heap_sort(T *data, size_t len) {
    size_t i, mid;
    mid = len / 2;

    // 建堆
    for (i = mid - 1; i >= 0 && i < len; i--) {
        max_heapify(data, len, i);
    }

    // 堆排序,从后往前
    for (i = len - 1; i >= 1; i--) {
        my_swap(data[i], data[0]);
        max_heapify(data, --len, 0);
    }
}

时间复杂度

堆排序的时间主要消耗再建堆上面,每次拿掉一个元素之后,都重新执行最大堆化

每次构造最大堆的时间复杂度为O(log(n)),因此堆排序的总时间复杂度为n(log(n)),n代表元素个数。

一、堆

堆是一种数据结构,通常通常所说的堆即二叉堆。二叉堆是一个数组,可以被看成一个完全二叉树,如下图所示:

他在数组中的表现形式为:

通过数组很容易得到每个父节点和其子节点的关系,假设数组的起始下标为0,那么有:

PARENT(i) = (i - 1) / 2  --> 如下标1和2的数组元素,其父节点是下标0的元素。
LEFT_CHILD(i) = (i * 2) + 1   
RIGHT_CHILD(i) = (i * 2) + 2  -->如下标为0的数组元素,其左右子节点的下标分别是1和2。

因此可以直接在程序中定义:

#define PARENT(i) ((i - 1) >> 1)
#define LEFT_CHILD(i) (((i) << 1) + 1)
#define RIGHT_CHILD(i) (((i) << 1 ) + 2)

二、最大堆和最小堆

堆有最大堆和最小堆之分,在最大堆中,除了根节点以外的所有节点i都要满足:A[PARENT[i]] >= A[i],所有的子节点的值不会超过其父节点的值。因此,在最大堆中,最大的元素存放在根节点中。

而最小堆和最大堆相反,除了根节点以外的所有节点i都要满足:A[PARENT[i]] <= A[i], 所有子节点的值都大于等于其根节点的值。因此,最小堆中根节点的元素是最小的。

在堆排序算法中,使用的是最大堆,最小堆通常用于构造优先级队列。

以最大堆为例,其包含的操作为:

  • MAX_HEAPIFY:用来维护一个最大堆。
  • BUILD_MAX_HEAP:从一堆无序的数组中构造出一个最大堆。
  • HEAPSORT:执行一次堆排序过程。

三、最大堆

3.1 维护最大堆

把一个堆构造成最大堆的流程:

  1. 从A[i], A[LEFT[i]], A[RIGHT[i]]中选出最小的,保存其下标largest
  2. 如果largest不等于i,交换A[i]和A[largest]

以下图为例,当前堆中4处于一个非正确的位置:

首先把4和其儿子中最大的14交换,得到以下堆:

交换后4依旧不是最小的元素,继续交换48

4当前已经是叶子节点了,此时对4的最大堆化操作就完成了。并且此时的堆也就是一个最大堆。

不难看出:对一个高度为h的树来说,这个操作的时间复杂度为O(h)

对应的c代码:

static void _max_heapify(int *data, const uint len, const uint i) {
    unsigned int lchild, rchild;
    unsigned int largest;

    lchild = LEFT_CHILD(i);
    rchild = RIGHT_CHILD(i);

    // 得到父子节点中最小的元素
    if (lchild < len && data[i] < data[lchild])
        largest = lchild;
    else
        largest = i;

    if (rchild < len && data[largest] < data[rchild])
        largest = rchild;

    // 交换最小的元素
    if (i != largest) {
        swap_int(&data[i], &data[largest]);
        _max_heapify(data, len, largest);
    }
}

3.2 建堆

建堆的过程实际上是执行多次MAX_HEAPIFY,我们只需对2/n以内的元素都执行MAX_HEAPIFY操作即可完成一个最大堆的构建。

因为[n/2, n)之间的元素都是叶子节点,所以无需对它们进行转换操作。

要注意的是建堆必须从下往上,否则可能出现堆只是局部有效,对全局而言并非有效:

以上图为例,如果从上到下建堆,在调整完4的位置之后,14被放在了树根。而实际上被放在树根的应该是16,因为接下来就不会调整14了,它的位置就永远不对,这个堆也就不是一个符合要求的堆。

代码

static void _build_max_heap(int *data, const uint len) {
    int i;
    for (i = (len / 2) - 1; i >= 0; i--) {
        _max_heapify(data, len, i);
    }
}

四、堆排序

参考:排序算法之堆排序

题目要求:随机输出100以内的不重复数字

解法一:暴力求解

最简单也最容易想到的解法:

  1. 创建含有100个元素的数组data[100],全部置零
  2. 生成100以内的随机数r
  3. 如果data[r]等于0,设置data[r]=1
  4. 如果data[r]等于1,重复第二步

此算法的时间复杂度为O(n^2),越到后面,碰撞的机会也越来越大,最坏的情况下,时间复杂度远不止O(n^2)。

解法二:Fisher-Yates shuffle洗牌算法

算法的逻辑为:

  1. 创建一个长度为n的数组(假设下标从1开始),每个元素的值都置为其下标
  2. 设max=n,从n开始到1逐步递减
  3. 生成[1, max]之间的随机数r,把data[r]和data[max]交换,max减一
  4. 重复第三步,直到max小于1

2.1 过程图解

假设要生生十个随机数,创建十个元素的数组,初时时的状态为:

+--+--+--+--+--+--+--+--+--+--+
| 1| 2| 3| 4| 5| 6| 7| 8| 9|10|
+--+--+--+--+--+--+--+--+--+--+
                             ^
                            max   

逐步生成随机数的过程:

max = 10, r = 3
        +--------------------+
        v                    v
+--+--+--+--+--+--+--+--+--+--+
| 1| 2|10| 4| 5| 6| 7| 8| 9| 3|
+--+--+--+--+--+--+--+--+--+--+

max = 9, r = 7
                    +-----+
                    v     v
+--+--+--+--+--+--+--+--+--+--+
| 1| 2|10| 4| 5| 6| 9| 8| 7: 3|
+--+--+--+--+--+--+--+--+--+--+

max = 8, r = 1
  +--------------------+
  v                    v
+--+--+--+--+--+--+--+--+--+--+
| 8| 2|10| 4| 5| 6| 9| 1: 7| 3|
+--+--+--+--+--+--+--+--+--+--+

max = 7, r = 5
              +-----+
              v     v
+--+--+--+--+--+--+--+--+--+--+
| 8| 2|10| 4| 9| 6| 5: 1| 7| 3|
+--+--+--+--+--+--+--+--+--+--+

...

2.2 代码实现

void shuffle(int *data, int n) {
    int i, r;
    for (i = 0; i < n; i++) {
        data[i] = i;
    }
    for (i = n; i > 0; i--) {
        r = rand() % n;
        swap(&data[r], &data[i - 1]); // 交换
    }
}

单测案例:

TEST(SHUFFLE_TEST, FEATURE_TEST) {
    int i, data[TEST_NODE_COUNT], verify[TEST_NODE_COUNT];
    shuffle(data, TEST_NODE_COUNT);
    memset(verify, 0, sizeof(int) * TEST_NODE_COUNT);
    for (i = 0; i < TEST_NODE_COUNT; i++) {
        verify[data[i]] ++;
    }
    for (i = 0; i < TEST_NODE_COUNT; i++) {
        ASSERT_EQ(verify[i], 1) << verify[i];
    }
}

int main(int argc, char **argv) {
    srand(time(NULL));

    testing::InitGoogleTest(&argc, argv);

    RUN_ALL_TESTS();
    return 0;
}

在visual studio中,使用extern "C"语句会导致后面的整个代码块都被缩进:

对强迫症患者来说这里看起来很不舒服,而且使用这个语句也只是为了使C兼容CPP,本身写的就是C而已,并不希望这里有缩进。

解决方案:

#ifndef __BST_TREE_H__
#define __BST_TREE_H__

#ifdef __cplusplus
extern "C" {
#endif
// 添加下面三行
#if 0
}
#endif

struct bst_tree_node_st {
};

#if 0
{
#endif
#ifdef __cplusplus
}
#endif

#endi

STL中的mapset默认时不支持存结构体的,如果要添加结构体的支持,必须手动重载<运算符。

原因:map和set底层都是通过红黑树实现的,红黑树搜索树的一种,插入数据时要比较大小,所以结构体必须重载小于号

示例:

#include <iostream>
#include <string>
#include <set>

using namespace std;

typedef struct stu_st {
    string name;
    int age;
}stu_t;

int main() {
    set<stu_t> stu_infos;

    stu_t a, b;
    a.name = "xiaoming";
    a.age = 20;

    b.name = "xiaohua";
    b.age = 21;

    stu_infos.insert(a);
    stu_infos.insert(b);

    cout << stu_infos.size() << endl;

    return 0;
}

以上代码在vs下编译报错:

问题很明确,没有重载<符号,添加上以下代码即可:

bool operator<(const stu_t& a, const stu_t& b) {
    return a.name < b.name;
}

一、申请方式

  • 栈是系统自动申请,自动释放。
  • 堆需要手动申请,手动释放。

二、增长方向

  • 栈是从高地址向地地址增长
  • 堆从地地址到高地址增长

三、存储位置

  • 栈的内存空间在用户空间的最顶端,3G以下
  • 堆位于全局静态区,在栈的下面

四、大小限制

  • 栈可分配的内存大小较小
  • 堆中可分配的内存较大

五、申请效率

  • 栈内存申请较快,不会产生碎片
  • 堆内存申请较慢,会产生碎片

  • 面向对象的原则是什么?
封装、继承和多态
  • C++的空类默认产生哪些类成员函数?
默认构造函数、析构函数、复制构造函数和赋值函数
  • 为什么拷贝构造函数只能传递引用
以传值方式调用函数时,会拷贝临时变量,此时又会调用拷贝构造函数来构造临时变量,从而出现无限循环。
  • 哪一种成员变量可以在所有该类的实例之间共享?
静态成员变量
  • C++中class和struct的区别
  • 如何在类中使用常量成员变量?
使用const修饰的成员变量,必须在初始化列表中初始化。
  • 把析构函数定义成virtual的意义在哪?
当析构函数被定义成virtual的以后,销毁父类对象时,会先执行子类的析构函数,销毁掉子类对象。
  • 为什么构造函数不能被定义成virtual的?
虚函数内部是通过虚函数表来实现的,在执行时能通过vptr指针指向正确的子类对象函数。而在创建对象时,必须要知道创建对象的准确类型,因此构造函数不能为虚。
  • 析构函数可以时内联函数吗?
可以

static关键字的作用:

  1. 修饰局部变量:使得该变量在函数运行完后不会被释放,一直存在于整个程序的运行周期。
  2. 限制函数或者变量的作用域:在某一模块内声明的static变量或者函数无法被其他模块使用,例如使用static修饰的全局变量其他模块不能使用,static修饰的函数其他模块也不能使用。
  3. 作为类成员函数或者变量:被static修饰过的成员变量或函数生存在整个程序周期中,所有的类共享同一个静态成员。使用前必须在类外部手动定义该变量,并且被static修饰过后无法访问类里面的this指针

一、基本排序算法

  1. 插入排序
  2. 选择排序
  3. 冒泡排序
  4. 梳排序

二、高级排序算法

  1. 快速排序
  2. 堆排序
  3. 计数排序
  4. 归并排序

三、各排序算法的比较

各排序算法总结

排序算法平均时间复杂度最好情况时间复杂度最坏情况时间复杂度空间复杂度是否稳定适用场景
插入排序O($n^2$)O(n),当序列已经有序时O($n^2$),序列逆序时。O(1)数组大部分有序
选择排序O($n^2$)O($n^2$)O($n^2$)O(1)数据量较小的情况
冒泡排序O($n^2$)O($n^2$)O($n^2$)O(1)数据量较小的情况
希尔排序O(n^(1.3~2)​)O(n^(1.3~2))O(n^(1.3~2))O(1)增量序列的选取会影响排序时间,希尔排序没有快速排序算法快,中等大小规模表现良好,对规模非常大的数据排序不是最优选择,但是比O($n^2$)复杂度的算法快。
快速排序O(nlogn)O(nlogn)哨兵选择为边界值时,O($n^2$)O(nlogn)不适合元素较小的数组排序
堆排序O(nlogn)O(nlogn)O(nlogn)O(n)需要大量的移动操作,且要额外的空间保存已排序数组
计数排序O(n)O(n)O(n)O(n*2)假设数组元素都在0-k的范围内,并且需要两个辅助数组,如果数据分布不均匀,出现一个特别大的数据会导致额外的空间增加。
归并排序O(nlogn)O(nlogn)O(nlogn)O(n)合并时需要额外的内存空间
基数排序O (nlog(r)m),其中r为所采取的基数,而m为堆数O (nlog(r)m)O (nlog(r)m)O(r*m)需要额外的m个队列的辅助空间

在微博上看到了一个很有意思的题目:

最开始想了半天都没有想到解法,总觉得这是个不可能完成的。后面看评论才知道解法原来很简单:

把十个盒子的球分别编号1-10,然后把每个编号的球都取出它编号的个数,例如1号球取一个,2号球取两个,依次类推。最后把所有的球放到称上称,比预计的少了几克那么几号球就是次品了。

总结

其实题目挺有玄机的,很多人可能忽略了装满了球的盒子这个题眼,大部分原因解不出来是因为没看到这里。

如果知道有很多球可以使用,相信很多人都能很快想到解法。归根结底还是审题不仔细,导致错误结论下得太早。