提示:建议看完侯捷老师的STL标准库视频后,再看这本书,会看的下去一些!本人对其中重点内容和概念进行了提取,希望对一起前行的人有些许帮助,码字不易,欢迎点个赞呦!
一、可能令你困惑的C++语法
临时对象:一种无名对象
在类型名称后直接加一对小括号,并可指定初值
shape(3,5),int(8),其意义相当于调用相应的构造函数且不指定对象名称
最常用于仿函数与算法的搭配上
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
template<typename T>
class print {
public:
void operator()(const T& elem) {
cout << elem << " ";
}
};
int main() {
int ia[6] = { 0,1,2,3,4,5 };
vector<int>iv(ia, ia + 6);
print<int>();//临时对象,不是一个函数调用操作
//当for_each()结束时,临时对象也结束了它的生命
for_each(iv.begin(), iv.end(), print<int>());
return 0;
}
//函数指针的用法
#include<iostream>
#include<cstdlib>
using namespace std;
int fcmp(const void *elem1,const void *elem2);
int main() {
int ia[10] = { 32,94,67,58,10,4,25,52,59,54 };
for (int i = 0; i < 10; i++) {
cout << ia[i] << " ";
}
cout << endl;
//过去将函数当参数传递,唯有通过函数指针
qsort(ia, sizeof(ia) / sizeof(int), sizeof(int), fcmp);
for (int i = 0; i < 10; i++) {
cout << ia[i] << " ";
}
return 0;
}
int fcmp(const void* elem1, const void* elem2) {
const int* i1 = (const int*)elem1;
const int* i2 = (const int*)elem2;
if (*i1 < *i2) {
return -1;
}
else if (*i1 == *i2) {
return 0;
}
else if (*i1 > *i2) {
return 1;
}
}
函数指针缺点:无法持有自己的状态,也无法达到组件技术中的可适配性
无法再将某些修饰条件加诸于其上而改变其状态
//C++仿函数
#include<iostream>
using namespace std;
template<class T>
struct plus1 {
T operator()(const T& x, const T& y) const{
return x + y;
}
};
template<class T>
struct minus1 {
T operator()(const T& x, const T& y) const {
return x - y;
}
};
int main() {
//产生仿函数对象
plus1<int>plusobj;
minus1<int>minusobj;
//以下使用仿函数,就像使用一般函数一样
cout << plusobj(3, 5) << endl;
cout << minusobj(3, 5) << endl;
//以下直接产生仿函数的临时对象(第一对小括号),并调用之(第二对小括号)
cout << plus1<int>()(43, 50) << endl;
cout << minus1<int>()(43, 50) << endl;
return 0;
}
二、空间配置器
allocator是空间配置器而不是内存配置器
因为空间不一定是内存,空间也可以是磁盘或其它辅助存储介质
#ifndef _JJALLOC
#define _JJALLOC
#include<new>
#include<cstddef>
#include<cstdlib>
#include<climits>
#include<iostream>
namespace JJ{
template<class T>
inline T* _allocate(ptrdiff_t size, T*) {
set_new_handler(0);
T* tmp = (T*)(::operator new((size_t)(size * sizeof(T))));
if (tmp == 0) {
cerr << "out of memory" << endl;
exit(1);
}
return tmp;
}
template<class T>
inline void _deallocate(T* buffer) {
::operator delete(buffer);
}
template<class T1,class T2>
inline void _destroy(T* ptr) {
ptr->~T();
}
template<class T>
class allocator {
public:
typedef T value_type;
typedef T* pointer;
typedef const T* const_pointer;
typedef T& reference;
typedef const T* const_ reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
template<class U>
struct rebind {
typedef allocator<U>other;
};
pointer allocate(size_type n, const void* hint = 0) {
return _allocate((difference_type)n, (pointer)0);
}
void deallocate(pointer p, size_type n) {
_deallocate(p);
}
void construct(pointer p, const T& value) {
_construct(p, value);
}
void destory(pointer p) {
_destroy(p);
}
void address(reference x) {
return (pointer)&x;
}
const_pointer const_address(const_reference x) {
return (const_pointer)&x;
}
size_type max_size()const {
return size_type(UINT_MAX / sizeof(T));
}
};
}
#endif // _JJALLOC
//必要接口
//allocator::value_type
//allocator::pointer
//allocator::const_pointer
//allocator::reference
//allocator::const_reference
//allocator::size_type
//allocator::difference_type
#include"jjalloc.h"
#include<vector>
#include<iostream>
using namespace std;
int main() {
int ia[6] = { 0,1,2,3,4 };
unsigned int i;
vector<int, JJ::allocator<int>>iv(ia, ia + 5);
for (i = 0; i < iv.size(); i++) {
cout << iv[i] << " ";
}
cout << endl;
return 0;
}
在STL allocator中
new
1)调用::operator new配置内存 alloc::allocate()
2)调用Foo::Foo()构造对象内容 ::construct()
delete
1)调用Foo::~Foo()将对象析构 ::destory()
2)调用::operator delete释放内存 alloc::deallocate()
class Foo{...};
Foo* pf = new Foo;//配置内存,构造对象
delete pf;//将对象析构,释放内存
construct()接受一个指针p和一个初值value:将初值设定到指针所指的空间上
destroy()第一个版本接受一个指针,准备将该指针所指之物析构掉
第二个版本接受first和last两个迭代器,准备将[first,last]范围内的所有对象析构掉
我们不知道这个范围多大,万一很大,而每个对象的析构函数都无关痛痒 (trivial destructor)
那么一次次调用这些无关痛痒的析构函数,对效率是一种伤害
首先用value_type获取迭代器所指对象的类别,再利用_type_traits判断该类型的析构函数是否无关痛痒
若是(__true_type),那什么也不做就结束
若否(__false_type),这才循环遍历整个范围,并在循环没经历一个对象就调用第一个版本的destroy()
C++内存配置基本操作是::operator new(),相当于malloc()
内存释放基本操作是::operator delete(),相当于free()
考虑到小型区块可能造成内存破碎问题,设计了双层级配置器
第一级配置器直接使用malloc()和free()
allocate()直接使用mallco()
deallocate()直接使用free()
模拟C++的set_new_handler()以处理内存不足的状况
**C++ new handler机制:**可以要求系统在内存配置需求无法被满足时调用一个你所指定的函数,一旦::operator new无法完成任务
在丢出std::bad_alloc异常状态之前:会先调用客户端指定的处理例程
该处理例程通常被称为new handler,解决内存不足的做法有特定的模式
第二级配置器,当配置区块超过128bytes时,视为“足够大”,调用第一级配置器
当小于128bytes时,视为“过小”,采用memory pool,不在求助于第一级配置器
维护16个自由链表,负责16个小型区块的次配置能力
内存池memory pool以malloc()配置而得
如果内存不足,转调用第一级配置器
配置器除了负责配置,也负责回收
会主动将任何小额区块的内存需求量上调至8的倍数,并维护16个链表
大小为8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128
如果free list之内有可用的区块,直接拿来用
如果没有可用区块,就将区块大小调至8倍数边界,然后调用refill()
为free list重新填充空间
free_lists的节点结构
obj可被视为一个指针,指向相同形式的另一个obj
union obj {
union obj* free_list_link;
char client_data[1];
};
内存池的原理
chunk_alloc()函数以end_free-start_free来判断内存池的水量
如果水量充足,就直接调出20个区块返回给free list
如果水量不足以提供20个区块,但还足够供应一个以上的区块
就拨出这不足的20个区块空间出去
这时候其pass by reference的nobjs参数就被修改为实际能够供应的区块数
如果内存池连一个区块空间都无法供应
此时需要利用malloc()从heap中配置内存
为内存池注入活水源头以应付需求
新水量的大小为需求量的两倍
再加上一个随着配置次数增加而愈来愈大的附加量
内存池分配内存假设
程序一开始,客户端调用chunk_alloc(32,20),malloc()配置40个32bytes区块
其中一个交给客户端,另19个交给free_list[3]维护,余20个留给内存池
客户端调用chunk_alloc(64, 20),此时free_list[7]空的,想内存池要求支持
内存池足够供应(32*20)/64=10个64bytes区块,把这10个区块返回
第一个交给客户端,余9个由free_list[7]维护,此时内存池全空
客户端再调用chunk_alloc(96, 20),此时free_list[11]空的,想内存池要求支持
内存池也是空的,malloc()配置40+n(附加量)个96bytes区块
其中一个交给客户端,另19个交给free_list[11]维护,
余20+n(附加量)个96bytes区块留给内存池
万一system heap的空间都不够了(以至于无法为内存池注入活水)
malloc()行动失败,chunk_alloc()就四处寻找有无“尚有未用区块,且区块够大”的链表
找到了就挖一块交出,找不到就调用第一级配置器
第一级配置器其实也是使用malloc()来配置内存,有out-of-memory处理机制
或许有机会释放其它的内存来此处使用,如果可以就成功,否则抛出bad_alloc异常
几个内存处理工具
如果需要实现一个容器,容器的全区间构造函数通常是以下两步完成
1)配置内存区块,足以包含范围内的所有元素
2)使用uninitialized_copy(),在该内存块上构造元素
要么构造出所有必要元素
要么当有任何一个copy constructor失败时不构造任何东西
类似
uninitialized_fill()
uninitialized_fill_n()
POD:Plain Old Data标量型别
三、迭代器
迭代器:提供一种方法,使之能够依序巡防某个容器所含的各个元素而又无需暴露该容器的内部表述方式,数据容器和算法之间的一帖胶着剂,迭代器是一种行为类似指针的对象
内容提领(dereference),成员访问(member access)
就是对operator*和operator->进行重载
#include<iostream>
#include<memory>
using namespace std;
void func() {
auto_ptr<string>ps(new string("jjhou"));
cout << *ps << endl;//jjhou
cout << ps->size() << endl;//5
//离开前不需delete,auto_ptr会自动释放内存
}
int main() {
func();
return 0;
}
//仿真auto_ptr
template<class T>
class auto_ptr {
public:
explicit auto_ptr(T* p = 0) :pointee(p){}
template<class U>
auto_ptr(auto_ptr<U>&rhs):pointee(rhs.release()){}
~auto_ptr() { delete pointee; }
template<class U>
auto_ptr<T>& operator=(auto_ptr<U>& rhs) {
if (this != &rhs)reset(rhs.release());
return *this;
}
T& operator*()const { return *pointee; }
T* operator->()const { return pointee; }
T* get()const { return pointee; }
private:
T* pointee;
};
traits扮演特性萃取机的角色,萃取各个迭代器的特性:迭代器的相应型别(associated types)
1)value type:迭代器所指对象的型别
2)different type:两个迭代器之间的距离
3)reference type:*p的型别
迭代器分两种,不允许改变"所指对象之内容",p是一个constant iterators,p是const T&,获得的右值,不允许赋值
允许改变"所指对象之内容",p是一个mutable iterators,p是T&,获得的左值,可以赋值
4)pointer type:传回一个pointer,指向迭代器所指之物
Item& operator()const { return ptr; },Item&是reference type
Item operator->()const { return ptr; },Item是pointer type
5)iterator_category:迭代器的分类
struct input_iterator_tag{};
struct output_iterator_tag{};
struct forward_iterator_tag :public input_iterator_tag{};
struct bidirectional_iterator_tag :public forward_iterator_tag{};
struct random_access_iterator_tag :public bidirectional_iterator_tag{};
继承是为了传递调用
四、序列式容器
此处一起展示,后续第五章关联式容器可看此表
array是静态空间,一旦配置就不能改变
要换大(小)的房子
1)配置一块新空间
2)将元素从旧地址搬到新地址
3)将原来的空间还给系统
vector动态数组,吃多少用多少
线性连续空间
所谓动态增加大小,并不是在原空间之后接续新空间(无法保证原空间之后尚有可供配置的空间)
而是以原大小的两倍另外配置一块较大空间,然后将原内容拷贝过来
然后才开始在原内容之后构造新元素,并释放原空间
因此对vector的任何操作,一旦引起空间重新配置
指向原vector的所有迭代器就都失效了
缺省使用alloc作为空间配置器
template<class T, class Alloc = alloc>
class vector {
public:
typedef T value_type;
typedef value_type* pointer;
typedef value_type* iterator;//vector的迭代器是普通指针,random_access_iterator
typedef value_type& reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
protected:
//方便以元素大小为配置单位
typedef simple_alloc<value_type, Alloc>data_allocator;
//主要这三个指针
iterator start;//表示目前使用空间的头
iterator finish;//表示目前使用空间的尾
iterator end_of_storage;//表示目前可用空间的尾,指向整块连续空间(含备用空间)的尾端
void insert_aux(iterator position, const T& x);
void deallocate() {
if (start) {
data_allocator::deallcoate(start, end_of_storage - start);
}
}
void fill_initialize(size_type n, const T& value) {
start = allocate_and_fill(n, value);
finish = start + n;
end_of_storage = finish;
}
public:
iterator begin() { return start; }
iterator end() { return finish; }
size_type size() const { return size_type(end() - begin()); }
size_type capacity()const { return size_type(end_of_storage - begin()); }
bool empty()const { return begin() == end(); }
reference operator[](size_type n) { return *(begin() + n); }
//提供多个构造函数
vector() :start(0), finish(0), end_of_storage(0);
vector(size_type n, const T& value) { fill_initialize(n, value); }
vector(int n, const T& value) { fill_initialize(n, value); }
vector(long n, const T& value) { fill_initialize(n, value); }
~vector() {
destory(start, finish);
deallocate();
}
reference front() { return *begin(); }
reference back() { return *(end() - 1); }
//当我们以push_back()将新元素插入vector尾端时,
//先检查是否还有备用空间
//如果有直接在备用空间构造元素,并调整迭代器finish,使vector变大
//如果没有 备用空间了,就扩充空间(重新配置,移动数据,释放原空间)
void push_back(const T& x) {
if (finish != end_of_storage) {//还有备用空间
construct(finish, x);//全局函数
++finish;//调整水位高度
}
else {//已无备用空间
insert_aux(end(), x);
}
}
void pop_back() {
--finish;//将尾端标记往前移一格,表示将放弃尾端元素
destory(finish);
}
//清除某个位置上的元素
iterator erase(iterator position) {
if (position + 1 != end()) {
copy(position + 1, finish, position);
}
--finish;
destory(finish);
return position;
}
void resize(size_type new_size, const T& x) {
if (new_size < size()) {
erase(begin() + new_size, end();
}
else {
insert(end(), new_size - size(), x);
}
}
void resize(size_type new_size) { resize(new_size, T()); }
void clear() { erase(begin(), end()); }
protected:
iterator allocate_and_fill)(size_type n, const T& x){
iterator result = data_allocator::allocator(n);
uninitialized_fill_n(result, n, x);
return result;
}
};
//已无备用空间,重新配置,移动数据,释放原空间
template<class T, class Alloc>
void vector<T, Alloc>::insert_aux(iterator position, const T& x) {
if (finish != end_of_storage) {//还有备用空间
//在备用空间起始处构建一个元素,并以vector最后一个元素为其初值
construct(finish, *(finish - 1));
++finish;//调整水位
T x_copy = x;
copy_backward(position, finish - 2, finish - 1);
*position = x_copy;
}
else {//已无备用空间
//分配原则,如果原大小为0,则分配1个元素大小
//如果原大小不为0,则分配原大小的两倍
//前半段用来放置原数据,后半段准备用来放置新数据
const size_type old_size = size();
const size_type len = old_size != 0 ? 2 * old_size : 1;
iterator new_start = data_allocator::allocate(len);
iterator new_finish = new_start;
try {
//将原vector的内容拷贝到新vector
new_finish = uninitialized_copy(start, position, new_start);
construct(new_finish, x);//为新元素设初值x
++new_finish;//调整水位
//拷贝安插点后的原内容,也可能被insert(p,x)调用
new_finish = uninitialized_copy(position, finish, new_finish);
}
catch (...) {
destory(new_start, new_finish);
data_allocator::deallocate(new_start, len);
throw;
}
//析构并释放原vector
destory(begin(), end());
deallocate();
//调整迭代器,指向新vector
start = new_start;
finish = new_finish;
end_of_storage = new_start + len;
}
}
list:每次插入或删除一个元素,就配置或释放一个元素空间
节点结构
template<class T>
struct __list_node{
typedef void* void_pointer;
void _pointer prev;
void _pointer next;
T data;
}
插入和删除是常数时间
提供的是bidirectional_iterato
插入(insert)和接合(splice)操作不会造成原有的list迭代器失效
删除操作也只有“指向被删除的元素”的那个迭代器失效,其它迭代器不受影响
是一个环状双向链表,尾端加上空白节点,就符合前闭后开区间了
缺省使用alloc作为空间配置器
额外定义了一个list_node_alocator
更方便地以节点大小为配置单位
list内部提供一个所谓的迁移(transfer)操作
将某个连续 范围的元素迁移到某个特定位置之前,节点间的指针移动而已
是splice,sort,merge的基础
list必须使用自己的sort函数
vector是单向开口的连续线性空间
deque,双向开口的连续线性空间:头尾插入删除
vector从技术角度页而已在头尾两端进行操作,但效率不行,无法接收
deque与vector的最大差异
1)允许常数时间内对起头端进行元素的插入或删除操作
2)没有所谓容量(capacity)观念,因为它是动态地以分段连续空间组合而成
随时可以增加一段新的空间并链接起来,
像vector那样因旧空间不足而重新配置一块更大空间,然后复制元素,释放旧空间
这样的事情在deque身上是不会发生的
deque没有必要提供所谓的空间保留(reverse)功能
提供的是random_access_iterator,但迭代器不是普通指针
除非必要,尽可能选择vector而非deque
对deque进行排序,为了最高效率,可将deque复制到vector
排序后再复制回deque
deque的中控器
采用一小块连续空间map(不是STL的容器)作为主控
其中每个元素(节点)都是指针,指向另一端较大的连续线性空间,称为缓冲区
缓冲区才是deque的储存空间主体,可以指定缓冲区的大小
map其实是个T**,是一个指向指针的指针
iterator start:指向第一个缓冲区的第一个元素
iterator finish:指向最后一个缓冲区的最后一个元素
size_type map_size:map内有多少指针,必须记住map的大小,一旦map提供的节点不足,重新配置更大的一块map
map_pointer map:指向map,map是块连续空间,其每个元素都是指针,指向一个节点(缓冲区)
保持与容器的联结的start和finish迭代器包含哪些:
T* cur:此迭代器所指缓冲器中的现行元素
T* first:此迭代器所指缓冲器的头
T* last:此迭代器所指缓冲器的尾(含备用空间)
map_pointer node:指向管控中心
如果中控器map尾端或前端的节点备用空间不足
则必须重新换一个map,配置更大的,拷贝原来的,释放原来的,类似vector的扩容
容器适配器
stack栈先进后出,不允许遍历,允许新增元素push,删除元素pop,取得最顶端的元素top()
以deque为底层,封闭其头端开口
不提供迭代器
若以list为底层并封闭其头端开口,也能形成stack
容器适配器
queue:先进先出,允许底端加入元素push,最顶端删除元素pop,取得最顶端元素front(),不允许遍历
没有迭代器,以deque为底层,封闭其底端出口,前端的入口
若以list为底层并封闭其底端出口,前端的入口,也能形成queue
heap堆:底层是vector,以vector表现的complete binary tree完全二叉树
max_heap:大顶堆,每个节点得值都大于等于其子节点值
min_heap:小顶堆,每个节点得值都小于等于其子节点值
pop_heap之后,最大元素只是被置放在底部容器得最尾端,尚未被取走
可以用vector得back()函数取,或者pop_back()删除
sort_heap:不断地对heap进行pop操作,达到排序效果
heap不提供遍历功能,也不提供迭代器
如果heap底层是array,因为array无法动态改变大小,无法进行push_heap()的操作
优先队列:priority_queue
利用max_heap,内含一个heap,底层是vector
没有迭代器,不提供遍历
所以元素进出都有一定的规则,只有queue顶端的元素(权值最高者,才有机会被取用)
slist:单向链表,迭代器属于forward_iterator
单向链表耗用的空间更小,某些操作更快
没有任何方便的办法可以回头定出前一个位置,必须从头找起
除了slist起点附近的区域,其他位置采用insert和erase操作不明智
故提供了insert_after(),erase_after()
不提供push_back(),只提供push_front(),从头部插入元素
所以slist元素的次序会和元素插入进来的次序相反
没有实现operator–
五、关联式容器
树的相关概念
关联式容器:类似关联式数据库
每笔数据(每个元素)都有一个键值(key)和一个实值(value)
没有所谓头尾,只有最大元素或最小元素
内部结构是一个bst(平衡二叉树),最广泛的是rb-tree
二叉搜索树:可以提供对数时间的元素插入和访问
任何节点的键值一定大于其左子树中的每一个节点的键值
并小于其右子树中的每一个节点的键值
移除操作,想删除节点a
1)如果a只有一个子节点,直接将a的子节点连到a的父节点,并删除a
2)如果a有两个子节点,以右子树内最小节点取代a,
右子树最小节点极易获得:从右子节点开始,一直向左走即可
avl tree:任何节点的左右子树高度相差最多1
avl-tree的四种“平衡破坏”
单旋转修复:左左外侧插入,右右外侧插入(对称)
双旋转修复:左右内侧插入,右左内侧插入(对称):利用2次单旋转完成
rb-tree不仅是一个二叉搜索树
满足以下规则
1)每个节点不是红色就是黑色
2)根节点是黑色
3)如果节点为红,其子节点必须为黑:新增节点之父节点必须为黑
4)任一节点至null(树尾端)的任何路径,所含之黑节点数必须相同:新增节点必须为红
当新节点根据二叉搜索树的规则到达其插入点
却未能符合上面的条件时,就必须调整颜色并旋转树形
两个连续黑色节点可以连续,两个连续红色节点不可以
typedef bool __rb_tree_color_type;
const __rb_tree_color_type __rb_tree_red = false;//红色为0
const __rb_tree_color_type __rb_tree_black= true;//黑色为1
struct __rb_tree_node_base {
typedef __rb_tree_color_type color_type;
typedef __rb_tree_node_base* base_ptr;
color_type color;//节点颜色,非红即黑
base_ptr parent;//rb树的许多操作,必须知道父节点
base_ptr left;//指向左节点
base_ptr right;//指向右节点
static base_ptr minimum(base_ptr x) {
while (x->left != 0) x = x->left;//一直向左走,就会找到最小值
return x;//二叉搜索树的特性
}
static base_ptr maximum(base_ptr x) {
while (x->right!= 0) x = x->right;;//一直向右走,就会找到最大值
return x;//二叉搜索树的特性
}
template<class value>
struct __rb_tree_node :public __rb_tree_node_base
{
typedef __rb_tree_node<value>* link_type;
value value_field;//节点值
};
};
rb_tree迭代器属于双向迭代器,不能随机定位元素
header和root互为对方的父节点
set:键值就是实值,实值就是键值,根据元素的键值自动排序
不可以通过set的迭代器改变set的元素值,constant iterators
底层红黑树,使用insert_unique(),不允许相同键值存在
应该使用自身的find,比全局find快
multiset:允许键值重复,插入操作使用insert_equal()
map:根据元素的键值(key)自动排序
所有元素都是pair,键值key,实值value
不允许两个元素拥有相同的键值
不可以修改map元素的键值,关系排列规则
可以修改元素的实值
迭代器既不是constant iterators,也不是mutable iterators
底层红黑树,使用insert_unique(),不允许相同键值存在
multimap:允许键值重复,插入操作使用insert_equal()
hash_table散列表:插入,删除,搜索,具有常数平均时间
使用数组做哈希,数目大了之后,索引值很大
哈希函数:让索引的大小可接受,就是将大数映射为小数
x%tablesize会得到一个值整数,范围在0-tablesize-1之间
哈希碰撞:因为元素个数大于数组容量,不同的元素映射到相同的位置(相同的索引上)
负载系数:元素个数/表格大小,在0-1之间
1)线性探测法:循序往下找,到达尾端,就绕道头部继续寻找,直到找到一个可用空间
采用惰性删除:只标记删除记号,实际删除待表格重新整理再进行
因为hash_table中的每一个元素不仅表述它自己,也关系其它元素的排列
需要两个假设:
1-表格足够大
2-每个元素都能独立,最坏的情况是巡防整个表格,平均情况巡防一半
主集团问题:平均插入成本的成长幅度,远高于负载系数的成长幅度,不端解决碰撞问题
2)二次探测法:主要用来解决主集团问题
在h位置碰撞后,尝试h+12,h+22,而不是h+1,h+2的线性探测
假设表格大小是质数,永远保持负载系数在0.5以下,超过0.5就重新配置并整理表格
可以确定每插入一个新元素的探测次数不多于2
二次探测可以消除主集团,但可能造成次集团:两个元素经hash_function计算出来的位置相同,但插入时所探测的位置也相同,形成浪费,消除次集团采用复式散列
2)开链法:每一个表格元素维护一个链表,每个单元涵盖的不只是一个节点(元素),可能是一桶节点
表格的负载系数将大于1
虽然开链法不要求表格的大小必须为质数,但仍然是以质数来设计表格
会找最接近某数并大于某数的质数,质数逐渐呈现大约两倍的关系
节点定义
//bucket所维护的linked list,并不是stl中的list或slist,而是自行维护的hash table node
template<class value>
struct __hasgtable_node {
__hasgtable_node* next;
value val;
};
buckets的聚合体,则是以vector完成,动态扩充
迭代器采用forward_iterator,没有后退操作或者逆向迭代器(reverse iterators)
hashtable的模板参数
value:节点的实值型别
key:节点的键值型别
hashfcn:hash function的函数型别
extractkey:从节点中取出键值的方法(函数或仿函数)
equalkey:判断键值相同与否的方法(函数或仿函数)
alloc:空间配置器,缺省使用std::alloc
判断表格重建与否:拿元素个数(新增的计入后),如果比原来的bucket的大,就重建表格
判断原始的落脚处bkt_num,因为有些元素型别无法直接拿来对hashtable的大小进行取模运算
例如const* char,需要做一些转换
由bkt_num()调用提供的hash function,取得一个可以对hashtable进行模运算的值
处理string,double,float,要自定义hash function
hash_set,hash_map,插入操作使用insert_unique()
hash_set,hash_map,hash_multiset,hash_multimap没有自动排序功能
hash_multiset,hash_multimap,插入操作使用insert_equal()
凡是hashtable无法处理的,hash_set,hash_map,hash_multiset,hash_multimap也无法处理
六、算法
算法:问题之解也
花费固定的时间(常数时间,O(1))将问题的规模降低某个固定比例,通常是1/2,此算法的时间复杂度是O(logN)
质变算法:会改变操作对象之值,指运算过程中会更改区间内(迭代器所指)的元素内容
拷贝、互换、替换、填写、删除、排列组合、分割、随机重排、排序等
1)in-place,就地进行版,就地改变其操作对象
2)copy,另地进行版,将操作对象的内容复制一份副本,然后在副本上修改并返回该副本
非质变算法:不改变操作对象之值
查找、匹配、计数、巡防、比较、寻找极值等
所有泛型算法的前两个参数都是一对迭代器,通常称为first,last,前闭后开
泛化:算法适用于任何(大多数)数据结构
find的泛化过程
在array中寻找特定值
在某个区间查找value,返回一个指针,指向它所找到的第一个符合条件的元素
如果没有找到,就返回最后一个元素的下一位置(地址)
int* find(int* arrayHead, int arraySize, int value) {
int i = 0;
while(i< arraySize) {
if (arrayHead[i] == value) {
break;
}
i++;
}
return &(arrayHead[i]);
}
find的使用
最后一个元素的下一位置称为end
const int arraySize = 7;
int ia[arraySize] = { 0,1,2,3,4,5,6 };
int* end = ia + arraySize;//最后元素的下一位置
int* ip = find(ia, sizeof(ia) / sizeof(int), 4);
if (ip == end) {
cout << "4 not found" << endl;
}
else {
cout << "4 found" <<*ip<< endl;
}
以上做法暴露太多实现细节了,例如arraySize
让find()接收两个指针作为参数,标示出一个操作区间
int* find(int* begin, int* end, int value) {
while(begin!=end&&*begin!=value) {
++begin;
}
return begin;
}
//find的使用
const int arraySize = 7;
int ia[arraySize] = { 0,1,2,3,4,5,6 };
int* end = ia + arraySize;//最后元素的下一位置
int* ip = find(ia, end, 4);
if (ip == end) {
cout << "4 not found" << endl;
}
else {
cout << "4 found" <<*ip<< endl;
}*/
//find查找array的子区间
int* ip = find(ia + 2, ia + 5, 3);
if (ip == end) {
cout << "3 not found" << endl;
}
else {
cout << "3 found" << *ip << endl;
}
//除了array,还要适合其他类型
template<typename T>
T* find(T* begin, T* end, const T& value) {
operator!=,operator*,operator++
while (begin != end && *begin != value) {
++begin;
}
return begin;
}
何必将find限制只能使用指针,行为很像指针的“某种对象”都可以被find使用
迭代器是一种行为类似指针的对象,是一种智能指针
<class Iterator, class T>
Iterator find(Iterator begin, Iterator end, const T& value) {
while (begin != end && *begin != value) {
++begin;
}
return begin;
}
copy里面的memmove():内存对拷方式
会先将整个输入区间的内容复制下来,没有被覆盖的风险
常见技巧:令函数传递调用过程中产生迭代器类型(iterator category)的临时对象
再利用编译器的参数推导机制,自动调用某个对应函数
unique只移除相邻的重复元素
如果要移除所有的重复元素,要先排序
七、仿函数
仿函数:函数对象,一种具有函数特质的对象
行为类似函数的对象
函数指针不能满足STL对抽象性的要求,也不能满足软件积木的要求
函数指针无法和STL其它组件(如配接器adapter)搭配
无名的临时对象,后面接一对()
STL内建的仿函数在头文件
unary_function用来呈现一元函数的参数型别和返回值型别
binary_function用来呈现二元函数的第一参数型别、第二参数型别和返回值类型
算术类:加减乘除,取模,否定等
关系类:等于、不等于、大于、大于或等于、小于、小于或等于
逻辑类:And、Or、Not
政同identity:任何数值通过此函数,不会有任何改变
选择select:接收一个pair,传回其第一个元素select1st
投射project:传回第一参数,忽略第二参数project1st
八、配接器
配接器:一个class的接口转换为另一个class的接口
使原本因接口不兼容而不能合作的classes可以一起合作
事实上是一种设计模式
改变仿函数接口者:function adapter
改变容器接口者:container adapter:如queue和stack,改变deque接口
改变迭代器接口者:iterator adapter:如reverse iterators,iostream iterators
#include<algorithm>
#include<functional>
#include<vector>
#include<iostream>
#include<iterator>
using namespace std;
int main() {
//将outite绑定到cout,每次对outite指派一个元素,就后接一个 " "
ostream_iterator<int>outite(cout, " ");
int ia[6] = { 2,21,12,7,19,23 };
vector<int>iv(ia, ia + 6);
copy(iv.begin(), iv.end(), outite);
cout << endl;
return 0;
}
只要双向序列容器提供了begin(),end(),如vector,list,deque
它的rbegin(),rend()就是上面那样的型式
单向序列slist不可使用reverse iterators,当然也没有rbegin(),rend()
有些容器stack,queue,priority_queue并不提供begin(),end(),也就没有rbegin(),rend()
当迭代器被逆转方向时,虽然其实体位置(真正的地址)不变
但其逻辑位置(迭代器所代表的元素)改变了(必须如此改变)
stream iterators:将迭代器绑定到一个stream数据流对象身上
容器:class templates
算法:function templates
迭代器:将operator++和operator*等指针习惯常行为重载的:class templates
配接器:class templates
mem_fun,mem_fun_ref:可以将成员函数当做仿函数使用
成员函数可以搭配各种泛型算法,当容器元素型式是X&或X*
以虚拟成员函数作为仿函数,便可以由泛型算法完成所谓的多态调用
这是泛型与多态之间的重要接轨
#include<algorithm>
#include<functional>
#include<vector>
#include<iostream>
using namespace std;
class Shape {
public:
virtual void display() = 0;
};
class Rect :public Shape {
public:
virtual void display() {
cout << " Rect";
}
};
class Circle :public Shape {
public:
virtual void display() {
cout << " Circle";
}
};
class Square :public Shape {
public:
virtual void display() {
cout << " Square";
}
};
int main() {
vector<Shape*>V;
V.push_back(new Rect);
V.push_back(new Circle);
V.push_back(new Square);
V.push_back(new Circle);
V.push_back(new Rect);
//polymorphically多态
for (int i = 0; i < V.size(); i++) {
V[i]->display();//Rect Circle Square Circle Rect
}
cout << endl;
//polymorphically多态
for_each(V.begin(), V.end(), mem_fun(&Shape::display));
cout << endl;//Rect Circle Square Circle Rect
return 0;
}