STL(Standard Template Library),即标准模板库,是一个具有工业强度的,高效的C++程序库。
以上图片来自
C++中的容器分为顺序容器和关联容器,所有容器都是类模板
使用模板可以编写一个类定义或函数定义,用于多个不同的数据类型。如vector 中T可为int,char,float,string等类型
顺序容器主要有以下几类
vector(动态数组)支持快速随机访问
- vector有内存管理的机制,对于插入和删除,vector可以动态调整所占用的内存空间。(经历另寻更大空间,将原数据复制过去,释放原空间三部曲。对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了。)
- 数据结构:线性连续空间
- 维护三个迭代器:start、finish、end_of_storage
- 尾部插入和删除,时间复杂度为o(1),头部插入和删除代价大
- vector和deque容器支持通过元素位置实现随机访问,其迭代器支持算术运算(如iter+n,iter-n)还支持关系运算(<=,<,>=,>。list容器的迭代器既不支持算术运算也不支持关系运算,它只提供前置和后置的自增、自减运算。使用一对迭代器标记迭代器范围,这两个迭代器分别指向同一个容器中的两个元素或超出末端的下一位置,通常将它们命名为first和last,或beg和end,用于标记容器中的一段元素范围。last其实指向的是最后一个元素的下一位置。
由于vector内部使用动态数组的方式实现的。如果动态数组的内存不够用,就要动态的重新分配,一般是当前大小的两倍,然后把原数组的内容拷贝过去。所以,在一般情况下,其访问速度与一般数组一样,只有在重新分配发生时,其性能才会下降。注意vector的size()和capacity()是不同的,前者表示数组中元素的多少,后者表示数组有多大的容量。由上面的分析可以看出,使用vector的时候需要注意内存的使用,如果频繁地进行内存的重新分配,会导致效率低下。它的内部使用allocator类进行内存管理,程序员不需要自己操作内存。
vector其中一个特点:内存空间只会增长,不会减小,援引C++ Primer:为了支持快速的随机访问,vector容器的元素以连续方式存放,每一个元素都紧挨着前一个元素存储。设想一下,当vector添加一个元素时,为了满足连续存放这个特性,都需要重新分配空间、拷贝元素、撤销旧空间,这样性能难以接受。因此STL实现者在对vector进行内存分配时,其实际分配的容量要比当前所需的空间多一些。就是说,vector容器预留了一些额外的存储区,用于存放新添加的元素,这样就不必为每个新元素重新分配整个容器的内存空间。
C++ STL 之 vector 的 capacity 和 size 属性区别
size 是当前 vector 容器真实占用的大小,也就是容器当前拥有多少个容器。
capacity 是指在发生 realloc 前能允许的最大元素数,即预分配的内存空间。
当然,这两个属性分别对应两个方法:resize() 和 reserve()。
使用 resize() 容器内的对象内存空间是真正存在的。
使用 reserve() 仅仅只是修改了 capacity 的值,容器内的对象并没有真实的内存空间(空间是"野"的)。
此时切记使用 [] 操作符访问容器内的对象,很可能出现数组越界的问题。
下面用例子进行说明:
#include <iostream>
#include <vector>
using std::vector;
int main(void)
{
vector<int> v;
std::cout<<"v.size() == " << v.size() << " v.capacity() = " << v.capacity() << std::endl;
//v.size() == 0 v.capacity() =0 初始化容器没有对象也没有预留内存空间
v.reserve(10);
std::cout<<"v.size() == " << v.size() << " v.capacity() = " << v.capacity() << std::endl;
//v.size() == 0 v.capacity() =10 预留10个对象的空间,但容器内并没有对象,直接[]访问会报越界错误
v.resize(10);
v.push_back(0);
std::cout<<"v.size() == " << v.size() << " v.capacity() = " << v.capacity() << std::endl;
//v.size() == 11 v.capacity() =15 size=capacity后再push一个对象要重新分配一部分内存空间,重新分配的大小对不同 的库实现不同(这里是增加原来大小的一半)
return 0;
}
针对 capacity 这个属性,STL 中的其他容器,如 list map set deque,由于这些容器的内存是散列分布的,因此不会发生类似 realloc() 的调用情况,因此设计时这些容器没有该属性。在 STL 中,拥有 capacity 属性的容器只有 vector 和 string
deque 双端队列
一种双向开口的连续线性空间(动态将多个连续空间通过指针数组接合在一起),每段数据空间内部是连续的,而每段数据空间之间则不一定连续
deque与vector的最大差异 :
- 允许常数时间内对起首端进行元素的插入和删除
- deque没有所谓的容量概念,因为它是以分段连续空间组合而成,随时可以增加一段新的空间并连接起来
上图deque的数据空间即(缓冲区)都是程序运行过程中在堆上动态分配的。
中控器(map)保存着一组指针(每个指针指向另一段(较大的)连续线性空间(缓冲区)的起始位置,缓冲区才是deque的储存空间主体),通过中控器可以找到所有的缓冲区。如果中控器的数据空间满了,会重新申请一块更大的空间,并将中控器的所有指针拷贝到新空间中。
deque的迭代器由四个属性组成,这四个属性是我们随机访问一个容器内元素的必要成分。 - node用于指向的“第一维”(中控器(map))的某个位置,该位置存放要访问元素的入口地址
- 剩下的三个属性则用于“第二维”,[first,last)规定了访问本区间元素的边界条件,而curr则指向实际我们要访问的元素。
deque的图像化描述可参考
list 支持快速插入/删除
- 有效利用空间
- 数据结构:环状双向链表
- 插入(insert)和接合(splice)操作都不会造成原来list的迭代器失效
- 删除(erase)操作仅仅使“指向被删除元素”的迭代器失效,其它迭代器不受影响
- 随机访问比较慢,它不支持根据下标随机存取元素。
用 list 解决约瑟夫问题
约瑟夫问题是:有 n 只猴子,按顺时针方向围成一圈选大王(编号为 1~n),从第 1 号开始报数,一直数到 m,数到 m 的猴子退到圈外,剩下的猴子再接着从 1 开始报数。就这样,直到圈内只剩下一只猴子时,这个猴子就是猴王。编程求输入 n、m 后,输出最后猴王的编号。
输入数据:每行是用空格分开的两个整数,第一个是 n,第二个是 m(0<m, n<=1 000 000)。最后一行是:
0 0
输出要求:对于每行输入数据(最后一行除外),输出数据也是一行,即最后猴王的编号。
输入样例:
6 2
12 4
8 3
0 0
输出样例:
5
1
7
程序如下:
#include <list>
#include <iostream>
using namespace std;
int main()
{
list<int> monkeys;
int n, m;
while (true) {
cin >> n >> m;
if (n == 0 && m == 0)
break;
monkeys.clear(); //清空list容器
for (int i = 1; i <= n; ++i) //将猴子的编号放入list
monkeys.push_back(i);
list<int>::iterator it = monkeys.begin();
while (monkeys.size() > 1) { //只要还有不止一只猴子,就要找一只猴子让其出列
for (int i = 1; i < m; ++i) { //报数
++it;
if (it == monkeys.end())
it = monkeys.begin();
}
it = monkeys.erase(it); //删除元素后,迭代器失效,要重新让迭代器指向被删元素的后面
if (it == monkeys.end())
it = monkeys.begin();
}
cout << monkeys.front() << endl; //front返回第一个元素的引用
}
return 0;
}
顺序容器对外接口主要可以分为:
构造、析构
C<Elem> c //创建名为c的空容器。C是容器类型名
C<Elem> c1(c2) //创建容器c2的副本c1,c1和c2必须具有相同的容器类型,并存放相同类型的元素
C<Elem> c(n) //创建有n个值初始化元素的容器c
C<Elem> c(n, elem) //用n个值为elem的元素创建容器c
C<Elem> c(beg,end) //创建c,其元素是beg和end标示的范围内元素的副本
c.~ vector <Elem>() //析构函数
插入、删除、赋值
c.push_back(elem) //容器c尾部添加值为elem的元素
c.pop_back() //尾删
c.pop_front() //头删 **只适用于list或deque容器**
c.insert(pos,elem) //在迭代器pos所指向的元素前面插入值为elem的新元素
c.insert(pos,n,elem) //在迭代器pos所指向的元素前面插入n个值为elem的新元素
c.insert(pos,beg,end) //在迭代器pos所指向的元素前面插入由迭代器beg和end标记的范围内的元素
c.erase(pos) //删除迭代器pos所指向的元素
c.erase(beg,end) //删除由迭代器beg和end标记的范围内的元素,返回一个迭代器,指向被删除元素段后面的元素
c.clear() //删除容器c中所有的元素
c1=c2 //删除容器c1的所有元素,然后将c2的所有元素复值给c1。c1和c2类型必须相同
c1.swap(c2) //交换内容:调用完该函数后,c1中存放的是c2原来的元素,c2中存放的是c1原来的元素。c1和c2类型必须相同。swap的执行速度通常比将c2的所有元素复值给c1快
c.assign(beg,end) //重新设置c中元素,将迭代器beg和end标记的范围内的元素复制到c中,**beg和end必须不是指向c中元素的迭代器**
c.assign(n,elem) //将容器c重新设置为存储n个值为elem的元素
大小相关
c.capacity() //容器c的容量
c.max_size() //返回容器c可容纳的最多元素个数
c.resize(num) //调整容器c的长度,使其能容纳n个元素()
c.reserve() //只是修改了 capacity 的值,容器内的对象并没有真实的内存空间(用来设置容器大小但是并不初始化)
c.size() //容器c中元素个数
获取迭代器
c.begin() //返回一个迭代器,指向容器c的第一个元素
c.end() //返回一个迭代器,指向容器c的最后一个元素的下一位置
c.rbegin() //返回一个逆序迭代器,指向容器c的最后一个元素
c.rend() //返回一个逆序迭代器,指向容器c的第一个元素的前面的位置
获取数据
c[n] //返回下标为n的元素的引用,该操作仅适用于vector和deque容器
c.at(idx) //返回下标为n的元素的引用,该操作仅适用于vector和deque容器
c.front() //返回容器c第一个元素的引用
c.back() //返回容器c最后一个元素的引用
顺序容器适配器(adaptor)
adaptor原意是插座、适配器、接合器的意思。适配器模式的基本思想:将一个类的接口转换成客户希望的另外一个接口。Adaptor模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。也就是说,在一个类的接口上提供一个“插座”类,使它变成你希望使用的接口。
stack
当需要一个栈结构,可以用deque、vector或list容器来模拟,只在一端进行元素插入和弹出,另一端不动。
具体以deuqe容器为例,它不能直接作为一个stack,因为你不能防止别人在另一端乱动你的东西。需要对它进行一些包装,作一些限制,使之只能在一端进行插入和删除。所以必须提供一个“插座”,这个“插座”一端插在deque上,另一端插在你的程序中,你就可以使用栈结构了。而stack就是这样的“插座”,它连接了deque和你的程序。表面上看你使用的是stack,实际上你是通过stack这个“插座”来使用deque(因为stack完全是用deque来实现的,它并没有任何其他的东西,它只是在deque上面作了一层包装,相当于一个“插座”的功能)。
其支持的操作:
s.empty()
s.size()
s.pop()
s.top()
s.push(item)
queue
默认是基于deque容器实现的,其关联的基础容器必须提供push_front,可建立在list容器上。
priority_queue
要求提供随机访问功能,可建立在vector或deque容器上。
队列和优先队列支持的操作:
q.empty()
q.size()
q.pop()
q.push()
q.front() //返回队首元素
q.back()
q.top() //返回具有最高优先级的元素值
关联容器
通过键(key)存储和读取元素
map
- 结构是按键值自动排序红黑树,所有可以根据key值快速查找记录,查找的复杂度基本是Log(N)。
- 快速插入Key -Value 记录。
- 快速删除记录
- 根据Key 修改value记录。
- 遍历所有记录。
- map中的键值不能重复。
map<K,T> 类模板定义在 map 文件头中,它定义了一个保存 T 类型对象的 map,每个 T 类型的对象都有一个关联的 K 类型的键。容器内对象的位置是通过比较键决定的。可以用适当的键值从 map 容器中检索对象。图 1 展示了一个用名称作为键的 map<K,T> 容器,对象是整数值,用来表示年龄。
通过比较 secondname 来确定对象的顺序
不要因为 map 使用 less 对元素排序就被误导,这些元素并没有被组织成一个简单的有序序列,STL map 容器对元素的组织方式并没有具体要求,但元素一般都会保存在一个平衡二叉树中。容器中的元素被组织成一个平衡二叉树,因而树的高度——即根节点到叶节点之间的高度是最低的。如果每个节点的左子树和右子树的高度差不超过 1,那么可以说这棵二叉树就是平衡的。图 2 展示了图 1 所表示的 map 容器可能的平衡二叉树。
图 2 所示的树有 3 层,所以从根节点开始,找到任意的元素最多需要 3 步。这里选择的根节点可以使树的高度最小,而且对于每个父节点来说,它的键值大于它的左子节点,但小于它的右子节点。为了保持二叉树的平衡,当添加一个新的元素时,可能会导致根节点发生改变。所以显然,在添加新元素时,为了保持树的平衡,会产生一些额外的开销。作为回报,容器中的元素越多,相对于线性排列和非平衡树,平衡树组织元素的效率也越高。从包含 n 个元素的平衡二叉树中检索一个随机元素所需的时间为 O(log2n),从序列中检索元素所需的时间为 O(n)。
在map中插入元素
map<int,string> enumMap;
改变map中的条目非常简单,因为map类已经对[]操作符进行了重载
enumMap[1] =“One”;
enumMap[2] =“Two”;
这样非常直观,但存在一个性能的问题。插入2时,先在enumMap中查找主键为2的项,没发现,然后将一个新的对象插入enumMap,键是2,值是一个空字符串,插入完成后,将字符串赋为"Two";该方法会将每个值都赋为缺省值,然后再赋为显示的值,如果元素是类对象,则开销比较大。
我们可以用以下方法来避免开销:
enumMap.insert(map<int,CString> :: value_type(2,"Two"))
enumMap.insert(pair<int,string>(102,"aclive"));
auto pr = make_pair(22,"Fred");
auto ret_pr = enumMap.insert(pr);
cout << ret_pr.first->first << " "<< ret_pr.first->second<< "" << std:: boolalpha <<ret_pr.second << "\n"; // Fred 22 true
auto ret_pr1 = enumMap.insert(make_pair(22, "Bill");
cout << ret_pr1.first->first <<" "<<ret_pr1.first->second<< " "<<std::boolalpha<<ret_pr1.second << "\n"; // 22 Fred false
insert后面的数据是pair类型或者是value_type类型,还可以使用make_pair产生的pair<k,v>。其实在insert插入的同时,会返回一个pair<iterator,bool> 对象。对象的成员 first 是一个迭代器,它要么指向插入元素,要么指向阻止插入的元素。如果 map 中已经保存了一个和这个键相同的对象,就会指向阻止插入的元素。这个对象的成员变量 second (布尔型)是返回对象,如果插入成功,返回值为 true,否则为 false。如本实例ret_pr就是插入后返回的pair对象。通过ret_pr1这个返回值可以看到当键值已经存在时,插入后的返回值为false。所以当元素已经存在时,如果想将键值22对应的名字改为"Bill",可以像下面这样使用 insert() 返回的 pair 对象来做到这一点:
if(!ret_pr1.second) // 如果第二个返回值是false,证明新插入的键值是已存在的
ret_pr.first—>second = "Bill";
//当键已经存在于 map 容器中时,ret_pr 的成员变量 second为false,所以这段代码会将 map 中这个元素的成员变量 second 的值设为 "Bill"。
但是用数组方式就不同了,它可以覆盖以前该关键字对应的值。
查找并获取map中的元素
下标操作符给出了获得一个值的最简单方法:
string tmp =enumMap[2];
但是,只有当map中有这个键的实例时才对,否则会自动插入一个实例,值为初始化值。
我们可以使用Find()和Count()方法来发现一个键是否存在。
查找map中是否包含某个关键字条目用find()方法,传入的参数是要查找的key,在这里需要提到的是begin()和end()两个成员,分别代表map对象中第一个条目和最后一个条目,这两个数据的类型是iterator.
int nFindKey= 2; //要查找的Key
//定义一个条目变量(实际是指针)
map<int,string>::iterator it=enumMap.find(nFindKey);
if(it!= enumMap.end()) {
cout<<"Find, the value is "<<iter->second<<endl;
}
else {
cout<<"Do not Find"<<endl;
}
通过map对象的方法获取的iterator数据类型是一个std::pair对象,包括两个数据 iterator->first和 iterator->second分别代表关键字和存储的数据
从map中删除元素
移除某个map中某个条目用erase()
该成员方法的定义如下:
iterator erase(iterator it);//通过一个条目对象删除
iterator erase(iterator first,iterator last)//删除一个范围
size_type erase(const Key&key);//通过关键字删除
clear()就相当于enumMap.erase(enumMap.begin(),enumMap.end());
map中的swap用法
Map中的swap不是一个容器中的元素交换,而是两个容器的交换;
map中的sort问题
Map中的元素是自动按Key升序排序,所以不能对map用sort函数;
map的基本操作函数:
begin() 返回指向map头部的迭代器
clear() 删除所有元素
empty() 如果map为空则返回true
end() 返回指向map末尾的迭代器
equal_range() 返回特殊条目的迭代器对
erase() 删除一个元素
find() 在容器中按关键字查找,返回其迭代器;如果找不到,返回 end()
get_allocator() 返回map的配置器
insert() 插入元素
key_comp() 返回比较元素key的函数
lower_bound() 返回键值>=给定val的第一个位置
max_size() 返回可以容纳的最大元素个数
rbegin() 返回一个指向map尾部的逆向迭代器
rend() 返回一个指向map头部的逆向迭代器
size() 返回map中元素的个数
swap() 交换两个map
upper_bound() 返回键值>给定val的第一个位置
value_comp() 返回比较元素value的函数
multimap
- 大小可变的关联容器,支持基于关联键值高效检索元素值。
- 可逆,因为它提供双向迭代器来访问其元素。
- 有序,因为它的元素在容器中根据指定的比较函数按键值排序。
- 多个,它的元素不需要具有唯一键,因此一个键值可具有多个相关联的元素数据值,键值key与元素value的映照关系是多对多的关系
- 没有定义[]操作运算
- count 函数求出某键出现的次数,find 操作返回一个迭代器,指向第一个拥有正在查找的键的实例
- 查找元素
1)使用find和count操作
2)equal_range函数
3)lower_bound 和upper_bound函数
multimap<string, int> vecE;
vecE.insert(make_pair("china", 1));
vecE.insert(make_pair("china", 1));//允许插入
vecE.insert(make_pair("china", 3));//允许插入
vecE.insert(make_pair("china", 4));//允许插入
vecE.insert(make_pair("china", 5));//允许插入
vecE.insert(make_pair("english", 1));
vecE.insert(make_pair("english", 2));//允许插入
vecE.insert(make_pair("america", 1));
vecE.insert(make_pair("america", 2));//允许插入
vecE.insert(make_pair("america", 3));//允许插入
cout << "multimap 初始化" << endl;
//查找区间
multimap<string, int> ::iterator it1 = vecE.lower_bound("china"); //指向vecD中第一个等于键值 “china”对应的元素
multimap<string, int> ::iterator it2 = vecE.upper_bound("china"); //指向vecD中第一个大于键值 “china”对应的元素
while(it1!=it2){
cout << it1->first << " " << it1->second << endl;
++it1;
}
// 等于 = lower_bound + upper_bound
pair<multimap<string, int>::iterator, multimap<string, int>::iterator > it3 = vecE.equal_range("china");
map<string, int>::iterator it4 = it3.first;
map<string, int>::iterator it5 = it3.second;
while(it4!=it5){//pair的第一个成员不等于第二个成员,即两个迭代器不等
cout<<it4->second<<endl;
++it4;
}
//查找key = “china”键值对的个数
int iCount = vecE.count("china");
//查找key = “china”对应键值对
multimap<string, int>::iterator it6 = vecE.find("china");
unordered_map
C++ 11标准中加入了unordered系列的容器。unordered_map记录元素的hash值,根据hash值判断元素是否相同。
map相当于java中的TreeMap,unordered_map相当于HashMap。
无论从查找、插入上来说,unordered_map的效率都优于hash_map,更优于map;而空间复杂度方面,hash_map最低,unordered_map次之,map最大。
unordered_map与map的对比:
存储时是根据key的hash值判断元素是否相同,即unordered_map内部元素是无序的,而map中的元素是按照二叉搜索树存储(用红黑树实现),进行中序遍历会得到有序遍历。所以使用时map的key需要定义operator<。而unordered_map需要定义hash_value函数并且重载operator==。但是很多系统内置的数据类型都自带这些。
总结:结构体用map重载<运算符,结构体用unordered_map重载==运算符。
unordered_map与hash_map对比:
unordered_map原来属于boost分支和std::tr1中,而hash_map属于非标准容器。
unordered_map感觉速度和hash_map差不多,但是支持string做key,也可以使用复杂的对象作为key。
unordered_map编译时gxx需要添加编译选项:–std=c++11
//unordered_map模板:
template < class Key, // unordered_map::key_type
class T, // unordered_map::mapped_type
class Hash = hash<Key>, // unordered_map::hasher
class Pred = equal_to<Key>, // unordered_map::key_equal
class Alloc = allocator< pair<const Key,T> > // unordered_map::allocator_type
> class unordered_map;
//迭代器:unordered_map的迭代器是一个指针,指向这个元素,通过迭代器来取得它的值。
unordered_map<Key,T>::iterator it;
(*it).first; // the key value (of type Key)
(*it).second; // the mapped value (of type T)
(*it); // the "element value" (of type pair<const Key,T>)
//它的键值分别是迭代器的first和second属性。
it->first; // same as (*it).first (the key value)
it->second; // same as (*it).second (the mapped value)
//成员函数:
=================迭代器=========================
begin //返回指向容器起始位置的迭代器(iterator)
end //返回指向容器末尾位置的迭代器
cbegin // 返回指向容器起始位置的常迭代器(const_iterator)
cend // 返回指向容器末尾位置的常迭代器
=================Capacity================
size // 返回有效元素个数
max_size //返回 unordered_map 支持的最大元素个数
empty // 判断是否为空
=================元素访问=================
operator[] // 访问元素
at // 访问元素
=================元素修改=================
insert //插入元素
erase //删除元素
swap //交换内容
clear //清空内容
emplace //构造及插入一个元素
emplace_hint //按提示构造及插入一个元素
================操作=========================
find //通过给定主键查找元素,没找到:返回unordered_map::end
count //返回匹配给定主键的元素的个数
equal_range //返回值匹配给定搜索值的元素组成的范围
================Buckets======================
bucket_count //返回槽(Bucket)数
max_bucket_count //返回最大槽数
bucket_size // 返回槽大小
bucket //返回元素所在槽的序号
load_factor //返回载入因子,即一个元素槽(Bucket)的最大元素数
max_load_factor // 返回或设置最大载入因子
rehash //设置槽数
reserve // 请求改变容器容量
set
- 所有元素都会根据元素的键值自动排序
- set的元素不像map那样可以同时拥有实值(value)和键值(key),set元素的键值就是实值,实值就是键值。
- set不允许两个元素有相同的键值。
插入操作
set<int> s;
s.insert(1);
s.insert(2);
//或者使用数组插入
int arr[5] = {0,1,2,3,4};
set<int> iset(arr,arr+5);
其它操作
s.begin() // 返回指向第一个元素的迭代器
s.clear() // 清除所有元素
s.count() // 返回某个值元素的个数
s.empty() // 如果集合为空,返回true(真)
s.end() // 返回指向最后一个元素之后的迭代器,不是最后一个元素
s.equal_range() // 返回集合中与给定值相等的上下限的两个迭代器
s.erase() // 删除集合中的元素
s.find() // 返回一个指向被查找到元素的迭代器
s.get_allocator() // 返回集合的分配器
s.lower_bound() // 返回指向大于(或等于)某值的第一个元素的迭代器
s.key_comp() // 返回一个用于元素间值比较的函数
s.max_size() // 返回集合能容纳的元素的最大限值
s.rbegin() // 返回指向集合中最后一个元素的反向迭代器
s.rend() // 返回指向集合中第一个元素的反向迭代器
s.size() // 集合中元素的数目
s.swap() // 交换两个集合变量
s.upper_bound() // 返回大于某个值元素的迭代器
s.value_comp() // 返回一个用于比较元素间的值的函
multiset
set中不允许有重复元素,multiset中允许有重复元素。
各个容器的使用时机
为何map和set的插入删除效率比用其他序列容器高?
因为对于关联容器来说,不需要做内存拷贝和内存移动。set容器内所有元素都是以节点的方式来存储,其节点结构和链表差不多,指向父节点和子节点。结构图可能如下:
A
/
B C
/ \ /
D E F G
因此插入的时候只需要稍做变换,把节点的指针指向新的节点就可以了。删除的时候类似,稍做变换后把指向删除节点的指针指向其他节点也OK了。这里的一切操作就是指针换来换去,和内存移动没有关系。