文章目录
引言
链表可谓是大二数据结构课上的万恶之源,因为其中涉及指针的操作!!😫,最后我数据结构80米有😭
链表非常像数组,只不过链表中各个元素的地址并不是连续的,而数组是,而且数组只能静态申请,也就是用之前需要规定数组长度,而链表不用,链表中元素内存可以随时开随时释放,这个特点很有用,c++的vector其实就是借助这个思想,只不过vector是一次性动态申请固定空间,不够之后再去申请额外的,而我们学的链表是一次申请一个。vector算是集合了链表和数组各自的特点,动态内存和连续寻址(打住)
在链表的基础上二叉树、二叉搜索树、平衡树、红黑树…等等高级数据结构,总之好好学!
链表
类型
链表根据有无头结点可以分为:带头结点链表, 不带头结点链表
根据尾节点是否指向首部节点分为:循环链表, 非循环链表
定义节点
我们说的定义是指定义单个节点:(写成类,规范点吧)
链表的节点分为值域和指针域其中指针域中的指针指向下一个节点的地址,当然我们初始化的时候指向nullptr,指向下一个节点地址是我们人为控制的
class Node{
#define element int //定义节点元素类型
public:
element val; //值
Node* next; //下一个节点
Node(element el){ //析构函数
this->val = el;
this->next = nullptr;
}
};
定义链表
我们吧链表也封装成为一个类,节点类作为链表类的一个属性(注意定义节点元素类型移动到了List类的定义中)
class List{
#define element int //定义节点元素类型
public:
private:
class Node{
public:
element val; //值
Node* next; //下一个节点
Node(element el){ //析构函数
this->val = el;
this->next = nullptr;
}
Node(){} //析构函数
};
Node* header = nullptr; //链表首部(注意要为空,当然也可以在List的析构函数中赋值空)
bool cmp(element a, element b); //声明比较函数
};
这里我们选择不带头结点的链表,感觉带一个空的头结点一点怪?虽然有时候会方便点,同样,非循环链表
通过List = list ;就成功创建一个链表了!当然也可以用new申请
元素插入
我们设定元素类型为int型,在这之前我们需要关注一下cmp函数,也就是比较大小,我们是希望大的元素优先还是小的优先呢?如果元素类型是一个自定义的结构体,那么应该如何比较呢?
那就设定一个比较函数,专门用来规定什么样的元素应该放前面,什么样的元素应该放后面,当然这个函数不能在类的内部写死了,应该交给用户自定义,我们在类的内部也只是声明而已。
bool List::cmp(int a, int b) {
return a < b; //这里是小的元素优先
// return a > b; //这里是大元素优先
}
这段代码应该定义在类外部
另外,更明智的做法是封装成一个容器,这样无论是元素类型还是比较函数都可以自定义,并且在一个cpp文件中可以有用于不同元素类型和比较函数的容器。这里我们暂不讨论。
元素插入分为:
- 首部节点(rt root)为空 或者 新元素小于首部元素值时,也就是在链表首部插入新元素为首部申请空间,修改该节点vaL;
- 在链表中间插入
- 在链表尾部插入
void insert(element el){
if(this->header == nullptr){ //根节点为空
Node* newNode = new Node(el);
this->header = newNode;
return;
}
if(cmp(el, this->header->val)){ //el优先级高
Node* newNode = new Node(el);
newNode->next = this->header;
this->header = newNode;
return;
}
Node *fa = this->header, *p = this->header;
while (p != nullptr && cmp(p->val, el)){ //找到第一个优先级小于el的节点
fa = p;
p = p->next;
}
Node* newNode = new Node(el);
fa->next = newNode;
newNode->next = p;
}
首先前两个if没什么问题,看while循环中,前面定义了两个指针fa, p;
更新的时候fa = p,然后p = p->next;
同样的起点,相当于fa延迟一步p更新,所以fa会是p的father节点
思考:如果需要寻找grandfather呢?
以在1 3 7 中插入5和9为例
-
插入5时,fa,p指针如下:
此时p指针指向的元素val优先级已经小于5了,创建一个新的节点值是5,fa指向5,5指向7
-
插入9时,fa ,p指针如下:
此时p指针指向null,说明新元素优先级最低,新元素插入到末尾,发现和插入中间是一样的,不多赘述
元素查找
判断一个元素是否在链表中,返回true/false,直接遍历整个链表,复杂度O(n)
bool isContain(element el){
Node* p = this->header;
while (p != nullptr){
if(p->val == el){
return true;
}
}
return false;
}
元素删除
遍历整个链表,设置fa,p指针,直到p遍历到nullptr或者p->val == el
需要特判首部, 节点不存在的情况
bool del(element el){
Node* fa = this->header, *p = this->header;
if(p->val == el){ //元素在首部
this->header = p->next;
free(p);
return true;
}
while (p != nullptr && p->val != el){
fa = p;
p = p->next;
}
if(p == nullptr){ //p如果等于nullptr说明链表中根本没有值为el的节点
if(fa->val )
free(fa);
return false;
}
//一般情况 和在尾部的情况
fa->next = p->next;
free(p);
return true;
}
元素修改
把链表中某元素修改为新值,如果旧值有多个只会修改一个,如果没有该旧值返回false
bool update(element from, element to){
Node* fa = this->header, *p = this->header;
if(p->val == from){
p->val = to;
return true;
}
while (p != nullptr && p->val != from){
fa = p;
p = p->next;
}
if(p == nullptr){ //说明旧值不存在
return false;
}
p->val = to;
return true;
}
完整code
class List{
#define element int //定义节点元素类型
public:
void insert(element el){
if(this->header == nullptr){ //根节点为空
Node* newNode = new Node(el);
this->header = newNode;
return;
}
if(cmp(el, this->header->val)){ //小于根节点
Node* newNode = new Node(el);
newNode->next = this->header;
this->header = newNode;
return;
}
Node *fa = this->header, *p = this->header;
while (p != nullptr && cmp(p->val, el)){ //找到第一个优先级小于el的节点
fa = p;
p = p->next;
}
Node* newNode = new Node(el);
fa->next = newNode;
newNode->next = p;
}
bool isContain(element el){
Node* p = this->header;
while (p != nullptr){
if(p->val == el){
return true;
}
}
return false;
}
bool del(element el){
Node* fa = this->header, *p = this->header;
if(p->val == el){ //元素在首部
this->header = p->next;
free(p);
return true;
}
while (p != nullptr && p->val != el){
fa = p;
p = p->next;
}
if(p == nullptr){ //p如果等于nullptr说明链表中根本没有值为el的节点
if(fa->val )
free(fa);
return false;
}
//一般情况 和在尾部的情况
fa->next = p->next;
free(p);
return true;
}
bool update(element from, element to){
Node* fa = this->header, *p = this->header;
if(p->val == from){
p->val = to;
return true;
}
while (p != nullptr && p->val != from){
fa = p;
p = p->next;
}
if(p == nullptr){ //说明旧值不存在
return false;
}
p->val = to;
return true;
}
void print(){
Node* p = this->header;
while (p != nullptr){
cout<<p->val<<" ";
p = p->next;
}
cout<<endl;
}
private:
class Node{
public:
element val; //值
Node* next; //下一个节点
Node(element el){ //析构函数
this->val = el;
this->next = nullptr;
}
Node(){} //析构函数
};
Node* header = nullptr; //链表首部
bool cmp(element a, element b);
};
bool List::cmp(int a, int b) {
return a < b; //这里是小的元素优先
// return a > b; //这里是大元素优先
}
int main(){
List list;
int n;cin>>n;
for(int i=1;i<=n;i++){
int w;
cin>>w;
list.insert(w);
}
list.print();
return 0;
}
二叉树
定义
上文中链表指针域只有一个next,指向下一个节点地址,而链表又名线性表,而如今二叉树指针域具有两个指针:lson,rson(左孩子右孩子,咱会简写为ls,rs)。一个节点最多拥有两个子节点。
因此一棵树大概会长这样:
在继续下去之前,我们得明白子树的概念,说白了一颗树,由节点本身 + 左子树 + 右子树 构成
当然子树也可以继续上述概念,也即是无论哪一个节点,都有本身 + 左子树 + 右子树 (当然,左右子树都可以为空)
当一个节点没有左右子树(左右节点)时,那么这个节点就叫做叶子节点
定义节点
和链表定义的节点类似,只不过next指针变成了ls,rs
class Node{
public:
element val; //值
Node* ls; //左孩子
Node* rs; //右孩子
Node(element el){ //析构函数
this->val = el;
this->ls = nullptr;
this->rs = nullptr;
}
Node(){} //析构函数
};
定义二叉树
class Tree{
#define element int //定义节点元素类型
public:
private:
class Node{
public:
element val; //值
Node* ls; //左孩子
Node* rs; //右孩子
Node(element el){ //析构函数
this->val = el;
this->ls = nullptr;
this->rs = nullptr;
}
Node(){} //析构函数
};
Node* rt = nullptr; //根节点
bool cmp(element a, element b);
};
遍历
不同于顺序表,从头走到尾,二叉树有左子树和右子树和本身,到底怎么走?
为此有了四种不同的遍历方式:
- 先序遍历:先遍历本身 , 再遍历左子树 , 最后遍历右子树
- 中序遍历:先遍历左子树 , 再遍历本身 , 最后遍历右子树
- 后序遍历:先遍历左子树 , 再遍历右子树 , 最后遍历本身
- 层序遍历:根据记录根节点的距离,若距离一样,从左往右遍历
先中后序遍历
以上图为例,进行先序遍历:规则是:
- 先遍历本身 , 再遍历左子树 , 最后遍历右子树
- 一直进行下直到该节点没有左右子树为止。
那么是: 1,左子树 ,右子树
左子树还是一棵树也套用上述规则:1 ,(3 ,左子树 , 右子树), (右子树)
那么最后是:1 3 4 5 8 6 10 7 0 9 14 11
那么代码应该长什么样子呢?,假设我们有个函数专门用来打印当前树(节点)的先序遍历将这个函数写为:print(Node* now);
那么他内部应该长这样:
void print(Node* now){
if(now == nullptr){ //该节点(树)没有左右子树(节点)了,停止打印
return ;
}
cout<<now->val<<" "; //输出当前节点值
print(now->ls);
print(now->rs);
}
因为这个函数的功能就是打印当前树(节点)的先序遍历,当我要打印该节点的左右子树时,当然也可以调用这个函数本身。这其实就是递归了,不理解的好好复习c语言去🐸
那么中序遍历和后序遍历其实长得差不多,就是把cout<val<<" ";放两个print中间, 后面的区别,那么我们干脆一个代码写出来,再额外传递一个cmd变量控制三种方式选一种
void print(Node* now, int cmd){
if(now == nullptr){ //该节点(树)没有左右子树(节点)了,停止打印
return ;
}
if(cmd == 1) cout<<now->val<<" "; //先序
print(now->ls,cmd);
if(cmd == 1) cout<<now->val<<" "; //中序
print(now->rs,cmd);
if(cmd == 1) cout<<now->val<<" "; //后序
}
层序遍历
回顾层序遍历的规则:优先输出距离根节点近的节点,若果一样近,从左往右输出
那么采用队列,先根节点入队,输入根节点之后,在左右节点入队,如此进行下去即可完成先序遍历
void bfs(){
queue<Node*> queue;
queue.push(this->rt);
while (!queue.empty()){
Node* front = queue.front();queue.pop();
cout<<front->val<<" ";
if(front->ls != nullptr) queue.push(front->ls);
if(front->rs != nullptr) queue.push(front->rs);
}
}
获取节点高度
当前节点高度 = max(左节点高度 , 右节点高度) + 1
int getHeight(Node* now){
if (now == nullptr) return 0; //null节点高度为0
return max(getHeight(now->ls), getHeight(now->rs)) + 1;
}
这里每次获取都要递归求一次,如果在不更新树的情况下,可以选择用一个在Node的数据域直接储存计算好的高度,这个效率更好
如果更新呢?那么需要记录树根节点到更新节点的所有节点,这些节点都需要更新高度?这个我们在平衡树中会讲到,算留个坑😉
判断是否完全二叉树
其他
例如判断某个节点是否是叶子节点,某个节点是否是单分支节点,某个节点包括所以子节点的个数…等等,自己加油尝试一下吧!
end
链表 和 二叉树 基本没什么好说的,算是引进门的玩意,但练习是需要好好连的,为之后的搜索树,平衡树,红黑树等等树打基础
练习题地址:P1305 新二叉树 - 洛谷考察建树和遍历P4913 二叉树深度 - 洛谷
二叉树的建立不一定要用指针形式来,也可以用数组,vector,也可以考虑建图用的链式前向星
数据域直接储存计算好的高度,这个效率更好
如果更新呢?那么需要记录树根节点到更新节点的所有节点,这些节点都需要更新高度?这个我们在平衡树中会讲到,算留个坑😉
判断是否完全二叉树
其他
例如判断某个节点是否是叶子节点,某个节点是否是单分支节点,某个节点包括所以子节点的个数…等等,自己加油尝试一下吧!
end
链表 和 二叉树 基本没什么好说的,算是引进门的玩意,但练习是需要好好连的,为之后的搜索树,平衡树,红黑树等等树打基础
练习题地址:P1305 新二叉树 - 洛谷考察建树和遍历P4913 二叉树深度 - 洛谷
二叉树的建立不一定要用指针形式来,也可以用数组,vector,也可以考虑建图用的链式前向星
感谢看到最后😀😀