数据结构笔记2
数据结构
第5章 树
5.1 树的基本概念
树的度: 是节点的度的最大值
树深度: 节点层次的最大值
节点的度:子树的个数
-
树的性质:
-
- n 个结点,每个节点\[度为\ d_i,则 \ n=\sum_{i=1}^{n}d_i +1 \]
- n 个结点,每个节点
-
- 度为 k 的树第 i 层的结点个数最多为\[k^{i-1} \]
- 度为 k 的树第 i 层的结点个数最多为
-
-
深度为h的k叉树,最多结点为
\[\frac{k^{h}-1}{k-1} \] -
具有n个结点的k叉树深度最小为
\[\lceil log_k(n(k-1))+1 \rceil \]
-
-
-
树的基本操作
bool CreateTree(position &t);bool DelTree(position &t);void DelSubTree(position &t,position p);TElelmType GetData(position p);-
-
//基本操作的代码实现 InitTree(&T) DestroyTree(&T) CreateTree(&T,definition) TreeEmpty(T) TreeDepth(T) Root(T) Parent(T, x) FirstChild(T,x) Nextsibling(T,x) InsertChild(&T,x,i,p) DeleteChild(&T,x,i) Traverse(T,visit()) -
设计算法
-
递归计算树的深度
若树空则返回
0或者递归到树的叶子节点,则返回1否则递归遍历其他子树,取其最大值加上1 则是树的高度
-
算法描述:
-
//递归计算树的深度
int height(Tree T){
if(IsEmpty(T)) return 0;
else if(Isleaf(T)) return 1;
else {
int h = 0,k;
for (Tree p=firstchild(T);P != NULL;p = Nextsibling(T,p))
if((k = height(P)) > h) h = k;
return h+1;
}
}
5.2 二叉树的基本概念
结点的度: 非空子树的个数
叶子结点 : 左右子树均空,度为0
完全二叉树: 深度为h,h-1为满二叉树,h层的结点都集中在左侧
5.2.1 二叉树的存储结构
-
-
顺序存储结构
完全二叉树适用
一般二叉树:造成存储空间大量浪费
typedef struct { Telemtype Nodes[Max_size];//存放二叉树结点的数组 int n;//二叉树结点个数 }SqBiTree;
-
-
-
链式存储结构

typedef struct BiTree{ TElemType data; Struct BiTNode *lchild,*rchild; }BiTNode,*BiTree;
-
5.2.2 二叉树的遍历及应用
1. 先序、中序和后序遍历二叉树
-
先序遍历
先序遍历定义
若树为空,则空操作;
否则:
-
访问根节点;
-
先序遍历左子树;
-
先序遍历右子树。
void PreOrder(BiTree T){ if(!T) return ; else { visited(T->data);//such as: /*printf("%c",T->data);*/ PreOrder(T->lchild); PreOrder(T->rchild); } } -
-
中序遍历
定义略
void InOrder(BiTree T){ if(!T) return ; else{ Inorder(T->lchlid); visited(T->data); Inordeer(T->rchild); } } -
后序遍历
定义略
void PostOrder(BiTree T){ if(!T) return ; else{ PostOrder(T->lchild); PostOrder(T->rchild); visited(T->data); } }
扩展二叉树
定义:将二叉树上每个空子树用一个虚结点表示(不妨设虚结点为'#'),这样的树称为扩展二叉树
空指针的虚结点称为外部节点
-
非递归法进行先序遍历
void PreOrder(BiTree T){ InitStack(S); while(T||!StackEmpty(S)){ while(T){ visited(T->data); Push(S,T); T = T->lchild; } if(!StackEmpty(S)){ Pop(S,T); T = T->rchild; } } } -
层序遍历二叉树
算法思路:
- 初始化一个空队列,用来保存已经访问过结点的孩子;
- 非空根指针入队;
- 若队列为空,则遍历结束;否则重复执行:
- 队头元素出队,访问;
- 若被访结点有左孩子,则左孩子入队;
- 若被访结点有右孩子,则右孩子入队;
void LayerTraversal(BiTree T){ InitQueue(Q); if(T) EnQueue(Q,T); while(!QueueEmpty(Q)){ DeQueue(Q,p); visite(p->data); if(p->lchild)EnQueue(Q,p->lchild); if(p->rchild)EnQueue(Q,p->rchild); } } -
求二叉树节点数
方案一:
int CountNodes1(BiTree T){ if(!T) return 0; else{ n1=CountNodes1(T->lchild); n2=CountNodes1(T->rchild); return (1+n1+n2); } }方案二:对树进行先序后序中序任何一种形式的遍历,在访问操作时进行计数;
int CountNodes2(BiTree T,int &n){ int n=0; if(!T) return 0; else{ n++; CountNodes2(T->lchild,n); CountNodes2(T->rchild,n); } }
- 输出二叉树每个结点的层次
void Level(BiTree T,int lev){ lev=0; if(T){ lev++; printf(T->data,lev); Level(T->lchild,lev); Level(T->rchild,lev); } }
//二叉树的操作
InitBiTree(&T)
DestroyBiTree(&T)
CreateBiTree(&T,definition)
BiTreeEmpty(T)
BiTreeDepth(T)
Parent(T,e)
LeftChild(T,e)
RightChild(T,e)
LeftSibling(T,e)
RightSibling(T,e)
InsertChild(&T,p,LR,C)
DeleteChild(&T,p,LR)
Traverse(T)
s
//二叉树的结构
typedef struct Node{
char data;
struct Node *lchild,*rchild;
}*BiTree,BiNode;,
//初始化二叉树
void InitBitree(BiTree &T){
char ch;
cin >> ch;
if(ch == '#'){
T = NULL;
}
else{
T = new BiNode;
T -> data = ch;
InitBitree(T->lchild);
InitBitree(T->rchild);
}
}
-
二叉树的几个基本性质
-
在 二叉树的第\(i\)层的结点个数最多为
\[2^{i-1} \] -
深度为\(k\)的二叉树的最大结点数为
\[2^k-1 \] -
任一二叉树\(T\),如果其叶子结点数为\(n_0\), 度为2的结点数为\(n_2\),则
\[n_0=n_2+1 \\n_0+n_1+n_2=n=2n_2+n_1+1 \] -
具有\(n\)个结点的完全二叉树深度为
\[\lceil log_2(n+1) \rceil \ 或\ \lfloor log_2n\rfloor+1 \] -
如果对一个有\(n\)个结点的完全二叉树\(T\)的结点按层序(从第一层到第\([logn]+1\)层,层内从左到右从1开始编号,则对任意一个编号为\(i(1<=i<=n)\)的结点有:
-
如果\(i=1\),则该结点是二叉树的根,无双亲;如果\(i>1\)则其双亲结点\(Parent(i)\)的编号为\([i/2]\)
-
如果\(2i>n\),则编号为\(i\) 的结点没有左孩子,为叶子结点;否则其左孩子\(LChild(i)\)的编号为2i
-
如果\(2i+1>n\),则编号为\(i\) 的结点没有右孩子;否则其右孩子\(RChild(i)\)的编号为\(2i+1\)
-
第6章 图
图遍历运用——迷宫问题
-
如何得到路径?
- 广度遍历时,队列元素增加一个 指向“队头”元素的指针
//代码实现
typedef struct {
int xpos;
int ypos;
}PosType;//迷宫每个单元信息
typedef struct DQNode{
PosType seat;
struct DQNode *next, *pre;//pre用于反向指向,找得到邻结点
}DQNode,*Dqueueptr;
typedef struct {
Dqueueptr front;
Dqueueptr rear;
}DlinkQueue;
//入队列
void EnQueue(DLinkQueue &Q,PosType e){
p=new DQNode;
p->seat.xpos=e.xpos;
p->seat.ypos=e.ypos;
p->next=NULL;
if(!Q.rear){ //首个结点
p->pre=NULL;//必须要写这句话
Q.rear=p;
Q.front=p;
}else{
p->pre=Q.front;
Q.rear->next=p;
Q.rear=p;
}
//出队列 自己写一下 不销毁结点
/*因Dequeue时找出邻接点, 如果先GetHead,并且Dequeue不销毁,
这样可以建立当前结点与队首结点关系*/
void DeQueue(){
}
//下一个结点
PosType NextPos(PosType cur, int v)
{
Postype npos;
npos.xpos=cur.xpos+di[v];
npos.ypos=cur.ypos+dj[v];
return npos;
}
bool Pass(Postype npos)
{
return(0<=npos.xpos && npos.xpos<=m-1 &&
0<=npos.ypos && npos.ypos<=n-1 &&
maze[npos.xpos][npos.ypos]= =0 &&
visited[npos.xpos][npos.ypos]= =FALSE)
}
bool ShortestPath(int maze[][], int m, int n, Stack &s){
//Stack起反向作用
DLinkQueue Q; bool visited[m][n]; InitQueue(Q);
for(i=0;i<m;i++)
for(j=0;j<n;j++)visited[i][j]=FALSE;
EnQueue(Q,(0,0)); visited[0][0]=TRUE;found=FALSE;
while(!found&&!QueueEmpty(Q)){//广度优先遍历可以中途停止当作指标
GetHead(Q,curp); //GetHead函数写出来一下,获得Q队首元素curp
for(v=0;v<8 &&!found;v++){
npos=NextPos(curp,v);
if(Pass(npos)){
EnQueue(Q,npos);//入队列
visited[npos.xpos][npos.ypos]=TRUE;
if(npos.xpos==m-1 && npos.ypos==n-1)//!!!!!!
found=TRUE;
}
}//for
DeQueue(Q,curp);
}//while
if(found){
InitStack(S);
p=Q.rear;
while(!p){
Push(S,p->seat); //自栈顶至栈底为路径
p=p->pre;
}//while
return TRUE;
}//if
else return FALSE;
}//ShortestPath
-
总结:
把问题抽象为图问题,怎么保存路径,销毁结点
6.4 最小生成树(MST)
-
相关概念:
极小连通子图:
n个结点的连通图中,包涵n个结点和n-1个边构成的连通子图
连通图的生成树:即极小连通子图
连通网的最小生成树:权值和最小的生成树
求连通网最小生成树的算法
– 克鲁斯卡尔(Kruskal)算法 复杂度:O(\(eloge\))
– 普里姆(Prim)算法 复杂度:O( \(n^2\) )
– 算法比较:当e(边)与\(n^2\) 差不多时,采用Prim算法快;当e远小于\(n^2\) 时,采用Kruskal算法快
-
Kruskal算法
- 算法思想
- 构造只含n个结点的森林。
-
按权值从小到大选择边加入到森林中,并使森林不产生回路。
-
重复2直到森林变成一颗树
- 算法描述
-
设\(G(V,E)\),把V={1,2,......n}看成孤立的n个连通子图。边按照权的非递减次序排列。
-
顺序查看边。对于第k条边(v,w),如果v,w分别属于两个连通字图\(T_1\)、T2,则用边(v、w)将T1、T2连成一个连通字图。
-
重复2,直至n个结点同属于一个连通图
太过复杂,本课不要求
-
Prim算法
算法思想 复杂度O(\(n^2\))
-
将所有结点分为两类集合:一类是已经落在生成树上的结点集合,另一类是尚未落在生成树上的结点集合。
-
在图中任选一个结点\(v\)构成生成树的树根,形成生成树结点集合。
-
在连接两类结点的边中选出权值最小的边,将该边所连接的尚未落在生成树上的结点加入到生成树上。同时保留该边作为生成树的树枝。
-
重复\(3\)直至所有结点都加入生成树
算法描述
\[\begin{align} &1.设G=(V,E),权A[m,n],令U=\{1\}。\\ &2.if(U<V) 取min(A[i,j]),使i∈U,j∈V-U。\\ &3.将j加入U\\ &4.重复2、3,直至U=V\\ \end{align} \] -
//代码实现
//利用数组记录权值然后遍历比较出最小权
void Prim(MGraph G, int v0, int adjvex[]){
//从序号为v0的顶点出发,构造连通网G的最小生成树
int lowcost[MAX_VERTEX_NUM]; //定义辅助数组
for(j=0;j<G.vexnum;j++) //用邻接矩阵的v0行初始化lowcost
if(j!=v0){lowcost[j]=G.arcs[v0][j];adjvex[j]=v0;}
lowcost[v0]=0; //将v0标记为红点
for(i=1;i<G.vexnum;i++){
k=MinEdge(lowcost, G.vexnum); //lowcost[]中非零最小值
printf("(%d,%d),%d\n",k,adjvex[k],lowcost[k]);
lowcost[k]= 0; //vk变成红点
for(j=0;j<G.vexnum;j++){ //调整紫边集
if(G.arcs[k][j]<lowcost[j]){
adjvex[j]=k;//将vj的候选紫边所关联的红点改成vk
lowcost[j]=G.arcs[k][j]; //调整vj候选紫边的权
}
}
}//Prim

6.5 拓扑排序
- 要求
有向无环图
活动顶点网络(AOV,activity on vertex )
- 以顶点表示活动,以弧表示活动之间的优先制约关系的有向图。
死锁:
- AOV中不允许出现回路,回路意味某活动以自己的结束作为开始的先决条件。称为死锁。
拓扑排序、拓扑有序序列
- 若在有向图中从\(u\)到\(v\)有一条弧,则在序列中\(u\)排在\(v\)之前,称有向图的这个操作为拓扑排序。所得序列为拓扑有序序列。若有死锁,则无法获得拓扑有序序列
-
操作方法:
- 选取一个没有前驱的顶点,输出它,并从AOV中网中删除此顶点以及所有以它为尾的弧。
- 重复1.直至输出所有结点
-
统计有向图邻接表各顶点的入度
void init_indegree(ALGraph G){
for(i=0;i<G.vexnum;i++)indegree[i]=0;
for(i=0;i<G.vexnum;i++){
p=G.vertices[i].firstarc;
while(p){//第i个元素第一个指针
indegree[p->adjvex]++;
p=p->nextarc;
}//while
}//for
} //init_indegree
//邻接矩阵的写法
for(i=0;i< G.vexnum;i++){
for(j=0;j < G.vexnum;j++){
if(!G.arcs[i][j])indegree[j]++;
}
}
//与储存方式无关的写法
for( i in V ){
for(w = firstadjvex(G,i);w!=-1;w = nextadjvex(G,i,w))
indegree[w]++;
}
- 取入度为0的点\(v\)
int getzerodegree(ALGrapg G){
for(i=0;i<G.vexnum;i++)
if(indegree[i]==0){indegree[i]=-1;return i;}
return –1;
}
- \(FirstAdj\)
int FirstAdj(ALGrapg G ,int v){
if(G.vertex[v].firstarc)
return G.vertex[v].firstarc->adjvex;
else
return –1;
}
- \(NextAdj\)
int NextAdj(ALGrapg G ,int v, int w){
p=G.vertices[v].firstarc;
while(p && p->adjvex!=w)p=p->nextarc;
if(p && p->nextarc)
return p->nextarc->adjvex;
else
return –1;
}
- 拓扑排序 复杂度\(O(n+e)\)
m=0;
init_indegree(G);
v=getzerodegree(G)
while(v!=-1){
cout<<v;
++m;
w=FirstAdj(G, v);
while(w!=-1){
indegree[w]--;//入度减少
w=nextAdj(G, v, w);
}
v=getzerodegree(G);
}
if(m!=G.vexnum) cout<< "有死锁!"//理解一下这句话
//使用栈的算法
void TopologicalSort(ALGraph G){
int count=0; InitStack(S);
int InDegree[MAX_VERTEX_NUM]={0};
for(i=0;i<G.vexnum;i++)
for(p=G.vertices[i].firstarc;p;p=p->nextarc)
InDegree[p->adjvex]++;
for(i=0;i<G.vexnum;i++)
if(InDegree[i]==0) Push(S,i); //入度为0的顶点进栈
while(!StackEmpty(S)){
Pop(S,j); cout<<G.vertices[j].data; count++;
for(p=G.vertices[j].firstarc; p; p=p->nextarc)
if(!(--InDegree[p->adjvex])) Push(S,k);
}//end while
if(count<G.vexnum) cout<<”该图有环” ;
} //TopologicalSort
//深度优先遍历 得到的序列为C7C4C5C2C3C1C6正好是逆序
//仿造DFS来进行写作算法
int visited[MAX_VERTEX_NUM]]; //访问标志数组,全局数组
void DFSTraverse(Graph G){
for(i=0;i<G.vexnum;i++) visited[i]=False; //置未访问标志
for(v=0; v<G.vexnum; v++) //对visited数组循环
if(!visited[v]) DFS(G, v);
}//DFSTraverse
void DFS_f(Graph G, int v){
//visite(v);
visited[v]=True;
for(w=FirstAdjVex(G,v);w!=-1;w=NextAdjVex(G,v,w))
if(!visited[w]) DFS_f(G, w);
printf(v);//打印节点名字
}//DFS
6.6 关键路径
事件:
- 关于活动开始或完成的断言或陈述。
活动边网络(AOE,activity on edge ):
- 以弧表示活动,以顶点表示事件的有向图。弧有权值,表示活动所需的时间。
源点、汇点:
- 整个工程的起始点为源点,整个工程的结束点为汇点。一个工程的AOE是一个单源、单汇点的无环图。
带权路径长度:
- 一条路径上所有弧权值之和。
关键路径、关键活动:
- 一个工程的最短完成时间是从源点到汇点的最长带权路径,称该路径为关键路径;该路径上的活动为关键活动
- 如何获得关键路径
设源点\(V_1\),汇点\(V_n\)
- \(v_{e_j}\):事件\(V_j\)可能发生的最早时刻。即从\(V_1\)到\(V_j\)的最长带权路径。
\[v_{e_j}=\left\{ \begin{matrix}0&(j=1)\\ max\{{v_{e_i}+w(V_i->V_j)}\} &(j=2,3......n) \end{matrix}\right.\\ \]
- \(v_{i_i}\):在不延误整个工期的前提下,事件\(V_i\)发生所允许的最晚时刻。等于\(ve(n)\)减去从\(Vi\)到\(Vn\)的最长带权路径长度。
\[v_{l_i}=\left\{ \begin{matrix}v_{e_n}&(i=n)\\ min\{{v_{l_j}-w(V_i->V_j)}\} &(j=1,2,3......m) \end{matrix}\right.\\ \]
\(ee(k)\):活动\(ak(Vi->Vj)\)可能开始的最早时刻。
\[ee(k)=ve(i) \]\(el(k)\):在不延误整个工期的前提下,活动\(ak(Vi->Vj)\)开始所允许的最晚时刻。
\[el(k)=vl(j)-w(Vi->Vj) (k=1,2,3... ...m) \]
- 若某弧\(ak\)的\(el(k)\)和\(ee(k)\)相等,则\(ak\)为关键活动;否则\(el(k)-ee(k)\)为活动的余量
6.7 单源最短路径
- 迪杰斯特拉(Dijkstra)算法
设\(AS[n,n]\)为有向网的邻接矩阵,\(S\)为已找到最短路径的终点的集合,其初值只有一个顶点,即源点。\(Dist[n]\)的每个分量表示当前所找到的从源点出发(经过集合S中的顶点)到各个终点的最短路径长度。其初值为\(Dist[k]=AS[i,k]\) (\(i\)为源点,\(k=0,1···n\))
选择\(u\),使得 \(dist[u]=min{Dist[w]|w!∈S,w∈V(G)}\),\(u\)为目前找到的从源点出发的最短路径的终点。将\(u\)加入\(S\)集合。
修改\(Dist\)数组中不在S中的终点对应的分量值。如果\(AS[u,w]\)为有限值,即\(u,w\)有弧存在,且 \(Dist[u]+AS[u,w]<Dist[w] 则令Dist[w]= Dist[u]+AS[u,w]\)。
重复\(2)、3)\)直至求得源点到所有终点的最短路径
查找表
7.1 静态查找表
1. 二分查找
//二分查找
typedef struct{
Datatype data; //元素数据
KeyType key; //元素关键字
}Elemtype; //数据元素类型
typedef struct{
Elemtype *elem; //约定从下标1开始
int len;
}StaticSrhTable; //顺序静态查找表类型
int BinSearch(StaticSrhTable SST,KeyType kval){
bot=1, top=SST.len; // 置查找范围初值
while(bot<=top) {
mid = (bot+top)/2;
if(SST.elem[mid].key==kval) return mid; //查找成功
else{
if (SST.elem[mid].key>kval) top = mid-1;//前半区
else bot = mid+1; // 后半区
}
}
return 0; // 未查找到
}
2. 分块查找
typedef struct {
KeyType key;
int stadr;
}indexItem;
typedef struct{
indexItem *elem;
int length;
}indexTable;//索引表

//首先在索引表里面查找
state BlkInxSearch(StaticSrhTable SST, InxTab Inx, KeyType kval){
bot = 1, top = Inx.len, blFound = FALSE; // 置查找范围初值
if (kval > Inx.elem[top].key) return 0; // 越界(上限判断)
while (bot <= top && !blFound) {
mid = (bot + top) / 2;
if (Inx.elem[mid].key == kval) { // 索引查找成功
blFound = TRUE; bot = mid;
}
else {
if (Inx.elem[mid].key > kval) top = mid - 1; // 前半区
else bot = mid + 1; // 后半区
}
} //退出循环时,bot所指的为所找的块
bn = Inx.elem[bot].StartAdd; //第bot块的数据记录起始地址
if (bot < Inx.len) en = Inx.elem[bot + 1].StartAdd – 1;
else en = SST.len; //第bot块的数据记录尾地址
for (i = bn; (i <= en) && (SST.elem[i].key != kval); i++);//无哨卡,加入越界判断
if (i <= en) return i;
return 0; //未查找到
}
StartAdd 起始地址
7.2 动态查找表
1. 数据结构
ADT DynamicSearchTable{ 数据对象:D是具有相同特性的元素集合。 数据关系:同属集合关系。 基本操作: InitDSTable(&DT) DestroyDSTable(&DT) SearchDSTable(DT,kval) InsertDSTable(&DT,e) DeleteDSTable(&DT,kval) TraverseDSTable(DT,Visit()) }ADT DynamicSearchTable;
2.查找,插入,删除
Bool BinSrTree(BiTree BT, KeyType kval, BiTree &p, BiTree &f)
{/* 查找关键字为kval的记录。 若找到,则指针p指向该记录并返回TRUE;否则,返回FALSE。指针f指向p所指记录的双亲记录;若查找失败则p为空指针,f则为这个空指针的双亲。*/
p = BT;
while (p) {
if (p->data.key == kval) return TRUE; // 查找成功
else {
f = p;
if (p->data.key > kval) p = p->lChild;// 查找左子树
else p = p->rChild; // 查找右子树
}
}
return FALSE; // 未查找到
}// BinSrTree
//插入算法
Bool BinSrTree_Ins(BiTree &BT, KeyType kval)
{/* 当二叉查找树BT中不存在关键字为kval的记录时,插入之并返回TRUE; 否则,不进行插入操作并返回FALSE。*/
f = NULL;
if (BinSrTree(BT, kval, p, f)) return FALSE; // 不插入
t = new BiTNode;
t->data.key = kval; t->lChild = t->rChild = NULL;
if (!f) BT = t; // 空树时为根结点
else if (kval < f->data.key)
f->lChild = t; // 为左孩子
else f->rChild = t; // 为右孩子
return TURE;
}//BinSrTree_Ins
//删除算法
Bool BinSrTree_Del(BiTree &BT, KeyType kval) {
f = NULL;
if (!BinSrTree(BT, kval, p, f)) return FALSE; // 不删除
if (p->lChild && p->rChild) { //度为2,左右子树都非空
q = p; t = p->lChild;
while (t->rChild) { q = t; t = t->rChild;}
p->data = t->data; // t指向左子树中关键字最大的结点
if (q != p) q->rChild = t->lChild;//q != p 特殊处理!!!
else q->lChild = t->lChild; // t结点为p结点的左子树根
free(t);
}
else { //度<=1
q = p;//p是删除结点
if (!p->rChild) p = p->lChild; // 右子树为空,挂其左子树
else p = p->rChild; // 左子树为空,挂其右子树
if(!f) BT=p; //删除的是根结点
else{
if(q==f->lchild)f->lchild=p;
else f->rchild=p;
}
delete q;
}
}// BinSrTree_Del
- 删除结点的处理方法
若是叶子结点,直接删除
只有一个孩子,则将其孩子直接挂到其双亲上。
有两个孩子,找左孩子中最大的一个元素,代替被删除结点,最大元素肯定只有一个孩子,按2)处理删除最大元素
3. 平衡二叉树
1.概念
什么是平衡二叉树
- 树中每个结点的左、右子树深度之差的绝对值不大于1,这种平衡状态的二叉查找树。
平衡因子
结点的左子树深度和右子树深度之差。
AVL树的平衡因子只能取-1,0,1
最小子树根
- 离插入结点最近且平衡因子不满足绝对值小于等于1的祖先结点。
实现方法:平衡旋转技术
- 四种平衡旋转方式





浙公网安备 33010602011771号