前情提要:这是我为了数据结构期末考试的复习总结,只涉及了我们考试要考部分,后期等我有空了再补充完整
目录
第一章 绪论
- 程序设计:为计算机处理问题编制的一组指令集
- 算法:处理问题的策略
- 数据结构:(非数值)问题的数学模型(如:线性模型、树状模型、网状模型等)
- 数据(Data):是信息的载体,是描述客观事物属性的数、字符及所有能输入到计算机中并被计算机程序识别和处理的符号的集合。
- 数据元素(Data Element):是数据的基本单位,在计算机程序中通常作为一个整体进行考虑和处理。可由若干数据项组成
- 数据项 (Data Item) :数据元素的分量,数据项是数据的不可分割的最小单位。
- 数据对象 (Data Object):同类型数据元素的集合,如一个系的全体学生等。,是数据的一个子集。
- 数据结构 (Data Structure):数据的逻辑结构,数据的存储结构,数据的运算合集
- 数据的逻辑结构:问题所涉及的数据对象,以及数据对象内部各个数据元素之间的特定关系(逻辑结构面向问题,与计算机无关)
- 逻辑结构:线性结构
、树形结构
、图状结构
、集合结构
- 数据的存储结构:全体数据元素以及数据元素之间的特定关系在计算机内部的表达(面向计算机)
- 存储结构:顺序存储、链式存储、散列存储、索引存储
- 数据的运算集合:为解决问题而对数据施加的一组操作,对数据进行加工和处理的一组算法
- 运算集合:既面向问题又面向计算机:1.操作集合的定义由问题决定;2.操作的实现与数据在计算机内的存储方式有关。
- 数据类型(Data Type):一个值的集合(数据的特征,取值范围等,如int)和定义在此集合上的一组操作(对值集定义的一组运算)的总称 数据类型包括:原子类型、结构类型、抽象数据类型
- 抽象数据类型 ADT (Abstract Data Type):可用三元组表示:( D--数据对象, S--D中数据元素之间的关系, P--对D的基本操作的集合 )
- 算法的五个特性:有穷性、确定性、可行性、输入、输出
- 常设计一个好的算法应考虑:正确性、可读性、健壮性、效率与低存储量需求
- 链式存储设计时,各个不同结点的存储空间可以不连续,但结点内的存储单元地址必须连续
- 算法效率分析:时间耗费:(渐进)时间复杂度; 空间耗费:(渐进)空间复杂度
- 语句的频度:算法中某条语句重复执行的次数称为该语句的频度。
- 算法的空间复杂度:算法执行期间需要占用的存储空间由3部分组成:(1) 数据元素集合所占的空间 (2) 程序本身所占的空间 (3) 辅助变量所占的空间(辅助空间)
- 算法原地工作是指算法所需辅助空间为常量,即O(1)
- 同一个算法,实现语言的级别越高,执行效率越低
例题:
- 链式存储结构中的数据元素之间的逻辑关系是由( D )表示的。
A. 线性结构 B. 非线性结构 C. 存储位置 D. 指针
- 设n是描述问题规模的非负整数,下列程序段的时间复杂度是(
)。
x=0;
while(n>=(x+1)*(x+1))
x=x+1;
- 与数据元素本身的形式、内容、相对位置、个数无关的是数据的( C )。
存储结构 B. 存储实现 C. 逻辑结构 D. 运算实现
-
for(i=1;i<=n-1;i++)
for(j=i+1;j<=n;j++) s; 语句中s的执行次数为((n-1)n/2)
第二章 线性表(Linear List)
- 线性表:是n个元素(n≥0)的有限序列。可记为L=(a1,a2,...,ai,ai+1,...,an)
- 线性表是一种逻辑结构,表示元素之间一对一的相邻关系。 其顺序存储:顺序表;其链式存储:单链表、双链表、循环链表、静态链表。
- 线性表的顺序存储又称顺序表,特点是表中元素的逻辑顺序与其物理顺序相同。用一组地址连续的空间存放线性表的数据元素。各元素按其逻辑次序依次存放;
- 元素物理地址的计算: LOC(ai+1)=LOC(ai)+L LOC(ai)=LOC(a1)+(i-1)×L
- 线性表的顺序表示和实现—顺序表,顺序表插入,删除和查找时间复杂度均为O(n)
-
设数组为A[m][n](m 行 n 列),要求的数组为A[i][j](i 行 j 列),L为存储单元所占空间。
行优先:
LOC(i,j) = LOC(0,0) + [i * n + j] *L;
列优先:
LOC(i,j) = LOC(0,0) + [j * m + i] *L; 或者记不住就画图推公式出来。
- 顺序表插入,删除和查找时间复杂度均为O(n)
-
顺序表 (顺序存储,随机存取的结构)
//顺序表 #include<iostream> using namespace std; #define MaxSize 10 typedef int ElemType; typedef struct { ElemType* elem; int length; }SqList; //初始化 bool initList(SqList& L) { L.elem = new int[MaxSize]; if (!L.elem) return false; L.length = 0; return true; } //输入数据 void Input(SqList& L, int n) { if (n > MaxSize) { cout << "太多了,放不下" << endl; return; } cout << "请输入数据:"; for (int i = 0; i < n; i++) { cin >> L.elem[i]; } L.length = n; } //输出 void Show(SqList L) { for (int i = 0; i < L.length; i++) cout << L.elem[i] << " "; } //取出位置的元素 int getElem(SqList L, int i) { if (i > L.length || i <= 0) { cout << "没有哦"; return -1; } return L.elem[i - 1]; } //表长 int getLength(SqList L) { return L.length; } //插入 bool insertList(SqList& L, int i, ElemType e) { if (L.length >= MaxSize) { cout << "空间满了,放不下" << endl; return false; } if (i <= 0 || i > L.length + 1) { cout << "插入的位置不对"; return false; } for (int j = L.length - 1; j > i - 2; j--) L.elem[j + 1] = L.elem[j]; L.elem[i - 1] = e; L.length++; return true; } //删除 bool ListDelete(SqList& L, int i, ElemType& e) { //删除顺序表的第 i 个元素,用e返回被删元素的值 if (i<1 || i>L.length) return false; //i非法 //ElemType* p = L.elem + i - 1;//令指针p指向ai,p=L.elem+i-1; //e = *p; //for (p++; p <= L.elem + L.length - 1; p++) // *(p - 1) = *p; //ai+1…an左移一位 //L.length--; //表的长度减1 //return true; e = L.elem[i - 1]; for (int j = i; j < L.length; j++) L.elem[j - 1] = L.elem[j]; L.length--; return true; } //ListDelete //判空 bool EmptyList(SqList L) { if (L.length == 0) return true; return false; } //合并 void MergeList(SqList La, SqList Lb, SqList& Lc) { //将有序线性表La和Lb归并成有序线性表Lc int i, j; i = j = 1; int k = 0; initList(Lc); //初始化 int ai, bj; int La_len = getLength(La); int Lb_len = getLength(Lb);//求表长 while (i <= La_len && j <= Lb_len) { //La和Lb均非空 ai = getElem(La, i); bj = getElem(Lb, j);//读ai和bj if (ai <= bj) { insertList(Lc, ++k, ai); i++; } else { insertList(Lc, ++k, bj); j++; } } while (i <= La_len) { //La中剩余元素一一插入Lc ai = getElem(La, i++); insertList(Lc, ++k, ai); } while (j <= Lb_len) { //Lb中剩余元素一一插入Lc bj = getElem(Lb, j++); insertList(Lc, ++k, bj); } }// MergeList //查找元素 int LocateElem(SqList& L, ElemType e) { //ElemType* p = L.elem; int i = 1; //while (i <= L.length && *p++ != e) i++; //return i <= L.length ? i : 0; for (int i = 0; i < L.length; i++) if (L.elem[i] == e) return i; return -1; } //销毁线性表: void DestroyList(SqList& L) { //释放顺序表L所占的空间 delete[] L.elem; L.length = 0; } //逆置顺序表 void Invert(ElemType* E, int s, int t) { //将数组E自下标s至下标t之间的元素逆置 int temp; while (s < t) { temp = E[s]; E[s] = E[t]; E[t] = temp; s++; t--; } } void InvertSqList(SqList& L) { //将顺序表L中的所有元素逆置 Invert(L.elem, 0, L.length - 1); } //交换 void Exchange(SqList L, int m) { //将顺序表L中前m个元素与后L,length-m个元素互换位置 Invert(L.elem, 0, m - 1); Invert(L.elem, m, L.length - 1); Invert(L.elem, 0, L.length - 1); }
- 头指针和头结点区分:不管带不带头结点,头指针都始终指向链表的第一个结点,而头结点是带头结点的链表中的第一个结点,结点内通常不存储信息
- 头节点的作用: 1)对链表的删除、插入操作时,第一个结点的操作更方便 2)统一空表和非空表的处理
- 线性表的链式表示和实现:单链表、双链表、循环链表、静态链表
- 线性链表:随机存储,顺序存取的结构(单链表)
- 单链表的插入删除的时间复杂度也是O(n),不需要大量移动元素,只需修改指针,时间是消耗在指针运动上。
- 采用头插法建立单链表时,读入数据的顺序与生成的链表中的元素的顺序是相反的,每个结点插入时间为O(1)一共为O(n)
- 尾插法必须增加一个尾指针使其始终指向当前链表的尾结点
- 带头结点的单链表
#include<iostream> using namespace std; typedef int ElemType; typedef struct LNode{ ElemType data; struct LNode* next; }LNode,*LinkList; //初始化 //带头结点 void initList(LinkList& L) { L = new LNode; L->next = NULL; } //头部插入法建表 void InputList_H(LinkList& L, int n) { //initList(L); LNode* p;//用于开辟新的结点 for (int i = 0; i < n; i++) { p = new LNode; cin >> p->data; //将新结点插入到头结点后 p->next = L->next; L->next = p; } }//头插法 //尾插法建表 void InputList_R(LinkList& L, int n) { //initList(L); LNode* p;//用于开辟新的结点 LNode* r;//用于指向表尾结点 r = L; for (int i = 0; i < n; i++) { p = new LNode; cin >> p->data; //将新结点插入到表尾后 r->next = p; r = p; } r->next = NULL; }//尾插法 //输出 void ShowList(LinkList L) { //if (L == nullptr) { // cout << "链表未初始化" << endl; // return; //} LNode* p; p = L->next; while (p) { cout << p->data << " "; p = p->next; } } //尾部插入 void insertR(LinkList& L) { LNode* p; p = new LNode; p->next = NULL; cout << "请输入在表尾插入的结点:" << endl; cin >> p->data; LNode* r; r = L; while (r->next) {//找到尾结点 r = r->next; } r->next = p; } //表长 int getLength(LinkList L) { LNode* p = L->next; int i = 0; while (p) { p = p->next; i++; } return i; } //读表元 bool GetElem(LinkList L, int i, ElemType& e) { //L是带头结点的单链表,读L的第i个元素,用e返回其值 LNode* p = L->next;int j = 1; //指针p指向ai while (p && j < i) { p = p->next; j++; } //指针p右移i-1次 if (!p || j > i)return false; //i非法 !p:i太大,j>i:i太小 e = p->data; return true; } //GetElem O(n) //删除 bool ListDelete(LinkList L, int i, ElemType& e) { L是带头结点的单链表,删除ai,用参数e带回被删结点的值 //LNode* p = L;int j = 0; //p指向头结点,j是计数器 //while (p && j < i - 1) { p = p->next; j++; } //p指向ai-1 //if (!p || j > i - 1||i > getLength(L)) return false; // i非法 //LNode* q = p->next; e = q->data; //p->next = q->next; free(q); //修改指针 //return true; if (i<0 || i>getLength(L)) return false; // L是带头结点的单链表,删除ai,用参数e带回被删结点的值 LNode* p = L;int j = 0; //p指向头结点,j是计数器 while (p && j < i - 1) { p = p->next; j++; } //p指向ai-1 if (!p || j > i - 1||i > getLength(L)) return false; // i非法 LNode* q = p->next; e = q->data; p->next = q->next; free(q); //修改指针 return true; } // ListInsert O(n) //插入 bool ListInsert(LinkList& L, int i, ElemType e) {//可以空链表从第一个位置插入 // L是带头结点的单链表,在ai之前插入新结点e LNode* p = L; int j = 0; //p指向头结点,j是计数器 while (p && j < i - 1) { p = p->next; j++; }//找指向ai-1的p if (!p || j > i - 1) return false;//i非法,i大于表长或i<1 LNode* s = (LNode*)malloc(sizeof(LNode)); s->data = e; s->next = p->next; p->next = s; //修改指针 return true; } //删除某个元素 bool Delete(LinkList& L, ElemType e) { LNode* p = L->next; LNode* q = L; while (p && p->data!=e) { p = p->next; q = q->next; } if (p) {//条件为p才能正常运行 ,是因为当删除不在链表的值时,p最后会p==NULL这时候就不存在p->data,从而无法判断p->data是否为e,因为此时p->data非法 q->next = p->next; free(p); return true; } else return false; } // 逆置 void ReverseList(LinkList& L) { LinkList p = L->next; // p 指向原链表的第一个元素 L->next = NULL; // 先将原链表的头节点的 next 置为空 while (p) { LinkList temp = p; // 保存当前节点 p = p->next; // p 指向下一个节点 temp->next = L->next; // 将当前节点的 next 指向新链表的头节点的下一个节点 L->next = temp; // 将当前节点插入到新链表的头部 } } //合并 void MergeList(LinkList La, LinkList Lb, LinkList& Lc) { //将有序线性表La和Lb归并成有序线性表Lc int i, j; i = j = 1; int k = 0; initList(Lc); //初始化 int ai, bj; int La_len = getLength(La); int Lb_len = getLength(Lb);//求表长 while (i <= La_len && j <= Lb_len) { //La和Lb均非空 GetElem(La, i,ai); GetElem(Lb, j,bj);//读ai和bj if (ai <= bj) { ListInsert(Lc, ++k, ai); i++; } else { ListInsert(Lc, ++k, bj); j++; } } while (i <= La_len) { //La中剩余元素一一插入Lc GetElem(La, i++, ai); ListInsert(Lc, ++k, ai); } while (j <= Lb_len) { //Lb中剩余元素一一插入Lc GetElem(Lb, j++, bj); ListInsert(Lc, ++k, bj); } }// MergeList //集合的交集 void IntersectList(LinkList La, LinkList Lb, LinkList& Lc) { LNode* i = La->next; LNode* j = Lb->next; int k = 0; LNode* x; while (i) { while (j && i->data != j->data) { j = j->next; } if (j) { x = Lc->next; while (x && x->data != j->data) { x = x->next; } if(!x) ListInsert(Lc, ++k, j->data); } i = i->next; j = Lb->next; } } //集合的差集 void DifferenceList(LinkList La, LinkList Lb, LinkList& Lc) { LinkList Ld; initList(Ld); IntersectList(La, Lb, Ld); LNode* i = La->next; LNode* j = Ld->next; int k = 0; while (i) { while (j && j->data != i->data) j = j->next; if (!j) { ListInsert(Lc, ++k, i->data); } i = i->next; j = Ld->next; } } //集合的补集 void ComplementList(LinkList La, LinkList Lb, LinkList& Lc) { //Lb为全集,La为求补集的集合,Lc为La关于Lb的补集 DifferenceList(Lb, La, Lc); }
- 静态链表的插入和删除操作同样不需要移动元素,仅需修改游 标(指针),所以静态链表仍具有链式存储结构的主要优点, 而且指针的修改也与动态单链表中的插入和删除算法类似。
- 静态链表以next==-1作为其结束的标志
- 通常较稳定的线性表选择顺序存储,而频繁进行插入、删除操作的线性表宜选择链式存储
- 给定n个元素的一维数组,建立一个有序单链表最低时间复杂度为O(nlogn)
- 双链表中的按值查找和按位查找的操作与单链表相同,但双链表在插入和删除操作的实现上时间复杂度仅为O(1)
- 用尾指针引导的循环单链表:访问表头和表尾都很方便
- 用头指针引导的循环单链表:能直接访问表头,但不能直接访问表尾
- 与单链表相比,双链表的优点之一是访问前后相邻结点更灵活
- 某线性表用带头结点的循环单链表存储,头指针为head,当head->next->next=head时,线性表长度可能是0或1
例题
- (6分)已知首元结点元素为c的不带头结点的单链表L在内存中的存储状态如表1所示。
1)画出对应的单链表(3分)
2)将f存放于1014H处,并插入到单链表L,且f在逻辑上位于a和e之间,请填表2完成操作(3分)
-
表1
地址
元素
链接地址
1000H
a
1010H
1004H
b
100CH
1008H
c
1000H
100CH
d
NULL
1010H
e
1004H
1014H
表2
地址
元素
链接地址
1000H
a
1014H
1004H
b
100CH
1008H
c
1000H
100CH
d
NULL
1010H
e
1004H
1014H
f
1010H
- 设rear是指向非空带头结点的循环单链表尾结点的指针。若想删除链表的首元结点,则应执行( D )操作。
- A. s=rear; rear=rear->next; delete s; B. rear=rear->next->next; delete rear;C. rear=rear->next; delete rear; D. s=rear->next->next; rear->next->next=s->next; delete s;
- 将两个各有n个元素的有序线性表合并成一个有序线性表,元素比较次数最多为( C )。
-
A. n-1 B. n C. 2n-1 D. 2n
- 最坏情况发生在两个线性表的元素交替比较,直到最后一个元素才确定放入顺序。
- 假设有两个带头结点的单链表L1和L2,结点的数据是int类型。编写函数void insert(LinkList &L1, int k, LinkList L2)实现在L1的第k个结点之前倒序插入L2的所有结点(如果k大于L1的长度,则在L1的尾部倒序添加L2的所有结点)。比如L1=(1,2,3,4,5,6)和L2=(10,20,30,40),则在L1的第4个结点之前倒序插入L2后的L1=(1,2,3,40,30,20,10,4,5,6)。还要求写出单向链表LinkList的结构定义。
-
#include <iostream> using namespace std; typedef struct LNode{ int data; struct LNode* next; }LNode,*LinkList; void insert(LinkList &L1,int k,LinkList L2){ LNode* p=L1;int j=1;//找到第k个结点的前驱 while(j<k && p->next){ p=p->next; j++;}//使p指向前驱或者最后一个结点 LNode* q=L2->next; //头插法 while(q){ LNode* temp=q->next; q->next=p->next; p->next=q; q=temp; } }
- 编写算法删除非降序单向链表中的重复结点。如下图所示,如果有多个结点的值相同,则只保留第一个结点。要求写出单向链表的结构定义。
-
include<iostream> using namespace std; typedef struct LNode{ int data; struct LNode* next; }LNode,*LinkList; void fun(LinkList& L) { LNode *p=L, q; while (p!=NULL && p->next!=NULL) if (p->data == p->next->data) { q=p->next; p->next=q->next; delete q; } else p=p->next; }
- 在含n个结点的顺序表中,算法的时间复杂度是O(1)的操作是( A)。
- A.访问第i个结点(1≤i≤n)和求第i个结点的直接前驱(2≤i≤n)
- B.在第i个结点后插入一个新结点(1≤i≤n)
- C.删除第i个结点(1≤i≤n)
- D.将n个结点从小到大排序
- 已知中缀表达式a×(b+c)-d#,使用顺序栈转换为后缀表达式
- 1)当中缀表达式扫描到d时,画出此时顺序栈的情况(3分)
- 2)给出转换而得的后缀表达式(3分)
-
将两个各有n个元素的有序表归并成一个有序表,
最少需要比较n次,最大需要比较2n-1次
第三章 栈(Stack)和队列(Queue)
- 栈:是一种插入和删除操作被限定在表尾的线性表,后进先出
- n个不同元素进栈,出栈元素不同排列的个数为
- 采用顺序存储的栈称为顺序栈;栈空:S.base == S.top ;栈满:S.top – S.base >= S.stacksize;栈长:S.top-S.base
- 栈的应用:数值转换,括号匹配,迷宫问题,表达式求值
- 队列:是一种限定仅在表尾插入,在表头删除的线性表,先进先出
- 循环队列:
- 空队列:Q.front等于Q.rear
- 队满:
- 法1:Q.rear 等于Q.front,并设一个全局变量区分队满还是空队列
- 法2:设置数字计数器(经常求长度时可用)
- 法3:浪费一个元素空间, (Q.rear+1 )%MAXQSIZE 等于Q.front
- 队长: (Q.rear-Q.front+MAXQSIZE)%MAXQSIZE
- 队列在层序遍历的应用:根结点入队;若队空则结束遍历,否则重复下一步操作;队列中的第一个结点出队并访问,若有左孩子则左孩子入队,若有右孩子则右孩子入队,返回上一步
例题
- 若元素按A、B、C、D、E顺序入栈(入栈过程可穿插进行出栈操作),假设元素入栈操作记作U,出栈操作记作O,能否得到出栈序列(1)B、C、A、E、D和(2)D、B、A、C、E?若能得到出栈序列,则用U、O符号串表示元素的入栈、出栈过程,若不能得到出栈序列,则说明理由。
1)出栈序列B、C、A、E、D可以得到。(1分)进栈出栈过程为UUOUOOUUOO。(2分)
2)出栈序列D、B、A、C、E 不可以得到。(1分)因为D出栈后,当前栈顶元素是C,B是栈内元素,所以C一定比B先出栈。该序列中B比C先出栈,所以错误。(2分)
- 为解决计算机主机与打印机速度不匹配问题,通常设一个打印数据缓冲区。主机将要输出的数据依次写入该缓冲区,而打印机则依次从该缓冲区中取出数据。该缓冲区的逻辑结构应该是 ( A )。先进先出
- 队列 B. 栈 C. 线性表 D. 有序表
- 链队中头指针为front,尾指针为rear,则将新结点(用指针x指向)插入队列需要执行的操作是( C )。
- A.front->next = x; front = front->next; x->next = NULL;
- B.rear->next = x; rear = rear->next;
- C.x->next = NULL; rear->next = x; rear = rear->next;
- D.x->next = rear->next; rear = x;
第四章 串
- 串(或字符串):是由零个或多个字符组成的有限序列
- 子串:串中任意个连续的字符组成的子序列称为该串的子串。
- 主串:包含子串的串相应地称为主串;
- 字符在序列中的序号为该字符在串中的位置;
- 串的表示与实现:定长顺序存储表示、堆分配存储表示、块链存储表示
- 串是一种特殊的线性表,其特殊性体现在( B )。
A. 可以顺序存储 B. 数据元素是一个字符
C. 可以链式存储 D. 数据元素可以是多个字符
第六章 树和二叉树
- 递归是树的固有特性。
- 树的特征(树型结构的共同特征):层次性和分支性
- 树的度:树内各结点度的最大值
- 二叉树不是结点的度都不超过2的有序树
- 树和二叉树是两种不同的树形结构,二叉树不是树的特殊形式。
- 先DLR+中LDR 或 中LDR + 后LRD 才能 画出 唯一的二叉树
- 结点的深度是从根开始自顶向下累加;结点的高度是从叶结点自底向上累加
- 在二叉树的第i层上至多有
个结点(i ≥ 1 );
- 深度为k的二叉树上至多有
个结点(k ≥ 1 ) ;
- 设任意一棵二叉树中有
个度为0的结点,
个度为2个结点,则有:
=
;
- 满二叉树:一棵深度为k且有 2k-1个结点的二叉树。即:所有非终端结点的度都等于2,且所有树叶都分布在同一个层次上。
- 完全二叉树:将深度为k,有n个结点的二叉树自上而下,自左向右进行编号,当且仅当编号与深度为k的满二叉树中前n个结点一一对应。
- 完全二叉树的深度为 k=
或 k=
;
- 所有结点的个数为n,度数为0的结点的个数为n0,度数为1的结点的个数为n1,度数为2的结点的个数为n2,度数之和为e
- n0 + n1 + n2 = n n0*0 + n1 *1 +n2 *2 = e n =e+1
- 关于二叉树链式存储的代码https://mp.youkuaiyun.com/mp_blog/creation/editor/144485093https://mp.youkuaiyun.com/mp_blog/creation/editor/144485093https://mp.youkuaiyun.com/mp_blog/creation/editor/144485093https://mp.youkuaiyun.com/mp_blog/creation/editor/144485093https://mp.youkuaiyun.com/mp_blog/creation/editor/144485093https://mp.youkuaiyun.com/mp_blog/creation/editor/144485093https://mp.youkuaiyun.com/mp_blog/creation/editor/144485093https://mp.youkuaiyun.com/mp_blog/creation/editor/144485093https://mp.youkuaiyun.com/mp_blog/creation/editor/144485093
https://mp.youkuaiyun.com/mp_blog/creation/editor/144485093
- 树的存储结构(1)
- 树的存储结构(2)
- 树的存储结构(3)
- 先序遍历树,等价于先序遍历由这棵树转换而成的二叉树;
- 后序遍历树,等价于中序遍历由这棵树转换而成的二叉树;
- 最优二叉树(赫夫曼树):由权值为{w1,w2,...,wn)的n片叶子构成的所有二叉树中,WPL值最小的二叉树。
- 哈夫曼树不一定是最矮的树
- 哈夫曼树形态可能不唯一
- 构造n个叶子的哈夫曼树需要经过n-1次合并,每次合并都要增加一个新结点。所以n个叶子的哈夫曼树上有且仅有2n-1个结点。
- 哈夫曼树上不存在度为1的结点。我们把这种不存在度为1的结点的二叉树称为严格二叉树或正则二叉树。
- 哈夫曼编码
- 平衡二叉树
-
按一组整数序列{40,25,6,53,32,28}的输入序列,试从空树开始构造平衡二叉排序树,画出每加入一个结点时二叉树的状态,若发生不平衡,给出调整结果
-
例题
- 2.在一棵度为4的树T中,若有20个度为4的结点,10个度为3的结点,1个度为2的结点,10个度为1的结点,则树T的叶子结点个数是( B )。
A. 41 B. 82 C. 113 D. 122 - 给定一组字符以及它们出现的权值(括号内的值为各字母出现的权值),D(7),E(31),I(19),L(23),A(11),C(2),S(3),V(4),对这些字符进行哈夫曼编码,
- 画出对应的哈夫曼树;(在构造哈夫曼树时要求每棵子树左孩子的权值不大于右孩子的权值)(4分)
- 左分支标记0,右分支标记1,给出每个字符的哈夫曼编码;(4分)
- 若有编码序列 “000110010110111010101111”,请依据上面的哈夫曼树写出译码结果。 (2分)
-
假设二叉树BT采用二叉链存储结构存储,每个结点存放单个字符,
- 写出二叉树的数据类型定义。(3分)
- 设计一个算法,求该二叉树中从根节点出发的一条最长的路径长度,并输出此路径上各节点的值(按从根出发每次经过的节点的顺序)。(10分)
-
//先求每个节点的高度,再找最长路径时。 //每次从左子树和右子树中,选择较高的子树进入,直到走到叶子节点。 #include<iostream> using namespace std; typedef struct BiNode{ char data; struct BiNode* lchild,*rchild; int h;//二叉树的高度 }BiNode,*BiTree; int Height(BiTree &bt){ if(!bt) return 0;//空树 if(!bt->lchild && !bt->rchild){ bt->h=1;return 1;}//只有根结点 int hl,hr; h1=Height(bt->lchild);//左子树的高度 hr=Height(bt->rchild);//右子树的高度 if(hl>hr){ bt->h=hl+1; return hl+1; else{ bt->h=hr+1; return hr+1; } } void longest(BiTree bt){ BiNode* p =bt; while(p->lchild || p->lchild){ cout<<p->data; if(p->lchild && !p->rchild) p=p->lchild; else if(p->rchild && !p->lchild) p=p->rchild; else if(p->lchild->h>p->rchild->h) p=p->lchild; else p=p->rchild; } //走到叶子了 cout<<p->data<<endl; }
-
/*方法2:由于二叉树中从根节点出发的最长路径一定是从根节点到某个叶子节点的路径,可以求出所有叶子节点到根节点的逆路径,通过比较长度得出最长路径。算法中用形参maxpath[]数组存放最长路径,maxpathlen存放最长路径长度。对二叉树进行递归遍历,当碰到叶子的时候,比较当前路径是否比最长路径更长,如果是,则替换最长路径。*/ typedef struct BiNode{ char data; struct BiNode* Lchild,*Rchild; }BiNode,*BiTree; void Longest2(BiTree BT,char path[],int pathlen,char maxpath[],int &maxpathlen) { if (BT== NULL) return; if (BT->Lchild==NULL && BT->Rchild==NULL) { path[pathlen]=BT->data; //将当前节点放入路径中 pathlen++; if (pathlen>maxpathlen) //通过比较求最长路径 { for (int i=pathlen-1;i>=0;i--) maxpath[i]=path[i]; maxpathlen=pathlen; } return; } path[pathlen]=BT->data; //将当前节点放入路径中 pathlen++; //路径长度增1 Longest2(BT->Lchild,path,pathlen,maxpath,maxpathlen); //递归扫描左子树 Longest2(BT->Rchild,path,pathlen,maxpath,maxpathlen); //递归扫描右子树 } //打印maxpath数组 void printpath(char maxpath[],int maxpathlen) { for (int i=0;i<maxpathlen;i++) cout<<maxpath[i]<<" "; cout<<endl; }
- 一棵完全二叉树上有1001个结点,其中叶子结点的个数是( D)。
- 250 B. 500 C. 254 D. 501
- 已知下列字符{A、B、C、D、E、F、G、H},若各字符的哈夫曼编码依次是0100,10,0000,0101,001,011,11 ,0001
- 写出编码序列“0100011001001001011110101”译码结果(3分)
- 画出对应的哈夫曼树(要求字符权值左小右大)(4分)
- 权值最大的可能字符(2分)
-
-
编程判断用二叉链表存储的二叉树是否是平衡二叉树。
-
1.typedef struct Node{ 2. int data; 3. struct Node *lchild, *rchild; 4.}Node, *BiTree; 5. 6.//如果二叉树T是平衡二叉树,函数isAVL将返回true;否则返回false 7.//函数isAVL通过引用参数height返回二叉树T的高度 8.boolean isAVL(BiTree T, int &height){ 9. if(!T) {height=0; return true;} 10. int l,r,lh,rh; 11. l=isAVL(T->lchild,lh); //后序判定T的左子树 12. r=isAVL(T->rchild,rh); //后序判定T的右子树 13. 14. if(lh>rh) height=lh+1; else height=rh+1; //计算二叉树T的高度height 15. 16. if(!l || !r) return false; //如果T的左子树或右子树不是平衡二叉树,则T也不是平衡二叉树 17. if(T->lchild && T->lchild->data>T->data) return false; //左孩子大于双亲 18. if(T->rchild && T->rchild->data<T->data) return false; //右孩子小于双亲 19. if(lh-rh>1 || lh-rh<-1) return false; //左右子树不平衡 20. 21. return true; //以上4个if语句都不满足则意味着T是平衡二叉树 22.} 23. 24.void main(){ 25. BiTree T=null; 26. //创建包含若干结点的二叉树T 27. boolean is=false; 28. int h=0; 29. is=isAVL(T,h); 30. if(is) cout<<"T是平衡二叉树"; else cout<<"T不是平衡二叉树"; 31.}
第七章 图(Graph)
- 弧或边上带权的图分别称为有向网和无向网。
- 若e=n(n-1)*1/2的无向图称为完全图
- 若e=n(n-1)的有向图称为有向完全图
- 对于无向图,若顶点v和顶点w之间存在边,则v和w互称为邻接点。边(v,w)和顶点v,w相关联。和顶点相关联的边数称为该顶点的度
- 对于有向图,若顶点v和顶点w之间存在弧,则v和w互称为邻接点。(P158:(v,v’)∈A,则称顶点v邻接到顶点v’,顶点v’邻接至顶点v。) 以顶点v为弧尾的弧数称为该顶点的出度; 以顶点v为弧头的弧数称为该定点的入度; 度(TD)=入度(ID)+出度(OD)——有向图有度,出度,入度的概念
- 简单路径:顶点序列中没有重复顶点 简单回路:v与w是同一个顶点
- 连通图:对于无向图,图中任意顶点间都有路径
- 连通分量:若无向图非连通,图中的各极大连通子图
- 强连通图:对于有向图,图中任意顶点间都有路径
- 强连通分量:若有向图非强连通,图中各极大强连通子图
- 连通图中,生成树是一个极小连通子图,由图的n个全部顶点,及足以构成树的n-1条边组成
- 非连通图中,各连通分量的生成树的集合称为生成森林
- 图种类标志:DG---有向图、DN---有向网//网是带权重的图、AG---无向图、AN---无向网
-
邻接矩阵表示法(数组表示法)
typedef struct { //弧结点与矩阵的类型 VRType adj; //VRType为弧的类型。图--0,1;网--权值 InfoType *Info; //与弧相关的信息的指针,可省略 }ArcCell, AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; typedef struct{ //图的类型 VertexType vexs[MAX_VERTEX_NUM]; //顶点向量 AdjMatrix arcs; //邻接矩阵 int vexnum, arcnum; //顶点数,边数 GraphKind kind; //图类型 }MGraph;
- 无向图邻接矩阵的特点:1)对称矩阵 2)顶点Vi的度等于第i行非零元个数,或第i列非零元个数 3)矩阵非零元总数等于边数的2倍
-
邻接表表示法
-
#define MAX_VERTEX_NUM 20 //最大顶点数 typedef struct ArcNode{ //边结点 int adjvex; //邻接点的下标 struct ArcNode *nextarc; //后继链指针 }ArcNode; typedef struct VNode{ //顶点结点 VertexType data; //顶点数据 ArcNode *firstarc; //边链头指针 }VNode, AdjList[MAX_VERTEX_NUM]; typedef struct{ AdjList vertices; //邻接表 int vexnum,arcnum; //顶点数和边数 GraphType kind; //图种类标志 }ALGraph;
- 无向图邻接表的特点:1)顶点Vi的度等于Vi所引导的单链表的长度 2)边结点的个数等于边数的2倍
- 有向图邻接表的特点:1)顶点Vi引导的单链表是出边链,链表的长度等于Vi的出度 2)找一个顶点的出边容易,找入边需要遍历整个邻接表 3)边结点的个数等于边数
-
#include <iostream> #include <stdlib.h> using namespace std; #define MVNum 100 //最大顶点数 #define MAXQSIZE 100// 最大队列长度 #define OK 1 typedef char VerTexType; //假设顶点的数据类型为字符型 typedef int ArcType; //假设边的权值类型为整型 bool visited[MVNum]; typedef struct TreeEdge { VerTexType from; VerTexType to; } TreeEdge; //图的邻接表存储表示 typedef struct ArcNode { //边结点 int adjvex; //该边所指向的顶点的位置 struct ArcNode* nextarc; //AdjList表示邻接表类型 } ArcNode; typedef struct VNode { VerTexType data; //顶点信息 ArcNode* firstarc; //指向下一条边的指针 } VNode, AdjList[MVNum]; typedef struct { AdjList vertices; //邻接表 int vexnum, arcnum; //图的当前顶点数和边数 } ALGraph; int LocateVex(ALGraph G, VerTexType v) { for (int i = 0; i < G.vexnum; ++i) { if (G.vertices[i].data == v) return i; } return -1; } int CreateUDG(ALGraph& G) { cout << "请输入总顶点数,总边数中间以空格隔开:"; cin >> G.vexnum >> G.arcnum; cout << endl; cout << "输入点的名称,如a" << endl; for (int i = 0; i < G.vexnum; ++i) { cout << "请输入第" << (i + 1) << "个点的名称:"; cin >> G.vertices[i].data; G.vertices[i].firstarc = NULL;//初始化表头结点的指针域为NULL } cout << endl; cout << "请输入一条边依附的顶点,如 a b" << endl; for (int k = 0; k < G.arcnum; ++k) { VerTexType v1, v2; int i, j; cout << "请输入第" << (k + 1) << "条边依附的顶点:"; cin >> v1 >> v2; i = LocateVex(G, v1); j = LocateVex(G, v2); ArcNode* p1 = new ArcNode; //生成一个新的边结点 p1->adjvex = j; p1->nextarc = G.vertices[i].firstarc; G.vertices[i].firstarc = p1; ArcNode* p2 = new ArcNode; p2->adjvex = i; p2->nextarc = G.vertices[j].firstarc; G.vertices[j].firstarc = p2; } return OK; } //队列的定义 typedef struct { ArcType* base; int front, rear; } sqQueue; void InitQueue(sqQueue& Q) { //构造一个空队列 Q.base = new ArcType[MAXQSIZE]; if (!Q.base) exit(1); Q.front = Q.rear = 0; } //入队 void EnQueue(sqQueue& Q, ArcType e) { //插入元素e为Q的新的队尾元素 if ((Q.rear + 1) % MAXQSIZE == Q.front) return; Q.base[Q.rear] = e; Q.rear = (Q.rear + 1) % MAXQSIZE; } //出队 void DeQueue(sqQueue& Q, ArcType& u) { u = Q.base[Q.front]; Q.front = (Q.front + 1) % MAXQSIZE; } bool QueueEmpty(sqQueue Q) { //判空 if (Q.front == Q.rear) return true; return false; } void DFS(ALGraph G, int v, int& i, TreeEdge* edge) { //从第v个顶点出发递归,深度优先遍历图G ArcNode* p; cout << G.vertices[v].data << " "; visited[v] = true; for (p = G.vertices[v].firstarc; p != NULL; p = p->nextarc) { if (!visited[p->adjvex]) { edge[i].from = G.vertices[v].data; edge[i].to = G.vertices[p->adjvex].data; i++; DFS(G, p->adjvex, i, edge); } } } void DFSTraverse(ALGraph G,int k, TreeEdge* edge) { //对非连通图G做深度优先遍历 int v; for (v = 0; v < G.vexnum; v++) visited[v] = false; int i = 0; //for (v = 0; v < G.vexnum; v++) { // if (!visited[v]) { DFS(G, k, i, edge); // } //} } void BFS(ALGraph G, int v, TreeEdge* edge) { //按广度优先非递归遍历连通图G sqQueue Q; ArcType u; ArcType w; int i = 0; for (int i = 0; i < G.vexnum; ++i) visited[i] = false; cout << G.vertices[v].data << " "; visited[v] = true; InitQueue(Q); EnQueue(Q, v); while (!QueueEmpty(Q)) { DeQueue(Q, u); ArcNode* p; for (p = G.vertices[u].firstarc; p != NULL; p = p->nextarc) { if (!visited[p->adjvex]) { w = p->adjvex; cout << G.vertices[w].data << " "; visited[w] = true; edge[i].from = G.vertices[u].data; edge[i].to = G.vertices[w].data; i++; EnQueue(Q, w); } } } } void PrintEdge(TreeEdge* edge, int edgeCount) { for (int i = 0; i < edgeCount; i++) { if (edge[i].from != '\0' && edge[i].to != '\0') { cout << edge[i].from << "->" << edge[i].to << " "; } } cout << endl; } int main() { cout << "*******采用邻接表表示法创建无向图*******" << endl << endl; ALGraph G; int i, j; CreateUDG(G); TreeEdge* DFSTreeEdge = new TreeEdge[G.vexnum * (G.vexnum - 1) / 2]; TreeEdge* BFSTreeEdge = new TreeEdge[G.vexnum * (G.vexnum - 1) / 2]; // 初始化边集 for (i = 0; i < G.vexnum * (G.vexnum - 1) / 2; i++) { DFSTreeEdge[i].from = '\0'; DFSTreeEdge[i].to = '\0'; BFSTreeEdge[i].from = '\0'; BFSTreeEdge[i].to = '\0'; } cout << endl; cout << " ***** 邻接表表示法创建的无向图 ***** " << endl; for (i = 0; i < G.vexnum; ++i) { ArcNode* p; p = G.vertices[i].firstarc; cout << G.vertices[i].data << " "; while (p) { cout << "-> " << G.vertices[p->adjvex].data << " "; p = p->nextarc; } cout << endl; } cout << endl; VerTexType c; while (1) { cout << "请输入遍历连通图的起始点:"; cin >> c; int i = LocateVex(G, c); if (i != -1) { cout << "深度优先搜索遍历连通图结果:" << endl; DFSTraverse(G,i, DFSTreeEdge); cout << endl; int dfEdgeCount = 0; while (DFSTreeEdge[dfEdgeCount].from != '\0' && DFSTreeEdge[dfEdgeCount].to != '\0') { dfEdgeCount++; } cout << "深度优先搜索遍历生成树的边集:" << endl; PrintEdge(DFSTreeEdge, dfEdgeCount); cout << "广度优先搜索遍历连通图结果:" << endl; BFS(G, i, BFSTreeEdge); cout << endl; int bfEdgeCount = 0; while (BFSTreeEdge[bfEdgeCount].from != '\0' && BFSTreeEdge[bfEdgeCount].to != '\0') { bfEdgeCount++; } cout << "广度优先搜索遍历生成树的边集:" << endl; PrintEdge(BFSTreeEdge, bfEdgeCount); cout << endl; } else { cout << "该顶点不存在:" << endl; } } // 释放内存 delete[] DFSTreeEdge; delete[] BFSTreeEdge; return 0; }
-
邻接表和邻接矩阵的相互转换
-
#include <iostream> #include <cstring> #define MVNum 100 // 最大顶点数 typedef char VerTexType; // 假设顶点的数据类型为字符型 typedef int ArcType; // 假设边的权值类型为整型 // 图的邻接表存储表示 typedef struct ArcNode { // 边结点 int adjvex; // 该边所指向的顶点的位置 struct ArcNode* nextarc; // 指向下一条边的指针 } ArcNode; typedef struct VNode { VerTexType data; // 顶点信息 ArcNode* firstarc; // 指向第一条边的指针 } VNode, AdjList[MVNum]; typedef struct { AdjList vertices; // 邻接表 int vexnum, arcnum; // 图的当前顶点数和边数 } ALGraph; // 图的邻接矩阵存储形式 typedef struct { VerTexType vexs[MVNum]; // 顶点表 ArcType arcs[MVNum][MVNum]; // 邻接矩阵 int vexnum, arcnum; } AMGraph; // 从邻接矩阵转换到邻接表 void AMToAL(AMGraph& amGraph, ALGraph& alGraph) { alGraph.vexnum = amGraph.vexnum; alGraph.arcnum = amGraph.arcnum; for (int i = 0; i < amGraph.vexnum; ++i) { alGraph.vertices[i].data = amGraph.vexs[i]; alGraph.vertices[i].firstarc = nullptr; for (int j = 0; j < amGraph.vexnum; ++j) { if (amGraph.arcs[i][j] != 0) { // 如果存在边 ArcNode* newArc = new ArcNode(); newArc->adjvex = j; newArc->nextarc = alGraph.vertices[i].firstarc; alGraph.vertices[i].firstarc = newArc; } } } } // 从邻接表转换到邻接矩阵 void ALToAM(ALGraph& alGraph, AMGraph& amGraph) { amGraph.vexnum = alGraph.vexnum; amGraph.arcnum = alGraph.arcnum; for (int i = 0; i < amGraph.vexnum; ++i) { amGraph.vexs[i] = alGraph.vertices[i].data; for (int j = 0; j < amGraph.vexnum; ++j) { amGraph.arcs[i][j] = 0; // 初始化为无边 } ArcNode* arcNode = alGraph.vertices[i].firstarc; while (arcNode != nullptr) { amGraph.arcs[i][arcNode->adjvex] = 1; // 假设权值为 1,若有权值可修改此处 arcNode = arcNode->nextarc; } } }
- 生成树:极小连通子图,包含图中n个顶点但只有构成一棵树的n-1条边。
-
-
拓扑排序
- 拓扑排序:获得拓扑有序(Topological Order)的操作
- 用DFS遍历一个无环有向图,并在DFS算法退栈返回时,打印出相应的顶点,则输出的顶点序列是___A_____。 A)逆拓扑有序的 B)拓扑有序的 C)无序的
-
最小生成树
-
最小生成树:图的生成树不唯一。同样,带权图的生成树也不唯一。树中边权之和称为树的权。所有生成树中权最小的生成树即最小生成树。
-
应用:在n个城市间要建立通信线路。如何确定线路使得花费最小? 方法: 1)普里姆(Prim)算法 2) 克鲁斯卡尔(Kruscal)算法
-
普里姆(Prim)算法,逐步找最小边
-
克鲁斯卡尔(Kruscal)算法,按权的递增次序选边
-
深度优先生成树,广度优先生成树
-
例题
-
所有节点的前驱和后继的个数都没有限制的数据结构是(图)
-
n个顶点的连通无向图,其边的条数至少为n-1,
存在n个顶点的有向图,每个顶点的度最大可以达到2(n-1)
-
在数据结构中,线性结构、树形结构和图形结构数据元素之间的分别存在(一对一)、(一对多)和(多对多)联系。
-
若无向图满足(n-1条边),则该图是树。
-
以下图的叙述中,正确的是(D、C)。
-
A.图与树的区别在于图的边数大于或等于顶点数
-
B.假设有图G=(V,{E}),顶点集
则v’和{E‘}构成G的子图。
-
C.无向图的连通分量指无向图中的极大连通子图
-
D.图的遍历就是从图中某一个顶点出发访遍图中其余顶点
-
以下图的叙述中,正确的是(C、D)。 A.强连通有向图的任何顶点到其它所有顶点都有弧 B. 任意图顶点的入度等于出度 C.有向完全图一定是强连通有向图 D.有向图边集的子集和顶点集的子集可构成原有向图的子图
-
有n个顶点的有向图,至少需要(n)条弧才能保证是连通的。
-
强连通分量是有向图的极大强连通子图
-
在一个含有 n 个顶点的有向连通图中,至少包含 n−1 条弧
-
一个有28条边的非连通无向图至少有(C)个顶点?A.7 B.8 C.9 D.10
-
分析:8个顶点的连通无向完全图共有8*(8-1)/2,所以非连通无向图则有至少9个顶点。
-
具有n个顶点e条边的无向图,若用邻接矩阵为存储结构,则求任意顶点的度数的时间复杂度为O(e)。(X)
-
分析:由于是用邻接矩阵表示的无向图,所以求任意顶点的度数可以直接查找该顶点对应的矩阵的列上的元素个数,故时间复杂度应该为O(n)。
-
对于存储为邻接矩阵的有向图,其边数等于邻接矩阵的(非零元素的个数)。
-
n个顶点的无向图的邻接表最多有(n(n-1))个表节点。
-
假设一个有n个顶点和e条弧的有向图用邻接表表示,则删除与某个顶点v相关的所有弧的时间复杂度是(C)。 A.O(n) B.O(e) C.O(n+e) D.O(n*e)
-
分析:由于最坏情况是,这个顶点和所有顶点都有边,而且在其它顶点的边表中,这些边都在表尾,所以需要访问所有的节点和边一次,复杂度是 O(n+e)。
-
遍历图的过程实质上是(对每个顶点查找其邻接点的过程)。设图G有n个顶点和e条边,则对用邻接矩阵表示的图进行深度或广度优先搜索遍历的时间复杂度为(O(
)),而对用邻接表表示的图进行深度或广度优先搜索遍历时的时间复杂度为(O(n+e))。
-
写算法判别以邻接表方式存储的无向图中是否存在由顶点Vi到Vj的路径(i≠j)
-
bool IsReachable(ALGraph G,int s,int t){ visited[s]=true; if(s==t) return true; for(ArcNode* arc=G.vers[s].firstarc;arc;arc=arc->nextarc) if(!visited[arc->adjvex] && IsReachable(G,arc->adjvex,t)) return true; return false; }
- 判断一个用邻接表表示的有向图是否存在回路,并写出算法思想。
-
#include <iostream> using namespace std; #define MVNum 100 // 最大顶点数 typedef char VerTexType; // 假设顶点的数据类型为字符型 // 图的邻接表存储表示 typedef struct ArcNode { // 边结点 int adjvex; // 该边所指向的顶点的位置 struct ArcNode* nextarc; // 指向下一条边的指针 } ArcNode; typedef struct VNode { VerTexType data; // 顶点信息 ArcNode* firstarc; // 指向第一条边的指针 } VNode, AdjList[MVNum]; typedef struct { AdjList vertices; // 邻接表 int vexnum, arcnum; // 图的当前顶点数和边数 } ALGraph; // 访问状态数组 int mark[MVNum]; // 深度优先搜索判断是否存在回路 bool DFS(ALGraph& G, int v) { if (mark[v] == 1) { // 如果当前节点已经被访问过,说明存在回路 return true; } mark[v] = 1; // 标记为已访问 for (ArcNode* arc = G.vertices[v].firstarc; arc != nullptr; arc = arc->nextarc) { if (DFS(G, arc->adjvex)) { // 递归检查邻接节点 return true; } } mark[v] = 0; // 在返回之前将当前节点标记为未访问,以便其他路径可以使用它 return false; } // 判断有向图是否存在回路 bool HasCycle(ALGraph& G) { for (int i = 0; i < MVNum; ++i) { mark[i] = 0; // 使用 for 循环初始化状态数组 } for (int i = 0; i < G.vexnum; ++i) { if (mark[i] == 0) { // 未访问 if (DFS(G, i)) { // 如果发现回路,返回 true return true; } } } return false; // 没有发现回路 } // 主函数示例(假设图已经构建好) int main() { ALGraph graph; graph.vexnum = 3; graph.arcnum = 3; graph.vertices[0].data = 'A'; graph.vertices[1].data = 'B'; graph.vertices[2].data = 'C'; graph.vertices[0].firstarc = new ArcNode{1, nullptr}; // A -> B graph.vertices[1].firstarc = new ArcNode{2, nullptr}; // B -> C graph.vertices[2].firstarc = new ArcNode{0, nullptr}; // C -> A (形成环) if (HasCycle(graph)) { cout << "The graph has a cycle." << endl; } else { cout << "The graph does not have a cycle." << endl; } return 0; }
- 在一个具有n个顶点的有向图中,构成强连通图时至少有( A )条边。A. n B. n+l C. n-1 D. n/2
- n个顶点的连通图用邻接矩阵表示时,该矩阵至少有( B)个非零元素。
A.n B. 2(n-1) C. n/2 D. n2 -
编写函数将用二叉链表存储的二叉树T转化为存储结构为邻接矩阵的有向图。还要求写出二叉链表和邻接矩阵的结构定义。
-
typedef struct Node { int data; struct Node *lchild, *rchild; } TreeNode, *BiTree; #define N 100 typedef struct { int vexs[N]; // 有向图的结点列表 int adjMatrix[N][N]; // 邻接矩阵,n为树中结点个数 int vexnum; // 结点数 } AMGraph; void BiTree2adjMatrix(BiTree T, AMGraph &G, int &id) { // T不等于null, id是T结点在邻接矩阵中的编号 G.vexs[id] = T->data; // 结点T作为G中编号为id的结点 if (!T->lchild && !T->rchild) { // T是叶子结点 // 啥也不做 } else if (T->lchild && !T->rchild) { // T只有左孩子 G.adjMatrix[id][id + 1] = 1; // 创建一条从结点T到结点T->lchild(编号为id+1)的有向边 BiTree2adjMatrix(T->lchild, G, ++id); } else if (!T->lchild && T->rchild) { // T只有右孩子 G.adjMatrix[id][id + 1] = 1; // 创建一条从结点T到结点T->rchild(编号为id+1)的有向边 BiTree2adjMatrix(T->rchild, G, ++id); } else { // T有左右孩子 G.adjMatrix[id][id + 1] = 1; // 创建一条从结点T到结点T->lchild(编号为id+1)的有向边 int src = id; // src保存结点T的编号 BiTree2adjMatrix(T->lchild, G, ++id); // 递归地将T左子树转化为有向图 G.adjMatrix[src][id + 1] = 1; // 创建一条从结点T到结点T->rchild(编号为当前id+1)的有向边 BiTree2adjMatrix(T->rchild, G, ++id); // 递归地将T右子树转化为有向图 } } void main() { BiTree T = nullptr; // 创建包含若干结点的二叉树T AMGraph G; // 初始化有向图G for (int i = 0; i < N; i++) for (int j = 0; j < N; j++) G.adjMatrix[i][j] = 0; int id = 1; // 图中结点的编号 BiTree2adjMatrix(T, G, id); // 实现将二叉树T转化为邻接矩阵G G.vexnum = id; }
-
给出基于邻接表结构求各顶点入度算法的时间赋值度(设顶点数为n,边数为e)
O(n+e)
Void CalulateIndegree(ALGraph G, int &indegree[]){
-
#include <iostream> #include <vector> using namespace std; typedef int VerTexType; // 假设顶点类型为整数 #define MVNum 100 // 最大顶点数 typedef struct ArcNode { // 边结点 int adjvex; // 该边所指向的顶点的位置 struct ArcNode* nextarc; // 指向下一条边的指针 } ArcNode; typedef struct VNode { VerTexType data; // 顶点信息 ArcNode* firstarc; // 指向下一条边的指针 } VNode, AdjList[MVNum]; typedef struct { AdjList vertices; // 邻接表 int vexnum, arcnum; // 图的当前顶点数和边数 } ALGraph; // 初始化图 void initGraph(ALGraph &G) { G.vexnum = 0; G.arcnum = 0; for (int i = 0; i < MVNum; ++i) { G.vertices[i].firstarc = nullptr; } } // 添加边 void addEdge(ALGraph &G, int from, int to) { ArcNode* newArc = new ArcNode{to, G.vertices[from].firstarc}; G.vertices[from].firstarc = newArc; G.arcnum++; } // 计算入度 void CalculateIndegree(ALGraph &G, int indegree[]) { // 初始化入度数组为0 for (int i = 0; i < G.vexnum; ++i) { indegree[i] = 0; } // 遍历每个顶点及其邻接链表,计算入度 for (int i = 0; i < G.vexnum; ++i) { ArcNode* arc = G.vertices[i].firstarc; while (arc != nullptr) { indegree[arc->adjvex]++; // 增加指向该节点的入度计数 arc = arc->nextarc; } } } // 打印图的信息(可选) void printGraph(const ALGraph &G) { for (int i = 0; i < G.vexnum; ++i) { cout << "Vertex " << G.vertices[i].data << ": "; ArcNode* arc = G.vertices[i].firstarc; while (arc != nullptr) { cout << " -> " << G.vertices[arc->adjvex].data; arc = arc->nextarc; } cout << endl; } } int main() { ALGraph G; initGraph(G); // 假设我们有5个顶点,分别用0到4表示,并设置其数据 for (int i = 0; i < 5; ++i) { G.vertices[i].data = i; G.vexnum++; } // 添加一些边,例如: addEdge(G, 0, 1); // 从0到1 addEdge(G, 0, 2); // 从0到2 addEdge(G, 1, 2); // 从1到2 addEdge(G, 2, 3); // 从2到3 addEdge(G, 3, 4); // 从3到4 cout << "Graph adjacency list:" << endl; printGraph(G); int indegree[MVNum]; // 存储入度的数组 CalculateIndegree(G, indegree); cout << "In-degrees:" << endl; for (int i = 0; i < G.vexnum; ++i) { cout << "Vertex " << G.vertices[i].data << " has in-degree: " << indegree[i] << endl; } return 0; }
第九章 查找
-
查找表的存储结构:线性结构:顺序表,链表 树形结构:二叉排序树,B-树 散列结构:哈希表(散列表)
-
查找性能的评价指标----平均查找长度ASL: 在查找过程中,给定值K与关键字比较次数的期望值
-
-
静态查找表:查找表被创建后,一般不做修改和更新,即不具有插入和删除操作;
-
动态查找表:既查找,又修改
-
静态查找表
-
顺序查找:这里“顺序”的含义不是查找表是顺序存储的,而是顺着表的自然次序逐个比较。1)对表的限制最少 2)不仅适用于顺序存储结构,而且适用于链式存储结构 3)时间性能最差,O(n) 4)等概情况下查找成功时ASL=(n+1)/2 5)最朴素的查找方法
-
折半查找:
-
int Search_Bin(SSTable ST, KeyType K){ int low=1, high=ST.length; int mid; while(low<=high){ mid=(low+high)/2; if(K<ST[mid].key) high=mid-1; //在左区间继续查找 else if(K>ST[mid].key) low=mid+1; //在右区间继续查找 else return mid; //查找成功的出口 }//while return 0; //查找失败的出口 }//Search_Bin
-
查找成功的过程是自树根沿树枝到达目标结点,K与关键字的最大比较次数不超过树高
-
-
-
索引顺序查找: 1)查找性能介于顺序查找和折半查找之间; 2)对表的限制也是介于顺序查找和折半查找之间; 3)查找表的特点:分块有序,块内无序,块间有序。第i块的最大关键字小于第i+1块的最小关键字; 4) 除了查找表外,还需要建一个索引表,查找分两级进行。
-
-
typedef struct{ KeyType key; //块内最大键值 int stadr; //块起始地址 }IndexItem; typedef struct{ IndexItem *elem; //索引表基地址 int length; //索引表长度 }IndexTable; int search_Idx( SSTable ST, IndexTable ID, KeyType K ) { low=0;high=ID.length-1; found=FALSE; if(K>ID.elem[high].key) return 0; while(low<=high && !found){//折半查找索引表 mid=(low+high)/2; if(K<ID.elem[mid].key) high=mid-1; else if(K>ID.elem[mid].key) low=mid+1; else {found=TRUE; low=mid;} }//while *** 下一步顺序查找ST表的第low块 *** s=ID.elem[low].stadr; //确定ST表的查找区间s..t if(low<ID.length-1) t=ID.elem[low+1].stadr-1; else t=ST.length; //low==ID.length-1最后一块 for(i=s; i<=t&&ST.elem[i].key!=K; i++);//查找ST表 if(i<=t) return i; //查找成功,返回目标在ST表的下标i else return 0; //查找失败, 返回0 }//search_Idx
-
-
动态查找表
- 二叉排序树:
- 二叉排序树的一个重要性质:中序序列是一个有序序列
- 查找的非递归算法
-
Status BSTSearch(BiTree bt, KeyType K, BiTree &p, BiTree &f){ //在根为bt的二叉排序树上查找键值等于K的记录 //查找成功,用指针p指向目标;f指向目标的双亲,f初值为NULL p=bt; while(p){ if(K<p->key){f=p;p=p->lchild;} //左子树上继续找 else if(K>p->key) {f=p; p=p->rchild;} //右子树上继续找 else return TRUE; //查找成功 }//end while return FALSE; //查找失败 }
- 相关算法https://mp.youkuaiyun.com/mp_blog/creation/editor/144529699https://mp.youkuaiyun.com/mp_blog/creation/editor/144529699https://mp.youkuaiyun.com/mp_blog/creation/editor/144529699https://mp.youkuaiyun.com/mp_blog/creation/editor/144529699https://mp.youkuaiyun.com/mp_blog/creation/editor/144529699https://mp.youkuaiyun.com/mp_blog/creation/editor/144529699https://mp.youkuaiyun.com/mp_blog/creation/editor/144529699https://mp.youkuaiyun.com/mp_blog/creation/editor/144529699https://mp.youkuaiyun.com/mp_blog/creation/editor/144529699
https://mp.youkuaiyun.com/mp_blog/creation/editor/144529699
-
散列表(哈希表)
-
- 装填系数反映了哈希表的装满程度,是影响哈希表查找性能的重要参数
- 数字分析法仅适用于事先明确知道表中所有关键字每一位数值的分布情况。它完全依赖于关键字集合。如果换一个关键字集合,选择哪几位要重新决定。
例题
- (本题10分)将关键字序列(7,8,30,11,18, 9, 14)散列存储到散列表中,散列表长为10,散列函数为 H(key)=(key * 3) % 7,采用线性探测法处理冲突。
- 给出关键字的散列地址,并构造散列表。(8分)
- 计算散列表的装载因子α。(2分)
- 已知二叉链表存储的二叉排序树bst: 1)(6分)在bst中查找关键字为key的元素,返回值表示查找是否成功,并分析算法的时间复杂度 //若找到p为指向元素的指针,否则p为NULL int searchBST(BiTree bst, keyType key, BiTree &p); 2)(6分)删除该二叉排序树的最大值和最小值。(假设树中元素的个数大于2)
- 采用线性探测法处理冲突,可能要探测多个位置,在查找成功的情况下,所探测的这些位置上的关键字( A)。
- A.不一定都是同义词
- B.一定都是同义词
- C.一定都不是同义词
- D.都相同
-
有一个空的Hash表,用线性探测法解决冲突,依次插入n个相同的关键字需要做_次线性探测
-
- 从19个元素中查找其中任意一个元素,如果最多进行4次元素比较,则采用的查找方法只可能是(C )。
-
A.顺序查找 B. 折半查找 C. 散列查找 D. 二叉排序树
-
- (12分)设有一个以二叉链表为存储结构的二叉排序树T(T中的结点值均不相同),试编写函数在T中查找值为k的结点在中序遍历序列中的直接后继,并在函数退出时返回该直接后继结点。要求写出二叉链表的结构定义。
-
//二叉链表的存储结构 typedef struct BiTNode{ int data; struct BiTNode *lchild, *rchild; }BiTNode, *BiTree; BiTNode * FindSuccNode(BiTree T, int k) { if (!T) return NULL; if (T->data == k) { BiTNode *p=T->rchild; while(p && p->lchild) p=p->lchild; if (p) return p; else return NULL; } else if (T->data>k) return FindSuccNode(T->lchild,k); else return FindSuccNode(T->rchild, k); // T->data<k }
第十章 排序
- 操作对象:同类型数据元素的集合。
- 操作目标:将数据元素的无序序列排列成按关键字值有序的序列。
- 稳定排序算法:有:Ri.key == Rj.key 假设排序前Ri的位置排列在Rj之前;经排序后若Ri的位置仍然排列在Rj之前; eg.冒泡,插入
- 非稳定排序算法:不能保证这一点
- 内部排序----待排序的记录全部存放在内存;
- 外部排序----一部分放在内存,一部分在外存。排序过程中存在着内、外存的数据交换。
- 排序的基本动作:①比较 ②移动
- 排序性能的评价:对比较次数和移动次数的评估
- 先进排序算法: 时间复杂度----O(nlogn)
- 一般排序算法 时间复杂度----O(n2)
- 堆排序
- 对初始状态为递增序列的表按递增顺序排序,最省时间的是__C_算法,最费时间的是B___算法。 A.堆排序 B.快速排序 C.插入排序 D.归并排序
- 升序建大顶堆,降序建小顶堆
- 归并排序
- 只涉及了我们要考排序的性能(画的有点丑请见谅)
例题
- 以下排序算法中,某一趟排序结束后未必能选出一个元素放在其最终位置上的是( C )。
A. 堆排序 B. 冒泡排序 C. 直接插入排序 D. 快速排序
- 对大部分元素已有序的数组进行排序时,使用直接插入排序比简单选择排序更好,其原因是( A )。
- 直接插入排序过程中元素之间的比较次数更少
- 直接插入排序过程中所需要的辅助空间更少
- 直接插入排序过程中元素的移动次数更少
- A.① B. ③ C. ①、② D. ①、②、③
- 对右边数组进行堆排序:1, 8, 2, 7, 3, 5, 6, 4。首先把这个无序数组建堆,得到的大顶堆按照数组中的顺序,全部数据是_____________。第一趟(把最大元素交换到数组最后,再完成筛选)以后的数组全部数据是___________,第二趟(把第二大元素交换到数组的倒数第二个位置,再完成筛选)以后的数组全部数据是___________,第三趟(把第三大元素交换到数组的倒数第三个位置,再完成筛选)以后的数组全部数据是__________。(本题的每个空里面都需要按照数组中元素的顺序写,每两个元素之间用逗号隔开)
-
8,7,6,4,3,5,2,1 , 7,4,6,1,3,5,2,8 , 6,4,5,1,3,2,7,8 , 5,4,2,1,3,6,7,8 。