mituh's notes


  • 首页

  • 归档

邓俊辉<<数据结构>>-公开课-02-D2

发表于 2018-01-12
d2 有序向量: 二分查找

Vector::find(e, lo, hi);
Vector::search(e, lo, hi);

操作参数和接口语义类似。
---------------------------------------------------------------------------------
# 统一接口
>* ADT接口
======================== source code ==========================
// ADT接口
template <typename T>
Rank Vector<T>::search(T const& e, Rank lo, Rank hi) const {
std::srand(std::time(0));
return (std::rand() % 2) ?
binSearch(_elem, e, lo, hi) // 二分查找算法
: fibSearch(_elem, e, lo, hi); // fibonacci查找算法
}

template <typename T>
Rank Vector<T>::binSearch(T* elem, T const& e, Rank lo, Rank hi) const {
std::cout << "calling binSearch.... " << std::endl;
return lo;
}

template <typename T>
Rank Vector<T>::fibSearch(T* elem, T const& e, Rank lo, Rank hi) const {
std::cout << "calling fibSearch... " << std::endl;
return lo;
}
================================================================
可以设计不同的算法, 根据数据的性质进算法的选择。
---------------------------------------------------------------------------------
# 语义约定

约点语义的好处就是, 能够更好的和其他代码配合。
>* 优点:
维护自身 V.insert(1 + V.search(e), e)
即便是失败, 也给出插入新元素的位置。
若允许重复元素, 则每组也按照其插入的次序排序。

>* 约点:
有序V[lo, hi), 不大于e的最后元素秩
-00 < e < V[lo], 返回 lo - 1 (左侧哨兵)
V[hi] < e < +00, 返回 hi - 1 (末元素, 右哨兵左邻)
---------------------------------------------------------------------------------
# 版本A: 原理

>* 减而治之
待查找区间分成三部分
S[lo, mi) <= S[mi] <= S(mi, hi) // S[mi]轴点

>* 三种比较情况
e < x: 左
x < e: 右
e = x: 命中 // 多个解?

>* 二分(折半)策略
每经过至多两次比较, 或命中, 或将规模缩减一半
----------------------------------------------------------------------------------
# 版本A: 实现

>* 我的尝试
======= source code ======================================
// 二分查找: 版本A
/* my test code
template <typename T>
Rank Vector<T>::binSearch(T* elem, T const& e, Rank lo, Rank hi) const {
std::cout << "calling binSearch.... " << std::endl;
if (lo < hi) { // 区间不存在, 没找到e
Rank mi = (lo + hi) >> 1; // 中点秩
if (elem[mi] == e) return mi; // 命中, 递归基
if (e < elem[mi]) { // 在mid左侧
return (binSearch(elem, e, lo, mi));
} else { return binSearch(elem, e, mi + 1, hi);}
}
return -1;
}
*/
==========================================================

>* 给出的版本
==================== source code ========================
template <typename T>
Rank Vector<T>::binSearch(T* elem, T const& e, Rank lo, Rank hi) const {
while (lo < hi) { // 区间存在
Rank mi = (lo + hi) >> 1; // 取中点
if (e < elem[mi]) hi = mi; // 深入前半段查找
else if (elem[mi] < e) lo = mi + 1; // 深入后半段查找
else return mi; // 在mi命中
}
return -1; // 查找失败
}
=========================================================

>* 分析
1, 将递归变成while loop, 内部对hi, lo进行修改。
2, while循环通过hi, lo改变, 改变区间。
3, if...else 和 else...if什么差别?

>* 技巧
善用 "<" 比较
因为数据结构画图理解时候, 都是从左向右, 小于号能直观的看出在哪个区间, 不容易出错。
-----------------------------------------------------------------------------------------
# 版本A: 实例与复杂度

>* S.search(8, 0, 7): 2 + 1 + 2 = 5, S[4]命中
>* S.search(3, 0, 7): 1 + 1 + 2 = 4, S[1]失败

>* 线性递归 T(n) = T(n/2) + O(1) = O(logn), 大大由于顺序查找O(n)
递归跟踪: 轴点总取中点, 递归深度O(logn); 各个递归实例均 O(1)
--------------------------------------------------------------------------------------
# 版本A:查找长度

>* 更为精细的评估算法性能:
考察关键码的比较次数, 查找长度(search length)
>* 成功与失败的最好, 最坏, 平均角度评估
>* 这个算法 = O(1.5logn)
向左节点 + 1, 向右节点 +2
n = 8
最好, 29/7 = 4+
最差, 36/8 = 4.5 = 1.5log28

C++ Zero To One 0.004

发表于 2018-01-12
# 1, bool和int的隐式转换
[cppd - bool_int_convert](https://github.com/timtingwei/cppd/blob/master/tmp/bool_int_convert.cpp)

>* 问题起因
======================= source code =========================
template <typename T>
int Vector<T>::disordered() const {
int count = 0; // 计数器
for (int i = i; i < _size; i++)
count += (_elem[i-1] > _elem[i]); // 逆序对则计数+1, bool和int的隐式转换
return count;
}
=============================================================
数据结构学习中, 一次循环将布尔值作为右值递增左值计数器。

>* sizeof(bool) = 1

>* bool-> not bool
布尔值转赋值给非布尔值, 初始值false时0, true时1

>* not bool -> bool
非布尔值转换成布尔值, 初始值非0为true, 初始值为0和空指针为false;

其他都没什么问题, 值得注意的是, 引用和指针
=================== source code =============================
void f_to_bool() {
int i1 = 5, i2 = 0;
int* pi0 = 0; // 定义空指针
int* pi1 = &i1; // 指针指向i1
int& ri0 = i2; // 引用i2, i2的值为0
int& ri1 = i1; // 引用i1

bool bi = i1; // prints bi = 1
bool bpi0 = pi0; // prints bpi0 = 0
bool bpi1 = pi1; // bpi1 = 1
bool bri0 = ri0; // bri0 = 0
bool bri1 = ri1; // bri1 = 1
=============================================================
----------------------------------------------------------------------------------------------------
# 2, 不能返回函数内部初始化的指针
[cppd - return_ptr](https://github.com/timtingwei/cppd/blob/master/tmp/return_ptr.cpp)

>* 问题
数据结构学习中, Rank* deduplicate_lower();
我想返回函数内部声明并初始化的指针, 即保存索引的数组, 但结果却出错了。
>* 错误的方法
函数体内声明定义的指针,返回不了
===================== source code ===========================
int* f() {
int iarr[] = {};
iarr[0] = 2;
return iarr;
}
--------------------- ./a.out -----------------------------
warning: address of local variable ‘iarr’ returned [-Wreturn-local-addr]
============================================================
我理解一下,函数内部定义的局部变量会在退出函数体后回收, 即使返回后地址也找不到,更谈不上指向谁。

>* 可行的方法
将指针作为参数传入
================== source code =============================
int* f1(int* iarr) {
*iarr += 1;
return iarr;
}
===========================================================
另外一个疑问, 将指针作为参数, 可以在修改指针后返回, 但既然是指针, 还有什么返回的必要??
便于函数之间的调用, 还是需要的。

>* 另一种可行方法: malloc返回指针
================= source code ============================
// 强制返回指针, 分配内存却不释放
int* fun() {
int* i_ptr = (int*) malloc(sizeof(int*)); // malloc分配内存
for (int i = 0; i < sizeof(int*); i++)
*(i_ptr+i) = 0;
// free(i_ptr); // 释放指针
return i_ptr; // 函数返回指针
}
============================================================
---------------------------------------------------------------------------------------
# 3, malloc和free
[cppd - malloc_free](https://github.com/timtingwei/cppd/blob/master/tmp/malloc_free.cpp)

> * 概念
malloc:
void* malloc (size_t size);
在语句块中分配一个memeory, 返回一个在作用域中的指针
free:
void free(void* ptr);
重新分配作用域中的memeory, 给未来的分配使用

> * 需要include的库
#include <stdlib.h>
#include <stdio.h>

> * 案例
=================== source code ===========================
int main() {
int i, n;
char* buffer;

printf("how long do you want the string?");
scanf("%d", &i);

buffer = (char*) malloc(i+1);
if (buffer==NULL) exit(1);

for (n=0; n < i; n++)
buffer[n] = rand()%26 + 'a';
buffer[i] = '\0';

printf("Random string: %s\n", buffer);
free(buffer);
}
------------------ ./a.out -------------------------------
how long do you want the string?5
Random string: nwlrb
===========================================================
------------------------------------------------------------------------------------------
# 4, 未声明大小的数组
[cppd - array_size](https://github.com/timtingwei/cppd/blob/master/tmp/array_size.cpp)

> * 问题
=================== source code ===========================
int size = 5;
int iarr[size] = {};
------------------- ./a.out ----------------------------------
error: variable-sized object ‘iarr1’ may not be initialized
===========================================================

> * const
=========== source code ==========
const unsigned int size = 5;
int iarr1[size] = {};
==================================

> * define
========== source code ==============
#define ARRAY_SIZE 5;
// #define ARRAY_SIZE = 5; // ERROR
int iarr[ARRAY_SIZE] = {}
======================================
-------------------------------------------------------------------------------------------------
# 5, continue 和 break 区别
[cppd - continue_break](https://github.com/timtingwei/cppd/blob/master/tmp/continue_break.cpp)

> * 概念
continue: 跳过for, range-for, while or do-while, 循环体剩余部分
break: 闭合的for, range-for, while, do-while 循环或 switch选择终止

> * continue实例:
loop:
=================== source code ============================
// 跳过loop剩余部分
for (int i = 0; i < 5; i++) {
for (int j = 2; j <7; j++) {
if (j - i == 2) {
std::cout << "j-i = 2" << std::endl;
continue;}
// ...
std::cout << "after continue " << std::endl;
}
}
============================================================

> * break实例:
1, loop:
==================== source code ===========================
// 终止循环
for (int i = 0; i < 5; i++) {
for (int j = 2; j <7; j++) {
if (j - i == 2) {break;} // 终止内存循环
std::cout << "j - i = " << j - i << std::endl;
}
}
============================================================

2, switch
=================== source code ===========================
int i = 2;
switch (i) {
case 1: std::cout << "1\n";
case 2: std::cout << "2\n"; // 选择到这里
case 3: std::cout << "3\n";
case 4:
case 5: std::cout << "45 \n";
break; // 终止switch
case 6: std::cout << "6\n";
}
------------------- ./a.out ------------------------------
2
3
45
============================================================
-----------------------------------------------------------------------------------------------------
# if...else 和 if...else if的区别
[cppd - if_else_elseif](https://github.com/timtingwei/dsacpp/blob/master/src/02/vector_ordered.cpp)
>* 问题
==================== source code ==========================
while (lo < hi) { // 区间存在
Rank mi = (lo + hi) >> 1; // 取中点
if (e < elem[mi]) hi = mi; // 深入前半段查找
else if (elem[mi] < e) lo = mi + 1; // 深入后半段查找
else return mi; // 在mi命中
}
===========================================================
if, else...if 和 else是什么关系?
从结果上看, 二分查找, 在前半段找到至少比较1次, 后半段2次。

>* if...else
====
int i = 6;
if (i == 5) {std::cout << "i = 5" << std::endl;
} else {
if (i > 5) {std::cout << "i > 5" << std::endl;
} else { std::cout << "i < 5" << std::endl;}
}
--------------------- ./a.out ---------------------------
i > 5
===
>* if...else if...else if
===
int i = 4;
if (i == 5) std::cout << "i = 5" << std::endl;
else if (i < 5) std::cout << "i < 5" << std::endl;
else if (i > 5) std::cout << "i > 5" << std::endl;
-----
i < 5
===
>* if
===
int i = 6;
if (i == 6) std::cout << "i = 6" << std::endl;
if (i != 7) std::cout << "i != 7" << std::endl;
----------------- ./a.out --------------------------
i = 6
i != 7
====================================================
>* 各自的作用域辨析
==================== source code ==========================
bool foo = false, bar = true, baz = true;
if (foo) {
// <- this block is only executed if 'foo' is true
std::cout << "if(foo)\n";
} else if (bar) { // <- 'bar' is only checked if 'foo' is false
// <- this block only executed if 'foo' is false and 'bar' is true
std::cout << "else if(bar)\n";
} else {
// <- this block only executed if 'foo' and 'bar' are both false
std::cout << "else {}\n";
}
if (baz) { // <- no 'else', so previous 'ifs' don't matter
// <- this block only executed if 'baz' is true. foo/bar don't matter
std::cout << "if(baz)\n";
}
---------------------- ./a.out ----------------------------------
// else if(bar)
// if(baz)
==============================================================
我用的是Google cpplint的cpp书写方式, else要在两个反花括号之间。
这样更容易看到 if, else..if, else 是一个语句块, if又是单独的一个。
>* 总结
if(i < n), if...else(n < i), 这两个语句条件除了n = i互补。
如果有一个true, 则不会执行接下来的else;
如果都为false, else执行, 条件相当于(n==i)为true.

因此,对于二分查找来说,
前半段找到只执行一次if, 后半段找到执行 if 和 if else两次比较, 还节省一次判等的操作。

1, if...else if...else 可作三个并列关系的选择, 但运行时有先后顺序
2, if...else 多用 (cond)? expre1 : expre2; 代替
===== source code ======
int i = 4, count = 0;
(i == 4) ? count++
: i = 4;
========================
缺点是执行的语句只能为一个表达式且单行, 也不能return
3, if, if 语句多为两个condtions没有并列关系。

邓俊辉<<数据结构>>-公开课-02-D1

发表于 2018-01-11

觉得markdown格式的页面, 排版很不舒服。虽然有标题的概念, 但是标题字体大小不容易分清, 结构不是显而易见。尝试使用不带格式的记笔记,仍然保留markdown语法, 以便复制对比效果。
------------------------------ split line ---------------------------
实现源码[dsacpp - vector_ordered](https://github.com/timtingwei/dsacpp/blob/master/src/02/vector_ordered.cpp)

# 有序向量唯一化
无序: 比对
有序: 比较

## 有序性及其甄别

> * 有序/无序序列中, 任意/总有一对相邻元素顺序/逆序。

> * 相邻逆序对数目, 可以度量逆序程度。

> * 逆序程度实现

===================== source code =========================
// 逆序程度
/* my test code
template <typename T>
int Vector<T>::disordered() const {
int count = 0;
for (int i = 0; i < _size - 1; i++)
if (_elem[i] != _elem[i+1]) count++; // ERROR: 逆序:前个元素大于后一元素
return count;
}
*/

template <typename T>
int Vector<T>::disordered() const {
int count = 0; // 计数器
for (int i = i; i < _size; i++)
count += (_elem[i-1] > _elem[i]); // 逆序对则计数+1, bool和int的隐式转换
return count; // 向量有序当且仅当n = 0
} // 若只需要判断是否有序, 首次遇到逆序对之后, 就立即终止
============================================================

> * 若元素支持大小比较, 可将无序向量转换为有序向量。
> * 无序向量转换为有序向量后, 算法多可优化。

## 低效算法

> 1, 我自己的低效deduplicate()版本,
依赖一次性remove函数.
> * 尝试自己实现,

======================== sourece code =======================
// my deduplicate code
// 唯一化(低效)
int deduplicate_lower(int rm_arr[]);
// 唯一化所依赖的通过索引数组一次性remove函数
void remove(int rm_arr[], int n);

template <typename T>
int Vector<T>::deduplicate_lower(int rm_arr[]) {
// 删除有序向量重复的元素, rm_arr是保存删除对象索引数组, 返回被删除元素的数量
assert(!disordered()); // 当前为有序向量
Rank n = 0; // 数组当前插入位置
Rank r1 = 0, r2 = 1; // 创建两个索引值线性扫描
while (r2 < _size) {
if (_elem[r1] == _elem[r2]) { // r2指向的元素和r1对应元素重复
rm_arr[n++] = r2; // 索引数组中加入r2
// remove(r2);
} else { r1 = r2;}
r2++; // 递增r2
}
remove(rm_arr, n); // 一次性删除索引对应的元素
return n; // 返回删除元素数量
}


// 有序向量唯一化所依赖的通过索引数组一次性remove函数
template <typename T>
void Vector<T>::remove(int rm_arr[], int n) {
// 删除索引除外的索引对应元素保留, 从左向右扫描
int i_n = 0; // 指向rm_arr中的元素
int new_i = 0; // 保留索引
T* old_elem = _elem; // 备份一份当前元素
_elem = new T[_capacity = _capacity];
for (int i = 0; i < _size; i++) {
if (i != rm_arr[i_n]) { // 当前索引不在表中
_elem[new_i++] = old_elem[i]; // 当前索引对应元素赋值给当前元素的新位置
} else { // 当前索引在删除索引的表中
i_n++; // 指向下一个rm_arr中的元素
}
}
_size -= n;
delete [] old_elem;
}
=============================================================

> 2, 视频中的唯一化(低效版)
代码如下:
======================= source code ========================
// 有序向量唯一化
template <typename T>
int Vector<T>::uniquify() {
int old_size = _size; int i = 0; // 首元素开始
while (i < _size - 1) {
(_elem[i+1] == _elem[i]) ? remove(i+1) : i++;
} // _size的改变由remove隐式完成
return old_size - _size;
}
============================================================

## 低效算法: 复杂度
> 1, 低效复杂度
运行时间取决于while循环, 总计 _size - 1 = n - 1
最坏情况: 每次调用remove(), 耗时O(n)~ O(1), 累计O(n^2)
> 2, 比较
我实现了一次性remove的操作, 应该可以将O(n^2)的复杂度降到O(n)


## 高效算法:
> 1, 反思
低效来自于, 同一元素可作为删除元素的后继, 而多次参与前移。
> 2, 启示
将重复区间视为单位, 成批的删除
> 3, 算法
[i] [] [ . . . . duplicates] [j]
从i开始扫描, 直达找到不相等的元素j, 将j移动到i的后一位置。

高明之处在于: 实际上没有删除, 却等效于删除
================== source code ==============================
/* my test code
template <typename>
int Vector<T>::uniquify() {
int old_size = _size;
int i = 0, j = 1; // i指向首位置
int n = 1; // 实际更新后的_elem的索引
while (i < _size - 1)
if (_elem[i] != _elem[j])
{ _elem[n++] = _elem[j]; i = j; size--;
} else {j++;}
return old_size - n;
}
*/

template <typename T>
int Vector<T>::uniquify_faster() {
int i = 0, j = 0; // 两个计数都指向首元素
while (++j < _size) { // j扫描到末尾
if (_elem[i] != _elem[j]) {_elem[++i] = _elem[j];} // j指向元素移到i后
}
_size = ++i; shrunk(); // 改变_size数值后, 缩容
return j - i; // 注意j扫到尾端的特性
} // 依赖shrink();
============================================================

/* 实现问题分析
// 为什么不要另外的n作为索引计数?
当j移动至i后, 计数i也增加, 重新指向移动位置后的j, 不需要对原有那份的拷贝即可完成索引操作。记录位置只用i。算法设计时,也考虑一个操作是否必要。一份复制是否必要。
// 为什么不在uniquify中进行new 和 delete?
接口分离的体现, 在函数中改变_size, 通过_size的改变, 再由shrunk/expand对空间进行操作。
*/

## 高效算法: 实例与复杂度
> 1, *反思 (典型例子)
共计n-1次迭代, 每次常数时间, 累计O(n)时间。

优化之处在于, 没有亦步亦趋的移动, 而是直接移动到最终的位置;又只通过计算得到_size规模, 没有任何显式的删除操作。
体现了, 一个差的算法, 在经过反思之后, 拟定的改进策略, 最终使得效率有所改进。

C++ Zero To One 0.003

发表于 2018-01-11

重载函数什么时候用virtual?

源码: source code - load_funtion.cpp

问题来源

template <typename VST> void traverse(VST visit);
// template <typename VST> virtual void traverse(VST visit, T* e);
template <typename VST> void traverse(VST visit, T* e);

为什么这里不需要virtual??

// 单个T类型元素加1的类
template <typename T>
struct Increase {
virtual void operator()(T &e) {e++;} // Increase<T>()实现函数功能
};

为什么这里又需要virtual??

概念偏差

virtual 和我原先理解的重载函数不太相同。也就是vitual和重载函数不是同一个概念。

vitual为虚函数:基类的引用或者指针调用一个虚成员函数时会执行动态绑定。

重载函数:同一作用域内的几个函数名字相同但形参列表不同。

使用与不使用virtual

解释一下 virtual 和 override 搭配, 继承类中可对基类的虚函数重载

struct Base {    // 声明virtual后, 可被继承类override重载
virtual void print() {std::cout << "Base\n";}
};

struct Derived : Base { // 'override'可写可不写
void print() override {std::cout << "Derived\n";}
};

struct Base {
void print() {std::cout << "Base\n";}
};

struct Derived : Base {
void print() {std::cout << "Derived\n";}
};

关键问题: 上述两者什么差别????

运行测试一下:

// 注释是使用virtual结果
Base b;
Derived d;

Base& br = b; // refer b, br的类型是Base&
Base& dr = d; // dr类型也是Base&
br.print(); // Base
dr.print(); // Derived

Base* pb = &b; // pointer to b, Base*
Base* pd = &d; // Base* as well

pb->print(); // Base
pd->print(); // Derived

b.Base::print(); // Base
d.Base::print(); // Base

使用vitual的运行结果:

Base
Derived
Base
Derived
Base
Base

不使用虚方法的运行结果:

Base
Base
Base
Base
Base
Base

virtual版本, 基类的引用或者指针会在调用一个虚函数时候, 动态绑定对应的函数。

解决最初问题

为什么traverse函数不需要virtual

traverse函数,属于在同一作用域,形参列表不同的函数。重载的概念是函数重载的概念,并不是继承类之间的虚函数override概念。

为什么函数对象需要virtual重载操作符函数?

定义继承类Increase_two, override在基类中被声明成virtual的类, 实现元素加2

// 函数对象用virtual声明operator()()
struct Increase {
// 重载()后, 对象可充当函数的功能
virtual void operator()(int* e) {(*e)++;}
};

struct Increase_two : Increase {
void operator()(int* e) override {(*e)+= 2;} // 元素+2;
};

测试函数, 以及prints结果如下:

void f_increase() {
Increase a;
Increase_two b;

Increase& ra = a;
Increase& rb = b; // rb的类型为 Increase&

int e = 5; int* pe = &e;
ra.operator()(pe);
std::cout << "e = " << e << std::endl; // prints "e = 6"
int e2 = 5; int* pe2 = &e2;
rb.operator()(pe2); // 调用基类的引用时, 动态绑定到Increase_two中的方法
std::cout << "e2 = " << e2 << std::endl; // prints "e2 = 7"
}

对重载操作符声明virtual, 可以让继承类对他的重载操作符行为进行重载。当调用基类的引用或者指针时候, 在编译阶段就能绑定到继承类对应的重载函数版本。

静态成员声明定义使用

源码: source code - static.cpp

静态成员的特点

静态数据成员不属于任何类的一个对象。

类似于全局变量, 存在于任何的函数定义之外, 一直存在于整个程序的生命周期

类内static成员如何声明定义?

  • 在类内部声明, 外部通过类名作用域访问定义
    {
    class MyStatic
    public:
    static int i_stc; // 类内部声明
    // ...
    }

    int MyStatic::i_stc = 0; // 类外部定义, 注意需要声明成员的类型

也可以通过静态成员函数和一般的成员函数对它进行修改, 此时静态成员函数可通过类名访问。

在不同实例中被多次调用改变后的情况?

void MyStatic::increment() {++i_stc;}

void f() {
MyStatic c0;
c0.increment();
std::cout << "c0 = " << c0.i_stc << std::endl;
MyStatic c1;
c1.increment();
std::cout << "c1 = " << c1.i_stc << std::endl;
}

void f1() {
MyStatic c2;
c2.increment();
std::cout << "c2 = " << c2.i_stc << std::endl;
}
c0 = 1
c1 = 2
c2 = 3

静态数据成员类似于全局变量, 无论在何处, 每调用一次修改静态成员的函数, 数据相应的改变, 且不会随着作用域的离开而销毁。

建筑设计应用和计算机科学一点随想

发表于 2018-01-10

要掌握一门技术或者一种思想, 离不开实践和思考。即使从最简单的问题入手, 也可以检验自己对知识是否融会贯通。这是计算机科学的一种特性吧。在认识到这一点之后, 学习应该更注重知识与知识之间的联系, 这一点我本身的思维特点决定了它不差; 另一点便是, 注重知识的应用, 我容易陷入这个陷阱, 以致于现在抛开应用, 离开建筑, 去学习思考这些问题。

批判性思维指引我, 我的做法和别人的作法有什么区别, 别人可以是书籍, 视频, 演讲还有编译器。得益于这一点, 我最近找到了一些学习语言的门道。但是如果要说到应用,可能需要批判性思考的嵌套, 也就是指向批判性思维的指针。这个技巧在实际应用中哪些地方会用到?如何运用?这个DSA和建筑问题是否相关?这个库的调用或者学习对建筑的生成有多少帮助?提这些问题, 并不是说对计算机科学的深入学习点到为止, 而是让我循序渐进的学习。它能告诉我这一段知识的学习, 内存泄漏的厉害; 这一段知识的学习, 太追求结果和封装; 这一段知识的学习, 缺少自我和外我的交流。

学习角度讲, 即使从一个很小很简单的问题入手, 也能对自己有所提高。 因为, 计算机科学没有简单, 复杂是由简单构成的, 计算机科学承认复杂, 计算机科学尝试破译复杂。而建筑恰恰就是一个复杂黑箱。事实也证明, 往往穿透的, 创新的思想, 在学科的结合之间会泵发出生命力。我该反思。

C++ Zero To One 0.002

发表于 2018-01-10

数组, 指针数组, 指向指针的指针的数组, ….

在逛Cplusplus Forum - Beginner的时候发现了一个看上去看简单的问题

How do I receive two numbers from the keyboard, save them in an array of two element?

第一次尝试

void f0() {     // ERROR: 先存放再输入不会修改数组中的数据
int a, b;
int iarr[2] = {a, b};
std::cin >> a >> b; // 不合理:先存放, 再输入
std::cout << iarr[0] << ' ' << iarr[1] << std::endl;
// 4197661 0
}

我想得太简单了。不能在标准输入中, 直接对数据进行修改。

下面给出的答案

1,

void f1() {
int iarr[2] = {};
std::cin >> iarr[0] >> iarr[1]; // 直接输入到数组中
}

2,

void f2() {
int a, b;
std::cin >> a >> b; // 先输入, 再存放
int iarr[2] = {a, b};
}

但是我仍然考虑我思路实现的可能性?

对已经放置在数组中的值进行修改的思路如何实现?

第一个想到的就是指针。

void f3() {  // BUG: 为什么不能通过数组中保存的指针修改数组??
int *pa, *pb;
int* iarr[2] = {pa, pb};
int tmp_a, tmp_b;
std::cin >> tmp_a >> tmp_b; // 存放到临时变量
pa = &tmp_a; pb = &tmp_b;
std::cout << "*pa = " << *pa << std::endl; // *pa = 1
std::cout << *(*iarr) << std::endl;
std::cout << *(iarr[0]) << ' ' << *(iarr[1]) << std::endl;
}
$ ./a.out
1
2
*pa = 1
Segmentation fault (core dumped)

很气, 我得出结论。不能通过数组中保存的指针修改数组。

数组作为函数形参

接着, 我想起之前在函数中通过数组改变过值, 便想要实现如下任务:
创建一个函数, 输入一个保存指向整数的指针的数组, 修改索引n位置的指针所指向的值, 返回

void revise_value_int(int* arr, int n) {
// 索引为n指向的整数递增1
(*(arr+n)) += 1;
}

void call_revise() {
int iarr[] = {0, 1, 2};
revise_value_int(iarr, 2);
std::cout << "iarr[2] = " << iarr[2] << std::endl;
}

call_revise();

可以改变。但是仔细一想, 我仅仅只是在数组中存放了整数。

函数形参为存放指针的数组

接着,我改变了数组,用于存放指针

int* pa, *pb, *pc;
int ptrArr[] = {pa, pb, pc};

编译时就出现了错误,.error: invalid conversion from ‘int’ to ‘int’ [-fpermissive]
因为现在数组保存着的是指向整型的指针, 因此数组的类型应该是 int\

修改

int* pa, *pb, *pc;
int* ptrArr[] = {pa, pb, pc};

写修改指针所指向整数的函数, 注意, 这个形参类型是int**, ptrArr是指向指针的指针, 调用数组, 实际上是调用指向数组首元素的指针。

void revise_value_ptr(int** ptrArr, int n) {
// 索引为n的指向的指针所指向的整数递增1
(**(ptrArr+n))+=1;
}

Segmentation fault (core dumped)

再次编译

Segmentation fault (core dumped)

出现 Segmentation fault (core dumped) 的原因, 参考[http://blog.sina.com.cn/s/blog_4b9eab320100uivj.html]

1 内存访问越界

a) 由于使用错误的下标,导致数组访问越界

b) 搜索字符串时,依靠字符串结束符来判断字符串是否结束,但是字符串没有正常的使用结束符

c) 使用strcpy, strcat, sprintf, strcmp, strcasecmp等字符串操作函数,将目标字符串读/写爆。应该使用strncpy, strlcpy, strncat, strlcat, snprintf, strncmp, strncasecmp等函数防止读写越界。

2 多线程程序使用了线程不安全的函数。

3 多线程读写的数据未加锁保护。

对于会被多个线程同时访问的全局数据,应该注意加锁保护,否则很容易造成core dump

非法指针

a) 使用空指针
b) 随意使用指针转换。一个指向一段内存的指针,除非确定这段内存原先就分配为某种结构或类型,或者这种结构或类型的数组,否则不要将它转换为这种结构或类型的指针,而应该将这段内存拷贝到一个这种结构或类型中,再访问这个结构或类型。这是因为如果这段内存的开始地址不是按照这种结构或类型对齐的,那么访问它时就很容易因为bus error而core dump.

堆栈溢出

不要使用大的局部变量(因为局部变量都分配在栈上),这样容易造成堆栈溢出,破坏系统的栈和堆结构,导致出现莫名其妙的错误。

非法指针

我的代码是:

int* pa, *pb, *pc;
int* ptrArr[] = {pa, pb, pc};

这里我使用了非法的指针, 这个指针没有指向任何

修改,

void call_revise() {
int a = 1, b = 2, c = 3;
int* pa = &a, *pb = &b, *pc = &c;
int* ptrArr[] = {pa, pb, pc};
another_revise_value_ptr(ptrArr, 2);
std::cout << "**(ptrArr) = " << **(ptrArr) << '\n'
<< "**(ptrArr+1) = " << **(ptrArr+1) << '\n'
<< "**(ptrArr+2) = " << **(ptrArr+2) << std::endl;
}

call_revise()

编译得到

**(ptrArr)   = 1
**(ptrArr+1) = 2
**(ptrArr+2) = 4

解决最开始问题并总结

修改后的代码

void f4() {     // DEBUG: 先存放再输入不会修改数组中的数据
int a, b;
int* pa = &a, int* pb = &b; // 注意: 不要定义空指针
int* iarr[2] = {pa, pb}; // 注意数组的类型是数组中元素的类型
std::cin >> a >> b; // 也能合理:先存放, 再输入
std::cout << **iarr << ' ' << **(iarr+1) << std::endl; // 解引用
// 4197661 0
}

不要定义空指针

如这样

int* pa;

即使要定义也要

int *pa = 0;
int *pb = nullptr;

尽量让指针指向某个对象

int *pa = &a;

数组的类型

数组的类型是数组中元素的类型

int* iarr[2] = {pa, pb};        // 类型为 int*

解引用数组

int* iarr[2] = {pa, pb};        // 如果数组这样定义
iarr; // iarr是指向数组首个元素的指针
*iarr; // 因为pa是指针, 所以解引用数组头指针, 得到pa地址
**iarr; // 再次解引用得到pa指向的int

标准输出它们得到

iarr = 0x7ffed5caa560
*iarr = 0x7ffed5caa548
**iarr = 1

更多的尝试

数组中存放对象

template <typename T>
class Name {
char* _c; int _length; int _capacity;
public:
Name(char* c, int length): _c(c), _length(length) {}
char* getName() const;
int getLength() const;
};

template <typename T>
char* Name<T>::getName() const {
return _c;
}

template <typename T>
int Name<T>::getLength() const {
return _length;
}



void class_array() {
char c1[] = {'t', 'i', 'm'};
char c2[] = {'h', 'u'};
int length1 = 3, length2 = 2;
Name<int> name1(c1, length1), name2(c2, length2);
Name<int> nArr[] = {name1, name2};
int size = 2;
print_instance_data_in_array(nArr, size);
}

void f() {
char c[] = {'t', 'i', 'm'};
int length = 3;
Name<int> n(c, length);
}

void print_instance_data_in_array(Name<int>* nArr, int size) {
for (int i = 0; i < size; i++) {
std::cout << "Name = ";
for (int j = 0; j < nArr[i].getLength(); j++) {
std::cout << *((nArr[i].getName())+j);
}
std::cout << "\n";
}
}
Name = tim
Name = hu

面向对象可以让我从无尽的指针中抽身出来, 关注相互之间的联系。

数组中存放长度不同的数组

void f_array() {
int a = 3, b = 5, c = 7;
int* pa = &a, *pb = &b, *pc = &c;
int _size_pa = 1, _size_pb = 1, _size_pc = 1;
int* pArr1[] = {pa}; // pArr1数组存放pa, pb指针
int _size_pArr1 = 1;
int* pArr2[] = {pa, pb, pc}; // pArr2数组存放pa, pb, pc指针
int _size_pArr2 = 3;
int* pArr3[] = {pa, pc};
int _size_pArr3 = 2;
int size_pArr[] = {1, 3, 2};
int** ppArr[] = {pArr1, pArr2, pArr3}; // ppArr存放3个指针数组
int _size_ppArr = 3;
int size_ppArr[] = {3};
int* size_arr[] = {size_ppArr, size_pArr};
// convert_size_structor(size_arr);
print_arr_b(ppArr);
}

void print_arr_b(int*** ppArr) {
// 解引用并打印ppArr数组中依次数组中的pa, pb, pc
std::cout << "a0 = " << ***ppArr<< " in pArr1[0]" << '\n'
<< "b0 = " << **(*(ppArr+1)+1)<< " in pArr2[1]" << '\n'
<< "c0 = " << **(*(ppArr+2)+1)<< " in pArr3[1]" << '\n'
<< std::endl;
}

对于简单的数组, 打印储存不同长度数组的数组, 需要更多的数据结构知识, 而且不同的算法还有好坏, 可见数据结构的重要性. 这部分的打印实现, 留作日后我数据结构解引用后的小试牛刀。

感想

即使从一个很小很简单的问题入手, 也能对自己有所提高, 计算机科学没有简单, 复杂是由简单构成的, 计算机科学承认复杂, 计算机科学尝试破译复杂。

常量成员函数

class MyClass {
int x = 0;
public:
int c = 5;
MyClass(int v) :x(v) {}
const int& get() const {return x;} // 常量成员函数在是否为常量上可以重载
int& get() {return --x;}
};

声明时引用符号总最靠近变量名

为什么可以对a.get()赋值

get()返回的是引用, 引用可作为左值, 且如果非常量引用的话, 该值可以被修改。

在是否为成员函数上重载后如何匹配?

成员函数在是否为常量函数上可以重载。如果对象实例化成const类型, 则匹配常量成员函数。

const int& get() const {return x;} 为什么返回类型需要const?

常量函数返回数据成员, 编译器会将数据成员声明成const类型;

什么是数据成员?

MyClass类public中定义新成员测试是否为数据成员。

//..
int a = 5
const int& get() const {return --a;} // 错误:改变a的值,

改变a的值无法通过编译, a为数据成员。

在class中声明的, 无论是私有的还是公有的数据, 都是成员数据。如果在常量函数中返回, 会常量函数会被声明成const类型。

template基础用法

typename T 和 class T的区别

在模板参数列表中, typename和class没有什么不同。 但每个参数类型前必须加上typename 或者 class

<<C++ Primer 5th>> P580 模板与泛型编程

特殊化模板

template <typename T>
class mycontainer {
T element;
public:
mycontainer(const T e) : element(e) {}
T increase() {return ++element;} // 递增element
T getElement() const {return element;} // 返回element数据
};

对char类型, 有特定的方法uppercase()大写化字母

template <>
class mycontainer<char> {
char element;
public:
mycontainer(const char e): element(e) {}
char increase() {return ++element;} // 递增char
char getElement() const {return element;} // 返回element数据
char uppercase() {
if (element <= 'z' && element >= 'a') { // 是小写字母
element += 'A' - 'a'; return element;
} // 返回元素
std::cout << "element " << element << "is not lower" << std::endl;
return element;
}
};

格式如下

template <typename T> class mycontainer {...}
template <> class mycontainer<char> {...}

C++ Zero To One 0.001

发表于 2018-01-10

接下来, 准备archive每天实际编程中遇到的C++问题,解决问题的过程, 以及得到的结果。以此来熟悉C++的各种特性。

这样做或许会造成知识点离散, 因此, 后续有必要有选择的,对某些特性进行深入剖析。先破破的来吧,以致于我不会从一个细节陷阱中出来,又陷入另一个。

右移,左移

_capacity >>= 1;     // _capacity *= 0.5;
_capacity <<= 1; // _capacity *= 2;

new delete

要先new一个新的, 才能delete旧的

template <typename T>
void Vector<T>::shrunk() {
if (_size > (_capacity/2)) return; // 规模大于1/2不必缩容
_capacity = std::max(_capacity, DEAFAULT_CAPACITY);
// 储存一份旧元素, 创建新的数据空间
T* old_elem = _elem; _elem = new T[_capacity >>= 1];
for (Rank r = 0; r < _size; r++) {
_elem[r] = old_elem[r];
}
// 删除旧元素的内存空间
delete [] old_elem;
}

new分配空间

void copyFrom(T* tarr, int lo, int hi) {
_elem = new T[_capacity = 2*(hi - lo)];
_size = hi - lo;
std::cout << "*tarr = " << tarr[1] << std::endl;
std::cout << "*_elem = " << *_elem << std::endl;
// for (int i = 0; i < _size; i++) {*(_elem+i) = tarr[i];}
}

void copyFrom(T* tarr, int lo, int hi) {

_size = hi - lo;
std::cout << "*tarr = " << tarr[1] << std::endl;
std::cout << "*_elem = " << *_elem << std::endl;
// for (int i = 0; i < _size; i++) {*(_elem+i) = tarr[i];}
}

为什么一定要为_elem分配空间? 内存回收问题。 在C++ Primer中具体学习

同一if语句, 同一行中的后置递增

void f() {
std::cout << "---- test in same if statement----" << std::endl;
int i = 1;
int arr[] = {0, 1, 2, 3, 4};
// 在同if, for, while中, 对某一变量i后置递增, i前后不变
if (equal(i++, arr[i])) std::cout << "equal\n"
<< "now i = " << i << std::endl;
}

同一if, for, while语句中, 后置递增返回当前值
同一行, 不同语句, 当然会让值产生变化

重载后置++操作符号

前置递增 operator++()
后置递增, 以前置递增为基础 operator++(int i)
increment_and_decrement_operators.cpp

递增运算符重载的返回类型:
为什么返回*_elem 而不是 *this??
递减运算符重载函数的返回类型是int&
*this的类型为Vector, 而*_elem 的类型是int

template <typename T>
T& Vector<T>::operator--() { // 重载前置--操作符
for (int i = 0; i < _size; i++)
_elem[i]--; // 对每个元素-1
return *_elem; // 返回当前 *this or *_elem
}

template <typename T>
T* Vector<T>::operator--(int i) { // 重载后置--操作符
T* e = _elem;
--*this; // 调用前置递减
return e;
}

<<C++ Primer 5th>> P502, P421

函数指针 和 函数对象

对无序向量的遍历操作中, 统一对各个元素分别实施visit操作

函数指针, 只读或者局部性修改

template <typename T>
void Vector<T>::traverse(void (*visit)(T&)) // 函数指针
{for (int i = 0; i < _size; i++) visit(_elem[i]);}

函数对象, 可全局性修改

template <typename T> <template VST>
void Vector<T>::traverse(VST & visit) { // 函数对象
for (int i = 0; i < _size; i++) visit(_elem[i];)
}

函数指针

int getLarger(const int& i1, const int& i2) {
return i1 < i2 ? i2: i1; // 返回两者较大
}

声明定义

声明和定义分离:

int (*pf1)(const int&, const int&);

pf1 = getLarget;
pf1 = &getLarget; // 等价,

个人认为&表达出pf1是个指针的语义明确点。

声明并定义:

int (*pf1)(const int&, const int&) = &getLarger;

使用

使用

int i_pf1 = (*pf1)(3, 5);
int i_pf1_2 = (pf1)(3, 5); // 等价
int i_pf1_3 = getLarget(3, 5); // 等价 调用原函数

赋值, 指向新的函数?

bool compareInt(const int& i1, const int& i2) {
return i1 < i2 ? true : false; // i1 < i2, 返回true
}

// 声明并定义
bool (*pf)(const int&, const int&) = &compareInt;

// 赋nullptr:指针没有指向任何一个函数
pf = 0;
pf = getLarger; // error: pf已经被声明成bool(*)(const int&, const int&)
pf = increment; // error: 同上

能重新指向一个nullptr或0, 但是不能指向一个与声明类型不同的新函数地址。

但如果是一个类型相同的函数, 便可以重新指向该函数。

bool otherCompare(const int& i1, const int&) {return 0;}

pf = otherCompare;

重载函数的指针

函数指针 指向重载函数, 需要在声明时, 形参列表和返回类型需要完全匹配

void ff(int*);
void ff(unsigned int);

// 重载
void (*pff1)(int *) = &ff; // pff1指向ff的 void (int*)版本

void (*pff2)(int) = &ff; // Error
int (*pff3)(unsigned) = &ff; // Error

函数指针作为形参(调用函数指针)

// 第三个参数是函数指针类型
void useBigger(const int&, const int&, bool (*pf)(const int&, const int&));

// 调用时, 传入指向compareInt的指针
useBigger(i1, i2, &compareInt);

可以用别名的方法简化定义

typedef decltype(compareInt) *FuncP2;    // FuncP2是指向函数的指针类型
typedef decltype(compareInt) Func; // decltype()返回函数类型, Func是函数类型

void useBigger(const int&, const int&, FuncP2);
void useBigger(const int&, const int&, Func2); // 编译器将Func2函数类型自动转换为了函数的指针类型

返回指向函数的指针

一般, 别名

using PF = bool(*)(const int&, const int&);      // PF是函数的指针类型
using F = bool(const int&, const int&); // F是指针类型

PF f1(int); // 返回函数的指针类型
F *f1(int); // 显式f1返回的是一个指针类型
F f2(int); // Error:F是函数类型, 无法返回一个函数

尾置

auto f1(int) -> int(*)(int*, int)

函数对象

重载()

说明这个对象, 他可以当作函数来使用。因此需要重载”()”

// 定义一个函数对象
class Sum{
public:
int sum = 0;
void operator()(int iarr[], int n) {
for (int i = 0; i < n; i++)
sum += iarr[i];
std::cout << "sum = " << sum << std::endl;
}
};

使用这个函数对象

Sum s;
int iarr[] = {1, 2, 3, 4, 5, 6};
int n = 6;
s(iarr, n); // sum = 21

作为其他函数的参数

// 函数对象作为另一个函数的形参
template <typename CLS>
void f(CLS & c) {
int iarr[] = {3, 6, 9};
int n = 3;
c(iarr, n);
}


// 调用
Sum s1;
f(s1); // sum = 18

回顾一下数据结构中的用法

template <typename T> template <typename VST>
void Vector<T>::traverse(VST visit) {
// 对每个元素执行visit操作
for (int i = 0; i < _size; i++) {visit(_elem[i]);}
}

// 为什么不能用引用类型?
// no known conversion for argument 1 from ‘Increase<int>’ to ‘Increase<int>&’
/* my error test
template <typename T>
void increase(Vector<T> V) {
V.traverse(Increase(T& e));
}
*/
template <typename T>
void increase(Vector<T> V) {
V.traverse(Increase<T>());
}

// 为什么不用实例化, 带入参数便可直接调用??

邓俊辉<<数据结构>>-公开课-02-C

发表于 2018-01-10

无序向量

template <typename T> class Vector {};    // template定义方式

模板和模板, 模板和类之间可以互相组合。意味着数据结构之间也可以互相组合

template <typename T> class Vector {
};
class BinTree {
};
template <typename T> class Tree {
};

int main() {
// ..
Vector<int> myVector; // Right

Vector<float> myfVector;

Vector<BinTree> binForest; // Combine with other class;
Vector<Tree<int>> binForest; // Combine with template;
return 0;
}

无序向量: 没有顺序, 甚至不可能排成顺序。

元素访问(寻秩访问)

v.get(r), v.put(e)

A[r]

重载下标运算符”[]”

// 寻秩访问
/* // my test
template <typename T>
T& Vector<T>::operator[](std::size_t n) { // 这个类 Vector<T>
assert(n < _size);
return _elem[n];
}
*/

template <typename T>
T& Vector<T>::operator[](Rank r) const { // 不改变数据成员, 定义成常量成员函数
// 在vector内部, 定义了秩的类型, 统一用Rank
assert(r < _size); // 对下标秩进行溢出检测
return _elem[r];
}

左值, 右值, 引用??
引用类型可作为左值。

寻秩访问

代码健壮性简化

  • assert 断言,
    #include <cassert>
    assert(r < _size);

插入

// 插入
/* my test
template <typename T>
void Vector<T>::insert(const Rank r, const int value) {
// 检查移动后是否需要扩容
if (++_size > _capacity) expand();
// 将秩为r后的所有元素后移一位
for (Rank i = _size-2; i >= r; i--) { // 为了不覆盖数据, 从尾部开始移动
_elem[i+1] = _elem[i]; // 向后移动一位
}
// 在r秩位置上填入要插入的值
_elem[r] = value;
}
*/
template <typename T>
void Vector<T>::insert(const Rank r, T const &e) {
// value不应该是某一中特点的类型, 而应该利用template的特性
assert(0<= r && r < _size);
expand(); // 若有必要扩容 结合expand()中, _size < _capacity的定义
for (int i = _size; i > r; i--) // 习惯把改变后的值的索引设置成i
_elem[i] = _elem[i-1]; // 后继元素顺次后移一个单元
_elem[r] = e; _size++;
}

Template中泛型T的作用,
模板类中函数的互相搭配,
插入元素对vector操作的顺序
对_capacity和_size的影响。

删除算法

自前向后的迁移操作
缩容

// 删除操作
/* my test code
template <typename T>
void Vector<T>::del(const Rank lo, const Rank hi) {
for (Rank i = lo; i < _size; i++) {
// 清空区间元素
if (i < hi) { _elem[i] = 0;
} else {
// 将元素整体前移
_elem[i - (hi-lo)] = _elem[i];
// 前移后元素清空
_elem[i] = 0;
}
}
// 缩短规模和空间容量
_size -= hi-lo; _capacity -= hi-lo;
}
*/


template <typename T>
int Vector<T>::del(Rank lo, Rank hi) {
// 处理退化情况
if (lo == hi) return 0;
const int length = hi - lo;
// 自前向后的迁移操作
while (lo < _size) {
if (hi < _capacity) {_elem[lo++] = _elem[hi++];
} else {_elem[lo++] = 0;} // 处理hi++超出_capacityg容量的情况
}
// 更新规模或者缩容
_size -= length;
shrunk();
// 返回被删除元素的数目
return hi-lo;
}

1, 规模仍旧不变? 删除一段区间, 这里可以不改变规模, 相当于后面留空? 改进成改变size的版本用于shrunk
2 , _elem[hi++]能够被一直索引到? 超过_capacity时, 返回未定义的值
3, _elem[hi++]为什么不清空?把_capacity的剩余空间对应元素赋值给它的方法清空
4, 看出移动操作过程中, 变量的同步性
5, 缩容不光光是改变_capacity的值, 仍旧要释放空间

查找

无序向量: T为可判等, 重载 “==”或者”!=”
有序向量: T为可比较,重载 “<” 或 “>”

// 查找
/* my test code
template <typename T>
int Vector<T>::find(Rank lo, Rank hi, T const &e) const {
// 查找e在区间[lo,hi)内
// 从右往左查找
while (hi >= lo) {
if (_elem[hi] == e) {return hi;
}
hi--;
}
// 没有在while循环中返回, 不存在匹配元素
return -1;
}
*/

template <typename T>
Rank Vector<T>::find(Rank lo, Rank hi, T const &e) const {
// O(hi - lo) = O(n), 在命中多个元素时可返回秩最大者
while (lo < hi-- && e != _elem[hi]) {} // 逆向查找
return hi; // hi < lo失败; 否则hi即命中元素的秩
}

利用while本身的条件语句;后置递增的特性
返回hi? 将判断是否成功, 交给上层的调用者;以及成功后被上层算法进一步利用

最好情况 O(1), 最坏情况O(n)
输入敏感(input-sensitive): 最好情况和最坏情况相差悬殊的算法。

删除单元素

视为区间操作的特例

// 删除单个元素
/* my test code
template <typename T>
void Vector<T>::remove(Rank r) {
// 单元素的删除操作, 视为区间操作的特例 [r, r+1)
remove(r, r+1);
}
*/
template <typename T> // 删除向量中秩为r的元素, 0 <= r < size
T& Vector<T>::remove(Rank r) { // O(n-r)
T& old_t = _elem[r]; // 备份被删除的元素
remove(r, r+1); // 调用区间删除算法
return old_t; // 返回被删除元素
}

颠倒考虑

复杂度分析:
每次循环的耗时正比于删除区间的后缀长度 O(n-hi)
循环的次数等于区间宽度 O(hi-lo)
总体 O(n^2) 复杂度

right, but not fast!!

唯一化

实现

// 唯一化
/*
template <typename T>
void Vector<T>::deduplicate(Rank lo, Rank hi) {
// 对向量中的元素遍历,
for (Rank i = lo; i < hi; i++) {
// find右往左查, 返回lo-1代表失败
// 删除当前元素, 不再对后续元素查找
if (find(lo, i, _elem[i]) != lo-1) {remove(i); break;}
if (find(i+1, hi, _elem[i]) != i) { remove(i);}
}
}
*/

template <typename T>
int Vector<T>::deduplicate() {
int old_size = _size;
Rank i = 1;
while (i < _size) {
find(0, i, _elem[i]) < 0 ?
i++ // 小于0说明无雷同, 继续查找
: remove(i); // 删除雷同者(至多一个?!)
}
return old_size - _size; // 返回规模的变化量
}

记录规模? 考虑返回值怎么设计,这里我估计会在更高级的接口中用到
从1开始? 因为要在当前i的前缀中查找
至多一个? 删除至多一个, 实际不一定至多一个
为什么不需要改变_size和_capacity大小? 说明remove为更底层的操作, 在底层操作中, 定义底层的数据成员的修改, 这样做的话, 在更高级的操作中, 可以不去管这些细节。也是封装调用的好处。

**正确性证明**:
不变性: 对于当前元素V[i]的前缀V[0, i)中, 各元素彼此互异
初始时, i = 1, 两个元素成立,…

单调性:
1)前缀单调非降, 后缀减少, 且前缀迟早增至_size; // 与1)结合
2)后缀单调下降, _size迟早减至0; // 2)更容易把握

故算法必然终止, 至多迭代O(n)轮

复杂度

主要来自find() 和 remove();
find()从右向左针对前缀, remove()从左向右针对后缀, 因此每次while循环不超过O(n);
while循环会进行n次,
总体复杂度为 O(n^2)

练习三种优化方案(未完成)

1, 仿照uniquify()高效版本的思路.
元素移动的次数可以降至O(n), 但比较次数依然是O(n^2); 而且破坏稳定性
2, 先对需要删除的重复元素标记, 然后统一删除.
稳定性保持, 但因查找长度更长, 从而导致更多的对比操作
3, V.sort().uniquify(): 简明实现最优的O(nlogn)

遍历

visit

关于函数指针和函数对象的笔记
函数指针机制 ??

函数对象机制 ??

两种方法优劣

实例: 将向量中所有的元素统一加一
重载操作符 “++”
重载操作符 “()”

练习更为复杂的遍历

减1

// 遍历运用函数对象机制,对各个元素减1
/* my test code
// 单个T类型元素减1的类
template <typename T>
Vector<T>::struct Decrease {
virtual void operator() {T &e--;} // 重载()操作, 类对象当作函数来用
};

template <typename T>
void decrease(Vector<T>& V) {
V.traverse(Decrease<T>());
}
// 泛型模板在调用的时候都要带<type>
*/

template <typename T>
struct Decrease {
virtual void operator()(T &e) {e--;}
};
template <typename T>
void decrease(Vector<T> & V) {
V.traverse(Decrease<T>());
}

为什么不是Vector::struct Decrease?

// Decrease对象, 不需要声明在vector类内, 这是借用函数对象作为traverse的参数, traverse声明在类内, 这个参数的类型是VST, 即函数对象。

为什么需要virtual

void operator()(T &e)?

// 第一个括号是代表重载运算符是(), 第二个是该重载函数的参数列表

三个函数之间的关系?

Decrease(T&)函数对象, Vector::traverse(VST&)遍历函数, decrease(Vector)

// 在类的外部定义decrease函数, 它具有泛型T, 参数为Vector& 类型
// decrease()函数内部, 实例V调用traverse方法, 通过多次调用这个函数对象去遍历所有元素

为什么需要函数对象?

本质问题: 为什么需要函数对象or 函数指针, 为什么不直接调用函数呢?

将遍历traverse作为一种统一的接口, 具体的操作让其他的函数对象来完成, 这样子的接口相对清晰。

加倍

Double(T &e)

template <typename T>
struct Double_value {
virtual void operator()(T& e) {e *= 2;} // 函数对象对元素翻倍
};

template <typename T>
void double_value(Vector<T>& V) {
V.traverse(Double_value<T>()); // 函数对象作为遍历函数的参数
}

求和

函数中传入指针

Sum(T& e, T* sumPtr)

// 用于traverse的求和函数对象
template <typename T>
struct Sum {
virtual void operator()(const T& e, T* sumPtr) {(*sumPtr) += e;}
};


template <typename T> template <typename VST>
T& Vector<T>::traverse(VST visit, T* e) {
// 重载traverse(), 对Sum对象函数传入保存求和结果的指针
for (int i = 1; i < _size; i++) visit(_elem[i], e); // 从第二个值开始累加
return *e; // 返回求和结果
}

Sum()函数对象中传入的参数, 在traverse的visit中定义好的, 因此需要重载traverse函数, 让它传入一个保存求和结果的指针。

前面都没什么问题,
实际调用的函数, 第一次我是这样写的, 返回的结果正确无误。

template <typename T>
void sum_ptr(Vector<T>& V) {
T* sumPtr = &(V[0]); // 别定义空指针, 该指针指向向量首位
T sum = V.traverse(Sum<T>(), sumPtr); // 调用并赋值
std::cout << "sum = " << sum << std::endl;
}

但print_vector()之后

-- --------print_vector--------- --
_size = 3
_capacity = 10
0:52 ==> _elem的首位被修改, 原本应该是10
1:24
2:18
3:0
4:0
5:0
6:0
7:0
8:0
9:0

因为Sum函数对象中, 会通过解引用指针, 不小心修改了V[0]的值!!!

于是我便做了如下修改:

template <typename T>
void sum_ptr(Vector<T>& V) {
T sum_init = V[0]; // 创建一个副本, 避免通过指针修改V[0]
T* sumPtr = &(sum_init); // 别定义空指针, 该指针指向副本
T sum = V.traverse(Sum<T>(), sumPtr); // 调用并赋值
std::cout << "sum = " << sum << std::endl;
}

-- --------print_vector--------- --
_size = 3
_capacity = 10
0:10
1:24
2:18
3:0
4:0
5:0
6:0
7:0
8:0
9:0
重载函数什么时候用virtual?

类内static存放

static sumStc
静态数据成员在类内部声明, 外部定义, 作用相当于全局变量。

// 通过类内static求和的函数对象
template <typename T>
struct Sum_static {
static T sum_t; // 内部声明存放求和结果的static变量
virtual void operator()(T& e) {
sum_t += e; std::cout << "revise static sum_t, sum_t = "
<< sum_t << std::endl; // 当心全局变量出错
}
};

template <typename T>
T Sum_static<T>::sum_t = 0; // 外部定义静态数据成员

template <typename T>
void sum_static(Vector<T> V) {
Sum_static<T> sumstc; // 实例化模板别忘记<type>
V.traverse(Sum_static<T>());
T sum = sumstc.sum_t;
std::cout << "in sum_static, sum = " << sum << std::endl;
}

关于static, 我的相关练习source code

邓俊辉<<数据结构>>-公开课-02-B

发表于 2018-01-10

可扩充向量

静态管理空间

_capacity固定, 存在不足:

1, 上溢(overflow), _elem[]不足以存放元素
2, 下溢(underflow), _elem[]中元素寥寥无几
装填因子(load factor) = size/ capacity << 50% 空间利用率低

动态管理空间

蝉的哲学:每过一段时间,身体生长,以致于无法外壳容纳自己的身体,退去一层外壳,但之以容纳新的外壳。

即将发生上溢,适当扩展容量

扩容算法实现

最终的扩容代码如下:

template <typename T>
void Vector<T>::expand() {
if (_size < _capacity) return; // 容量未到达不必扩容
_capacity = max(_capacity, DEAFAULT_CAPACITY); // 不小于最小容量
// 存储旧元素, 新数组容量扩大
T* old_elem = _elem; _elem = new T[_capacity <<= 1];
for (int i = 0; i < _size; i++) {
_elem[i] = old_elem[i]; // 复制原数组到新数组的对应位置
}
delete [] old_elem; // 释放原数组的对应空间,归还系统
}

另外尝试两种出现BUG的代码:

/* my test code
template <typename T>
void Vector<T>::expand() {
if (_size == _capacity) { // 规模到达最大容量时
// 创建一个容量更大的数组
T* _new_elem = new T[_capacity *= 1.2]; // ERROR:没有delete又重新new了一个
Rank _tmp_size = _size;
_size = 0;
// 复制原数组到新数组的对应位置
copyFrom(this->_elem, 0, _tmp_size);
// 释放原数组的对应空间,归还系统
delete [] _elem;
}
}
*/

/* 最终版本的expand()中, new一个数组储存旧元素, 下面new一个数组, 作为扩容数组
template <typename T>
void Vector<T>::expand() {
if (_size < _capacity) return; // 容量未到达不必扩容
_capacity = max(_capacity, DEAFAULT_CAPACITY); // 不小于最小容量
// 存储旧元素, 新数组容量扩大
T* new_elem = new T[_capacity <<= 1];
for (int i = 0; i < _size; i++) {
new_elem[i] = _elem[i]; // 复制原数组到新数组的对应位置
}
// ERROR:不能直接赋值, 需要copyFrom(), 这里对_elem的更新不直接, 先新后旧的原则
_elem = new_elem; // ERROR:当前数组元素更新为新数组元素, 可T类型还没有operator=()
delete [] new_elem; // 释放临时数组的对应空间,归还系统
}
*/

第二种错误代码分析:

  • 备份一份旧的, 把当前的刷成新的。
  • 创建一份新的, 把旧的拷贝过去, 在赋值给旧的。这种方法做了多余的操作。

封装的好处

  • 得益于向量的封装, 尽管扩容之后数据区的物理地址有所改变, 却不致出现野指针, 封装后,上述通过_elem统一的指示器标记起点.

为何必须采用容量加倍

  • 递增式扩容
  • 加倍式扩容

递增式扩容

_elem = new[T = _capacity + INCREMENT];     // 追加固定大小量

每次扩容, 复制原向量的成本
I, 2I, 3I, … (m-1)I // 算术级数

算术级数:从某个数开始, 以固定间隔为
单位, 不断的线性递增,总和成为算术级数。总和和末项成平方关系

总耗时 = I (m-1) m/2 = O(n^2), 每次O(n)

加倍式扩容

1, 2, 4, 8, 16…2^m 次扩容 // 几何级数
几何级数与末项等阶, O(n)

总耗时O(n), 每次扩容分摊成本为O(1)

空间上的牺牲, 在时间上获得巨大的收益。

分摊复杂度

平均分析 vs 分摊分析

平均分析(average/ expected complexity)
独立事件, 割裂相关性
往往不能准确反应。

分摊分析(amortized complexity)
连续的, 足够多的操作。
实际可行,整体考量。
更为真实反应。

邓俊辉<<数据结构>>-公开课-02-A

发表于 2018-01-10

接口与实现

如何根据同一接口规范,定制ADT和实现implementation一个数据结构?
如何通过更有效的算法,使得对外接口更高效的工作?

  • search
  • sort

    Abstract Data Type vs. Data Structure

    抽象数据类型 = 数据模型 + 定义在该模型上的一组操作
    数据结构 = 基于某种特定语言,实现ADT的一套算法

向量ADT

数组到向量

数组是连续的内存空间,均匀划分成若干个单元,而每一个单元都与[0, n)的编号一一对应

A[i] = A + i * s, s为单个元素所占空间量, 故亦称作线性数组(linear array)

向量是数组的抽象和泛化, 由一组元素按照线性次序封装而成
与[0, n)内的秩(rank)一一对应 // 循秩访问(call-by-rank)
元素类型不限于基本类型
操作,管理维护更加简化安全。
可更为简便的参与更复杂的数据结构的定制。

操作实例

Vector模板类

typedef int Rank;             // 秩
#define DEAFAULT_CAPACITY 3 // 默认初始容量

template <typename T> class Vector { // 向量模板类
private:
Rank _size; int _capacity; T* _elem; // 规模, 容量, 数据区
protected:
/* ... 内部函数*/
public:
/* ... 构造函数*/
/* ... 析构函数*/
/* ... 只读函数*/
/* ... 可写函数*/
/* ... 遍历函数*/
};

应用和实现相互分离;
实现对内部数据项的封装。

构造和析构

template <typename T> class Vector {   // 向量模板类
private:
Rank _size; int _capacity; T* _elem; // 规模, 容量, 数据区
protected:
/* ... 内部函数*/
public:
// /* ... 构造函数
void copyFrom(T* const A, Rank lo, Rank hi);
Vector(int c = DEAFAULT_CAPACITY)
{_elem = new T[_capacity = c]; _size = 0;} // 默认
Vector(T* const A, Rank lo, Rank hi) // 数组区间复制
{copyFrom(A, lo, hi);}
Vector(Vector<T> const& V, Rank lo, Rank hi) // 向量区间复制
{copyFrom(V, lo, hi);}
Vector(Vector<T> const& V) // 向量整体复制
{copyFrom(V._elem, 0, V._size);}
// */
// /* ... 析构函数
~Vector() {delete [] _elem;} // 释放内部空间
// */
/* ... 只读函数*/
/* ... 可写函数*/
/* ... 遍历函数*/
};

template <typename T>
void Vector<T>::copyFrom(T* const A, Rank lo, Rank hi) {
_elem = new T[_capacity = 2*(hi- lo)]; // 分配空间
_size = 0; // 清零规模
while (lo < hi) // A[lo, hi)中的元素逐一
_elem[_size++] = A[lo++];
}
1…15161718

mituh

179 日志
99 标签
© 2018 mituh
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4