d2 有序向量: 二分查找 |
C++ Zero To One 0.004
# 1, bool和int的隐式转换 |
邓俊辉<<数据结构>>-公开课-02-D1
|
C++ Zero To One 0.003
重载函数什么时候用virtual?
源码: source code - load_funtion.cpp
问题来源
template <typename VST> void traverse(VST visit); |
为什么这里不需要virtual??
// 单个T类型元素加1的类 |
为什么这里又需要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 { |
关键问题: 上述两者什么差别????
运行测试一下:// 注释是使用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 |
不使用虚方法的运行结果: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, 可以让继承类对他的重载操作符行为进行重载。当调用基类的引用或者指针时候, 在编译阶段就能绑定到继承类对应的重载函数版本。
静态成员声明定义使用
静态成员的特点
静态数据成员不属于任何类的一个对象。
类似于全局变量, 存在于任何的函数定义之外, 一直存在于整个程序的生命周期
类内static成员如何声明定义?
- 在类内部声明, 外部通过类名作用域访问定义
{
class MyStatic
public:
static int i_stc; // 类内部声明
// ...
}
int MyStatic::i_stc = 0; // 类外部定义, 注意需要声明成员的类型
也可以通过静态成员函数和一般的成员函数对它进行修改, 此时静态成员函数可通过类名访问。
在不同实例中被多次调用改变后的情况?
void MyStatic::increment() {++i_stc;} |
c0 = 1 |
静态数据成员类似于全局变量, 无论在何处, 每调用一次修改静态成员的函数, 数据相应的改变, 且不会随着作用域的离开而销毁。
建筑设计应用和计算机科学一点随想
要掌握一门技术或者一种思想, 离不开实践和思考。即使从最简单的问题入手, 也可以检验自己对知识是否融会贯通。这是计算机科学的一种特性吧。在认识到这一点之后, 学习应该更注重知识与知识之间的联系, 这一点我本身的思维特点决定了它不差; 另一点便是, 注重知识的应用, 我容易陷入这个陷阱, 以致于现在抛开应用, 离开建筑, 去学习思考这些问题。
批判性思维指引我, 我的做法和别人的作法有什么区别, 别人可以是书籍, 视频, 演讲还有编译器。得益于这一点, 我最近找到了一些学习语言的门道。但是如果要说到应用,可能需要批判性思考的嵌套, 也就是指向批判性思维的指针。这个技巧在实际应用中哪些地方会用到?如何运用?这个DSA和建筑问题是否相关?这个库的调用或者学习对建筑的生成有多少帮助?提这些问题, 并不是说对计算机科学的深入学习点到为止, 而是让我循序渐进的学习。它能告诉我这一段知识的学习, 内存泄漏的厉害; 这一段知识的学习, 太追求结果和封装; 这一段知识的学习, 缺少自我和外我的交流。
学习角度讲, 即使从一个很小很简单的问题入手, 也能对自己有所提高。 因为, 计算机科学没有简单, 复杂是由简单构成的, 计算机科学承认复杂, 计算机科学尝试破译复杂。而建筑恰恰就是一个复杂黑箱。事实也证明, 往往穿透的, 创新的思想, 在学科的结合之间会泵发出生命力。我该反思。
C++ Zero To One 0.002
数组, 指针数组, 指向指针的指针的数组, ….
在逛Cplusplus Forum - Beginner的时候发现了一个看上去看简单的问题
How do I receive two numbers from the keyboard, save them in an array of two element?
第一次尝试
void f0() { // ERROR: 先存放再输入不会修改数组中的数据 |
我想得太简单了。不能在标准输入中, 直接对数据进行修改。
下面给出的答案
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: 为什么不能通过数组中保存的指针修改数组?? |
$ ./a.out |
很气, 我得出结论。不能通过数组中保存的指针修改数组。
数组作为函数形参
接着, 我想起之前在函数中通过数组改变过值, 便想要实现如下任务:
创建一个函数, 输入一个保存指向整数的指针的数组, 修改索引n位置的指针所指向的值, 返回
void revise_value_int(int* arr, int n) { |
可以改变。但是仔细一想, 我仅仅只是在数组中存放了整数。
函数形参为存放指针的数组
接着,我改变了数组,用于存放指针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() { |
编译得到**(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 = 0x7ffed5caa560
*iarr = 0x7ffed5caa548
**iarr = 1
更多的尝试
数组中存放对象
template <typename T> |
Name = tim |
面向对象可以让我从无尽的指针中抽身出来, 关注相互之间的联系。
数组中存放长度不同的数组
void f_array() { |
对于简单的数组, 打印储存不同长度数组的数组, 需要更多的数据结构知识, 而且不同的算法还有好坏, 可见数据结构的重要性. 这部分的打印实现, 留作日后我数据结构解引用后的小试牛刀。
感想
即使从一个很小很简单的问题入手, 也能对自己有所提高, 计算机科学没有简单, 复杂是由简单构成的, 计算机科学承认复杂, 计算机科学尝试破译复杂。
常量成员函数
class MyClass { |
声明时引用符号总最靠近变量名
为什么可以对a.get()赋值
get()返回的是引用, 引用可作为左值, 且如果非常量引用的话, 该值可以被修改。
在是否为成员函数上重载后如何匹配?
成员函数在是否为常量函数上可以重载。如果对象实例化成const类型, 则匹配常量成员函数。
const int& get() const {return x;} 为什么返回类型需要const?
常量函数返回数据成员, 编译器会将数据成员声明成const类型;
什么是数据成员?
MyClass类public中定义新成员测试是否为数据成员。
//.. |
改变a的值无法通过编译, a为数据成员。
在class中声明的, 无论是私有的还是公有的数据, 都是成员数据。如果在常量函数中返回, 会常量函数会被声明成const类型。
template基础用法
typename T 和 class T的区别
在模板参数列表中, typename和class没有什么不同。 但每个参数类型前必须加上typename 或者 class
<<C++ Primer 5th>> P580 模板与泛型编程
特殊化模板
template <typename T> |
对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
接下来, 准备archive每天实际编程中遇到的C++问题,解决问题的过程, 以及得到的结果。以此来熟悉C++的各种特性。
这样做或许会造成知识点离散, 因此, 后续有必要有选择的,对某些特性进行深入剖析。先破破的来吧,以致于我不会从一个细节陷阱中出来,又陷入另一个。
右移,左移
_capacity >>= 1; // _capacity *= 0.5; |
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) { |
为什么一定要为_elem分配空间? 内存回收问题。 在C++ Primer中具体学习
同一if语句, 同一行中的后置递增
void f() { |
同一if, for, while语句中, 后置递增返回当前值
同一行, 不同语句, 当然会让值产生变化
重载后置++操作符号
前置递增 operator++()
后置递增, 以前置递增为基础 operator++(int i)
increment_and_decrement_operators.cpp
递增运算符重载的返回类型:为什么返回*_elem 而不是 *this??
递减运算符重载函数的返回类型是int&
*this的类型为Vector
template <typename T> |
<<C++ Primer 5th>> P502, P421
函数指针 和 函数对象
对无序向量的遍历操作中, 统一对各个元素分别实施visit操作
函数指针, 只读或者局部性修改
template <typename T> |
函数对象, 可全局性修改
template <typename T> <template VST> |
函数指针
int getLarger(const int& i1, const int& i2) { |
声明定义
声明和定义分离: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) { |
能重新指向一个nullptr或0, 但是不能指向一个与声明类型不同的新函数地址。
但如果是一个类型相同的函数, 便可以重新指向该函数。
bool otherCompare(const int& i1, const int&) {return 0;} |
重载函数的指针
函数指针 指向重载函数, 需要在声明时, 形参列表和返回类型需要完全匹配
void ff(int*); |
函数指针作为形参(调用函数指针)
// 第三个参数是函数指针类型 |
可以用别名的方法简化定义
typedef decltype(compareInt) *FuncP2; // FuncP2是指向函数的指针类型 |
返回指向函数的指针
一般, 别名
using PF = bool(*)(const int&, const int&); // PF是函数的指针类型 |
尾置auto f1(int) -> int(*)(int*, int)
函数对象
重载()
说明这个对象, 他可以当作函数来使用。因此需要重载”()”
// 定义一个函数对象 |
使用这个函数对象
Sum s; |
作为其他函数的参数
// 函数对象作为另一个函数的形参 |
回顾一下数据结构中的用法
template <typename T> template <typename VST> |
/* my error test |
邓俊辉<<数据结构>>-公开课-02-C
无序向量
template <typename T> class Vector {}; // template定义方式 |
模板和模板, 模板和类之间可以互相组合。意味着数据结构之间也可以互相组合
template <typename T> class Vector { |
无序向量: 没有顺序, 甚至不可能排成顺序。
元素访问(寻秩访问)
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);
插入
// 插入 |
Template中泛型T的作用,
模板类中函数的互相搭配,
插入元素对vector操作的顺序
对_capacity和_size的影响。
删除算法
自前向后的迁移操作
缩容
// 删除操作 |
1, 规模仍旧不变? 删除一段区间, 这里可以不改变规模, 相当于后面留空?
改进成改变size的版本用于shrunk2 , _elem[hi++]能够被一直索引到?
超过_capacity时, 返回未定义的值3, _elem[hi++]为什么不清空?
把_capacity的剩余空间对应元素赋值给它的方法清空4, 看出移动操作过程中, 变量的同步性
5, 缩容不光光是改变_capacity的值, 仍旧要释放空间
查找
无序向量: T为可判等, 重载 “==”或者”!=”
有序向量: T为可比较,重载 “<” 或 “>”
// 查找 |
利用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!!
唯一化
实现
// 唯一化 |
记录规模?
考虑返回值怎么设计,这里我估计会在更高级的接口中用到从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 |
template <typename T> |
为什么不是Vector::struct Decrease?
// Decrease对象, 不需要声明在vector类内, 这是借用函数对象作为traverse的参数, traverse声明在类内, 这个参数的类型是VST, 即函数对象。
为什么需要virtual
void operator()(T &e)?
// 第一个括号是代表重载运算符是(), 第二个是该重载函数的参数列表
三个函数之间的关系?
Decrease
// 在类的外部定义decrease函数, 它具有泛型T, 参数为Vector
// decrease()函数内部, 实例V调用traverse方法, 通过多次调用这个函数对象去遍历所有元素
为什么需要函数对象?
本质问题: 为什么需要函数对象or 函数指针, 为什么不直接调用函数呢?
将遍历traverse作为一种统一的接口, 具体的操作让其他的函数对象来完成, 这样子的接口相对清晰。
加倍
Double(T &e)
template <typename T> |
求和
函数中传入指针
Sum(T& e, T* sumPtr)
// 用于traverse的求和函数对象 |
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--------- -- |
重载函数什么时候用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
可扩充向量
静态管理空间
_capacity固定, 存在不足:
1, 上溢(overflow), _elem[]不足以存放元素
2, 下溢(underflow), _elem[]中元素寥寥无几
装填因子(load factor) = size/ capacity << 50% 空间利用率低
动态管理空间
蝉的哲学:每过一段时间,身体生长,以致于无法外壳容纳自己的身体,退去一层外壳,但之以容纳新的外壳。
即将发生上溢,适当扩展容量
扩容算法实现
最终的扩容代码如下:
template <typename T> |
另外尝试两种出现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
接口与实现
如何根据同一接口规范,定制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; // 秩 |
应用和实现相互分离;
实现对内部数据项的封装。
构造和析构
template <typename T> class Vector { // 向量模板类 |