第九章 数据结构专题
-
树与二叉树
-
概念
- 子树的棵树称为结点的度。
-
完全二叉树
-
判断是否是完全二叉树可以用结点下标的关系
-
代码模板
//层序遍历中,如果一个结点没有左孩子,那么这个结点编号 * 2 > n,如果不满足这个条件,就说明不是完全二叉树;右孩子的情况同理。 bool isCom = true; void LayerOrder(int root){ queue<node> q; q.push(tree[root]); while(!q.empty()){ node now = q.front(); q.pop(); cnt++; maxid = now.id; if(now.left != -1){ q.push(tree[now.left]); }else{//判断是否是完全二叉树 if(cnt * 2 <= n){ isCom = false; } } if(now.right != -1)q.push(tree[now.right]); else{ if(cnt * 2 + 1 <= n)isCom = false; } } }
-
-
二叉树的存储结构与基本操作
-
二叉链表
struct node{ typename data; node* lchild; node* rchild; }; //生成一个新结点,v为结点权值 node* newNode(int v){ node* Node = new node; Node->data = v; Node->lchild = Node->rchild = NULL; return Node; }
-
-
二叉树结点的查找、修改
void search(node* root, int x, int newdata){ if(root == NULL){ return; //空树,返回 } if(root->data == x){//找到结点值为x的结点,修改 root->data = newdata; } search(root->lchild, x, newdata); search(root->rchild, x, newdata); }
-
二叉树结点的插入
- 引用的使用:如果是函数中需要新建结点,即对二叉树的结构做出修改,就需要用引用;如果只是修改结点的内容,或者仅仅是遍历二叉树,就不用加引用。
//inseart插入一个数据域为x的新结点 //注意root要使用引用,否则会插入失败 void insert(node* &root, int x){ if(root == NULL){ root = newNode(x); //newNode(x)函数见上面 return; } if(二叉树的性质,选择左子树还是右子树){ insert(root->lchild, x) }else{ insert(root->rchild, x) } }
-
二叉树的创建
//二叉树的创建就是把二叉树结点依次插入的过程 node* Create(int data[], int n){ node* root = NULL;//新建空的根结点 for(int i = 0; i < n; i++){ insert(root, data[i]); } return root; }
-
二叉树的遍历
-
//先序遍历:根-左-右 void preorder(node* root){ if(root == NULL)return; printf("%d",root->data); preorder(root->lchild); preorder(root->rchild);
}
//中序遍历:左-根-右
void inorder(node* root){
if(root == NULL)return;
inorder(root->lchild);
printf("%d", root->data);
inorder(root->rchild);
}//后序遍历:左-右-根
void postorder(node* root){
if(root == NULL)return;
postorder(root->lchild);
postorder(root->rchild);
printf("%d", root->data);
}//层序遍历:按层次
//思路:根结点入队->队不空,出队,判断此结点的左右子树是否为空,不为空则入队
void LayerOrder(node* root){
queue<node*> q;//注意队列中存储的是地址
q.push(root);
while(!q.empty()){
node* now = q.front();//注意此处是node*,不是node;如果是node,队列中保存的是原元素的副本,改变此元素将不会改变在队列中的值;故使用node*对地址进行修改
q.pop();
printf("%d", now->data);
if(now->lchild != NULL)q.push(now->lchild);
if(now->rchild != NULL) q.push(now->rchild);
}
}- 很多题目中要求计算出每个结点所处于的层次,这时需要在二叉树结点的定义中添加一个记录层次layer的变量 ```c++ struct node{ int data; int layer; node* lchild; node* rchild; }; //层序遍历 void LayerOrder(node* root){ queue<node*> q; //注意队列当中是存储地址 root->layer = 1;//根结点的层为1 q.push(root);//根结点入队 while(!q.empty()){ node* now = q.front(); q.pop(); printf("%d", now->data); if(now->lchild != NULL){ now->lchild->layer = now->layer + 1;//层号加1 q.push(now->lchild); } if(now->rchild != NULL){ now->rchild->layer = now->layer + 1; q.push(now->rchild); } } }
-
给定二叉树的先序遍历序列和中序遍历序列或者后序和中序序列或者层序序列和中序序列,重建这棵二叉树
//当前先序序列为[preL,preR],中序序列为[inL, inR],返回根结点地址 node* create(int preL, int preR, int inL, int inR){ if(preL > preR){ return NULL;//先序序列长度小于等于0,直接返回 } node* root = new node; root->data = pre[preL];//新结点的数据域为根结点的值 //寻找根结点在中序序列中位置 int k; for(k = inL; k <= inR; k++){ if(in[k] == pre[preL]){ break; } } int numLeft = k - inL;//左子树结点的个数 root->lchild = create(preL+1, preL+numLeft, inL, k-1); root->rchild = create(preL+numLeft+1,preR,k+1,inR); return root; } //=====中序序列和后序序列构建二叉树======== node* create(int postL, int postR, int inL, int inR){ if(postL > postR)return NULL; node* root = new node; root->data = post[postR];//找到根结点,赋值 int k; for(k = inL; k <= inR; k++){ if(in[k] == post[postR]){ break; } } int numLeft = k - inL; root->lchild = create(postL, postL+numLeft-1, inL, k-1); root->rchild = create(postL+numLeft, postR-1, k+1, inR); return root; } //=========层序序列和中序序列=========== node* create(int levelL, int levelR, int inL, int inR){ //第一步,确定递归边界 if(inL > inR)return NULL; //第二步,找到当前层序序列中 第一个 出现在当前中序序列中的元素,作为根节点 node* root = new ndoe; int k, flag = 0; for(; leveL <= levelR; ++leveL){ for(k = inL; k <= inR; k++){ if(in[k] == level[levelL]){ flag = 1;//找到 break; } } if(flag)break; } root->data = level[levelL]; //第三步,确定左右子树 root->lchild = create(levelL+1, levelR, inL, k-1); root->rchild = create(levelL+1, levelR, k+1, inR); return root; }
-
-
-
树的遍历
-
树的静态写法
struct node{ typename data; vector child; }Node[maxn]; //新建结点 int index = 0; int newNode(int v){ Node[index].data = v; Node[index].child.clear();//清空子结点 return index++; }
-
树的遍历
//树的先根遍历 void preOrder(int root){ printf("%d", Node[root].data); for(int i = 0; i < Node[root].child.size(); i++){ preOrder(Node[root].child[i]); } } //树的层序遍历 void LayerOrder(int root){ queue<int> q; q.push(root); while(!q.empty()){ int front = q.front(); printf("%d ", Node[front].data); q.pop(); for(int i = 0; i < Node[front].child.size(); i++){ q.push(Node[front].child[i]); } } }
-
如果需要对结点的层号进行求解,只需要在结构体node中增加层号
struct node{ int layer; int data; vector<int> child; }Node[maxn]; void LayerOrder(int root){ queue<int> q; q.push(root); Node[root].layer = 0;//记录根节点称号为0 while(!q.empty()){ int front = q.front(); printf("%d ", Node[front].data); q.pop(); for(int i = 0; i < Node[front].child.size(); i++){ Node[Node[front].child[i]].layer = Node[front].layer + 1; q.push(Node[front].child[i]); } } }
-
二叉查找树
-
左子树上的结点都小于或者等于结点值;右子树上的结点都大于结点 值;如果题目没有给出明确定义,那么按照这个定义来解;如果题目给出了定义,那么按照题目给出的来解。
-
查找操作,复杂度O(h),h是二叉查找树的高度
//search函数查找二叉树中数据域为x的结点 void search(node* root, int x){ if(root == NULL)return; if(x == root->data){ printf("%d", root->data); } else if(x < root->data){ search(root->lchild, x); }else{ search(root->rchild, x); } }
-
插入操作(以为root加了地址符&,所以每一次的root都是第一个地址,也就是根结点位置。故是从全局来判断大小,保证二叉树的顺序正确)
node* newNode(int x){ node* Node = new node; Node->v = x; Node->lchild = Node->rchild = NULL; return Node; } //insert函数将在二叉树中插入一个数据域为x的新结点 void insert(node* &root, int x){ if(root == NULL){//查找失败,说明是插入的位置 root = newNode(x); return; } //结点已经存在,返回 if(x == root->data){ retrun; } else if(x < root->data){ insert(root->lchild, x); } else{ insert(root->rchild, x); } }
-
二叉查找树的建立
//就是不断地插入结点 node* create(int data[], int n){ node* root = NULL; for(int i = 0; i < n; i++){ insert(root, data[i]); } return root; }
-
二叉查找树的删除,复杂度为O(h),h为树高(把树中比结点权值小的最大结点称为该结点的前驱,而把比结点权值大的最小结点称为该结点的后继)
//寻找最大权值结点,就是寻找右子树;最小就是左子树 //寻找以root为根结点的树中的最大权值结点 node* findMax(node* root){ while(root->rchild != NULL){ root = root->rchild; } return root; } //寻找以root为根结点的树中最小权值结点 node* findMin(node* root){ while(root->lchild != NULL){ root = root->lchild; } return root; } //删除以root为根结点的树中权值为x的结点 void deleteNode(node* &root, int x){ if(root == NULL) return;//不存在值为x的结点 if(root->data == x){ if(root->lchild == NULL && root->rchild == NULL){ root = NULL;//没有左右子树,直接删除 }else if(root->lchild != NULL){//左子树不空 node* pre = findMax(root->lchild); root->data = pre->data; deleteNode(root->lchild, pre->data) }else{ node* next = findMin(root->rchild); root->data = next->data; deleteNode(root->rchild, next->data); } }else if(root->data > x){//往左子树找 deleteNode(root->lchild, x); }else{//往右子树找 deleteNode(root->rchild, x); } }
-
二叉查找树的性质
- 对二叉查找树进行中序遍历,遍历的结果是有序的
-
-
平衡二叉树(AVL树)
-
平衡二叉树仍然是一棵二叉查找树,它使树的高度在每次插入元素后仍然能保持O(logn)级别。
-
二叉树的查找(与二叉查找树一致),复杂度为O(logn)
-
二叉树的基本操作
//结点结构 struct node{ int v, height;//v为权值,height为高度 node * lchild, *rchild; }; //生成一个新结点,v为结点权值 node* newNode(int v){ node* Node = new node; Node->v = v; Node->height = 1; //结点初始高度为1 Node->lchild = Node->rchild = NULL; return Node; } //计算结点的平衡因子 int getBalanceFactor(node* root){ //左子树高度减去右子树高度 return getHeight(root->lchild) - getHeight(root->rchild); } //得到当前树的高度 int getHeight(node* root){ if(root == NULL)return 0; return root->height; } //更新结点的高度 void updateHeight(node* root){ root->height = max(getHeight(root->lcihld, getHeight(root->rchild)) + 1; } //左旋,和图一起记忆理解 void L(node* &root){ node* temp = root->rchild; root->rchild = temp->lchild;//第一步,将b的左子树称为a的右子树 temp->lchild = root;//第二步,a成为b的左子树 updateHeight(root);//更新结点A的高度 updateHeight(temp);//更新结点B的高度 root = temp;//更改根结点 } //右旋 void R(node* &root){ node* temp = root->lchild; root->lchild = temp->rchild; temp->rchild = root; updateHeight(root); updateHeight(temp); root = temp; }
-
二叉树的插入操作
情况汇总:
LL:BF(root) = 2,BF(root->lchild) = 1.调整方法:对root进行右旋
LR: BF(root) = 2, BF(root->lchild) = -1,调整方法:对root->lchild进行左旋,再对root进行右旋
RR: BF(root) = -2, BF(root->rchild) = -1,调整方法:对root进行左旋
RL:BF(root) = -2, BF(root->rchild) = 1,调整方法:先对root->rchild进行右旋,再对root进行左旋
void insert(node* &root, int v){ if(root == NULL){ root = newNode(v); return; } if(v < root->v){ insert(root->lchild, v);//往左子树插入 updateHeight(root); //更新树高 if(getBalanceFactor(root) == 2){ if(getBalanceFactor(root->lchild) == 1){//LL型 R(root); }else if(getBalanceFactor(root->lchild) == -1){//LR型 L(root->lchild); R(root); } } }else{ insert(root->rchild, v); updateHeight(root); if(getBalanceFactor(root) == -2){ if(getBalanceFactor(root->rchild) == -1){//RR型 L(root); }else if(getBalanceFactor(root->rchild) == 1){//RL型 R(root->rchild); L(root); } } } } //AVL树的建立 node* create(int data[], int n){ node* root = NULL; for(int i = 0; i < n; i++){ insert(root, data[i]); } return root; }
-
-
并查集
-
并查集的定义:用数组实现。支持两个操作:合并两个集合和查找——判断两个元素是否在一个集合内
-
并查集的基本操作:
- 并查集产生的每一个集合是一棵树,不会有环
//初始化 for(int i = 1; i <= N; i++){ father[i] = i; } //查找(同一个集合之中只有一个根节点,因此这个过程就是查找元素根节点的过程) //递推方法 int findFather(int x){ while(x != father[x]){ x = father[x];//获得自己的父亲结点 } return x; } //递归方法 int findFather(int x){ if(x == father[x])return x; else return findFather(father[x]); } //合并 void union(int a, int b){ int faA = findFather(a); int faB = findFather(b); if(faA != faB){ //如果两个元素不属于同一个集合 father[faA] = faB; //合并 } } //注意:两个元素在同一个集合之中不会进行操作,这样可以保证集合之中没有环,即并查集产生的每一个集合都是一棵树
-
并查集的查找优化——路径压缩(把当前查询结点的路径上的所有结点的父亲都指向根节点),查找时时间复杂度为O(1)
int findFather(int v){ if(v == father[v])return v;//找到根节点 else{ int F = findFather(father[v]);//寻找father[v]的根节点F father[v] = F;//将根节点F赋值给father[v] return F; } }
-
-
堆
-
堆是一棵完全二叉树
-
堆的基本操作
- 建堆完成不是初始堆,而是初始堆经过向下调整之后的堆才叫建堆完成。
//定义数组来表示堆 const int maxn = 100; //heap为堆,n为元素个数。 int heap[maxn], n = 10; //对堆进行从上往下调整,时间复杂度为O(logn) //对heap数组在[low, high]范围内进行向下调整 //其中low为欲调整结点的数组下标,high一般为堆的最后一个元素的数组下标 void downAdjust(int low, int high){ int i = low, j = i * 2; //i为欲调整结点,j为其左孩子 while(j <= high){//存在孩子节点 //如果右孩子存在,且右孩子的值大于左孩子 if(j + 1 <= high && heap[j+1] > heap[j]){ j = j + 1; } //如果孩子中的最大的权值比欲调整结点i大 if(heap[j] > heap[i]){ swap(heap[j], heap[i]);//交换最大权值的孩子与欲调整结点i i = j; j = i * 2; }else{ break; //孩子的权值均小于欲调整的结点值,循环结束。 } } } //建堆,时间复杂度为O(n) void createHeap(){ for(int i = n / 2; i >= 1; i--){ downAdjust(i, n); } } //删除堆顶元素,时间复杂度为O(logn) void deleteHeap(){ heap[1] = heap[n--];//用最后一个元素覆盖堆顶元素,并让元素个数i减1 downAdjust(1, n); } //向上调整——适用于想要插入一个元素,将要插入的元素放在数组末尾,然后向上调整。 //low一般设置为1,high表示欲调整的结点的数组下标 void upAdjust(int low, int high){ int i = high, j = i / 2; while(j >= low){ //父亲在[low, high]之间 //父亲的权值小于欲调整结点i的权值 if(heap[j] < heap[i]){ swap(heap[j], heap[i]); i = j; j = i / 2; }else{ break; } } } //添加元素 void insert(int x){ heap[++n] = x;//让元素个数先增加,然后将数组末位赋值x upAdjust(1, n); }
-
堆排序(顶部最大)
- 堆排序的直观思路就是取出堆顶元素,然后将堆的最后一个元素换至堆顶,再进行一次针对堆顶元素的向下调整。
void heapSort(){ createHeap(); for(int i = n; i > 1; i--){ swap(heap[i], heap[1]); downAdjust(1, i - 1); } }
-
-
哈夫曼树
-
问题描述:已知n个数,寻找一棵树,使得树的所有叶子结点的权值恰好为这n个数,并且使得这棵树的带权路径长度最小。带权路径长度最小的树称为哈夫曼树,又称为最优二叉树。
-
注意,在解决实际问题时,通常不需要构建一棵哈夫曼树,只要利用哈夫曼树的思想即可——不断挑选两个权值最小的结点,合并,直到只剩下一个元素。此处就可以用到优先队列
-
代码如下:
#include<bits/stdc++.h> using namespace std; //以数值小的数的优先级高 priority_queue<long long, vector<long long>, greater<long long> >q; int main(){ int n; long long temp, x, y, ans = 0; cin >> n; for(int i = 0; i < n; i++){ cin >> temp; q.push(temp);//压入优先队列中 } while(q.size() > 1){//当只剩一个元素的时候,跳出循环 x = q.top(); q.pop(); y = q.top(); q.pop(); q.push(x + y); ans += x + y;//两个数相加,表示当前的路径值 } cout << ans; return 0; }
-
-