数据结构期末复习 By Persona_owl
第一章 绪论
1. 基本概念和术语
-
数据:计算机操作的对象的总称,是信息的符号表示形式。
-
数据元素: 数据的基本单位,通常作为一个整体进行处理,由更小的数据项组成。数据项是数据不可分割的最小单位。
-
数据结构: 存在特定关系的数据元素集合,包括逻辑结构和物理结构。
- 逻辑结构:数据元素之间的逻辑关系,如线性、树形、图形。
- 物理结构(存储结构):数据逻辑结构在计算机中的表示,包括顺序存储和链式存储。
-
顺序存储结构:数据存放在地址连续的存储单元里,逻辑关系和物理关系一致。
-
链式存储结构:数据元素存放在任意存储单元,逻辑关系通过指针反映。
2. 算法的描述和分析
- 算法的特征:有穷性、正确性、可行行、输入、输出
- 算法的设计要求:正确性、可读性、健壮性、时间效率和存储量需求
- 算法的时间复杂度分析
第二章 线性表
1. 线性表的类型定义
- 线性表是由n个(n≥0)相同类型数据元素构成的有限序列,其中n为线性表的长度。
- 元素之间有顺序,第一个元素无前驱,最后一个元素无后继,其他每个元素都有且只有一个前驱和后继。
- 线性表是有限的,即元素个数有限。
template<class T>
class List {
public:virtual bool IsEmpty()=0;virtual T GetElem(int i)=0;virtual int LocateElem(T e)=0;virtual int Length()=0;virtual Status Insert (int i, T e)=0; //在i位置插入evirtual Status Delete (int i, T &e)=0; //删除在i位置的元素virtual Status Insert (T e)=0; //在线性表的末尾插入e
};
2. 线性表的顺序表示和实现
-
线性表的顺序存储结构的表示:
- 定义:线性表的元素存储在一组地址连续的存储单元中。
-
顺序存储结构的特点:
- 逻辑顺序与物理顺序一致。
- 随机存取存储结构,存取每个元素的时间相同。
-
顺序存储结构地址计算公式:
- 对于线性表(a1,a2, …, ai-1,ai, …, an),每个元素ai所占存储空间为L,则Loc(ai)=Loc(a1)+(i-1)*L。
template <class T>
class SqList: List<T> {
public:T *elem; //保持不变,NULL 不存在栈int length; //实际存放元素的个数int listsize; //可以容纳的最大元素的个数
public:SqList();~SqList();bool IsEmpty();T GetElem(int i);int LocateElem(T e);int Length();virtual Status Insert (int i, T e); //在i位置插入evirtual Status Delete (int i, T &e); //删除在i位置的元素virtual Status Insert (T e); //在线性表的末尾插入evirtual void InputList();void OutputList();void Union(SqList<T>& lb);
};
-
算法关注:
-
插入元素
template <class T> Status SqList<T>::Insert(int i, T e) {// 确保索引有效if (i < 1 || i > length + 1)return ERROR;// 检查当前列表大小是否足够if (length >= listsize){// 分配新的内存并增加大小T *newbase = new T[listsize + ListIncrease];if (!newbase)return ERROR;// 使用数组索引复制元素到新数组for (int j = 0; j < length; j++)newbase[j] = elem[j];// 更新元素数组和列表大小delete[] elem; // 释放旧的内存elem = newbase;listsize += ListIncrease;}// 向前移动元素以为新元素腾出空间for (int j = length - 1; j >= i - 1; --j)elem[j + 1] = elem[j];// 插入新元素elem[i - 1] = e;++length;return OK; }
-
删除元素
template<class T> Status SqList<T>::Delete(int i, T &e){// 删除位置i的元素// 1<=i<=ListLength_Sq(L)if (i < 1 || i > length) return ERROR;// 使用数组索引访问元素e = elem[i - 1];// 向前移动元素以覆盖删除的元素for (int j = i; j < length; ++j) {elem[j - 1] = elem[j];}// 减少长度--length;return OK; }
-
3.线性表的链式存储
- 定义:用一组任意的存储单元(可能不连续)存储线性表的数据元素。
- 结点结构:包含数据元素和直接后继结点的地址信息(指针域)。
- 链式存储结构的种类
- 单链表:每个结点只含一个指向下一个元素的指针。
- 循环链表:尾结点的指针域指向头结点,形成一个环。
- 特点:没有NULL指针,遍历终止条件是指定指针,如头指针或尾指针。
- 双向链表:每个结点有两个指针域,分别指向直接前驱和直接后继。
- 链式结构的特点:
- 逻辑顺序与物理顺序:可能不一致。
- 存取方式:顺序存取,存取每个元素必须从第一个元素开始遍历。
template<class T>
struct Node
{T data;Node<T> *next;
};template<class T>
class LinkedList:List<T> {
public:Node<T> *head;
public:LinkedList();~LinkedList();bool IsEmpty();T GetElem(int i);int LocateElem(T e);int Length();virtual Status Insert (int i, T e); //在i位置插入evirtual Status Delete (int i, T &e); //删除在i位置的元素virtual Status Insert (T e); //在线性表的末尾插入evirtual void OutputList();virtual void InputList(); //正序建表void InputList_R(); //正序建表Status Reverse();void Union(LinkedList<T>& lb);
};
-
算法关注(单链表)
-
插入操作
template<class T> Status LinkedList<T>::Insert(int i, T e){Node<T> *p, *s;p = head;int j = 0;while (p && j < i - 1) { // 寻找第i-1个结点p = p->next;++j;}if (!p || j > i - 1) return ERROR; // i小于1或者大于表长s =new Node<T>; // 生成新结点s->data = e; s->next = p->next; // 插入L中,在第i-1个元素p之后插入sp->next = s;return OK; }
-
删除操作
template<class T> Status LinkedList<T>::Delete(int i, T &e){Node<T> *p, *q;p = head;int j = 0;while (p->next && j < i - 1) { // 寻找第i个结点,并令p指向其前趋p = p->next;++j;}if (!(p->next) || j > i - 1) return ERROR; // 删除位置不合理q = p->next;p->next = q->next; // 删除并释放结点e = q->data;delete q;return OK; }
-
正序建表(尾插法)
// 建立带表头结点的单链线性表L template <class T> void LinkedList<T>::InputList() {Node<T> *p, *last;last = head; // 保留last指针int n;std::cout << "请输入元素个数:";std::cin >> n;for (int i = 0; i < n; i++){p = new Node<T>;std::cout << "请输入第" << i + 1 << "个元素:";std::cin >> p->data;last->next = p; // 插入到表尾last = p;}last->next = NULL; }
-
逆序建表(头插法)
// 建立带表头结点的单链线性表L template <class T> void LinkedList<T>::InputList_R() {Node<T> *p;int n;std::cout << "请输入元素个数:";std::cin >> n;for (int i = 0; i < n; i++){p = new Node<T>; // 生成新结点std::cout << "请输入第" << i + 1 << "个元素:";std::cin >> p->data;p->next = head->next;head->next = p; // 插入到表头} }
-
4.线性表的应用
第三章 栈和队列
1.范型编程思想
2.栈的定义和实现
-
栈的定义:
- 定义:栈(Stack)是一种线性表,其插入和删除操作限制在同一端,称为栈顶(Top),另一端称为栈底(Bottom)。空栈指没有元素的栈。
- 特性:栈中的元素遵循后进先出(LIFO)的原则,即最后插入的元素最先被删除。
- 操作:插入操作称为进栈或入栈,删除操作称为出栈或退栈。栈满入栈为上溢,栈空出栈为下溢
-
顺序栈
template <class T> class SqStack {T *base; // 保持不变,NULL 不存在栈T *top; // 栈顶,指向不用(空)元素,与定义不同int stacksize;public:SqStack();~SqStack();Status Push(T e);Status Pop(T &e);Status GetTop(T &e);int Length();Status IsEmpty();void DispStack(); };
-
算法关注:
-
入栈
template <class T> Status SqStack<T>::Push(T e) { // 入栈操作if (top - base >= stacksize)return FULL;*top++ = e; // 先赋值,再加指针return OK; }
-
取栈顶
template <class T> Status SqStack<T>::GetTop(T &e) { // 取栈顶元素if (top == base)return ERROR;e = *(top - 1);return OK; }
-
出栈
template <class T> Status SqStack<T>::Pop(T &e) { // 出栈操作if (top == base)return EMPTY;e = *--top; // 先减指针,再取值return OK; }
-
-
链栈
template<class T> struct Node {T data;Node<T> * next; };template<class T> class LinkedStack { private:Node<T> *top; public:LinkedStack();~LinkedStack();Status Push(T e);Status Pop(T &e);Status GetTop(T &e);int Length();Status IsEmpty( );void DispStack(); };
-
算法关注:
-
入栈
template <class T> Status LinkedStack<T>::Push(T e) {Node<T> *p = new Node<T>;if (!p)return ERROR;p->data = e;p->next = top->next;top->next = p;return OK; }
-
取栈顶
template <class T> Status LinkedStack<T>::GetTop(T &e) {Node<T> *p;if (top->next == NULL)return EMPTY;else{p = top->next;e = p->data;return OK;} }
-
出栈
template <class T> Status LinkedStack<T>::Pop(T &e) {Node<T> *p;if (top->next == NULL)return EMPTY;p = top->next;e = p->data;top->next = p->next;delete p;return OK; }
-
3.栈的应用
- 进制转化
- 表达式求值:中缀表达式,前缀表达式,后缀表达式
4.队列的定义和实现
-
定义:队列(Queue)是一种先进先出(FIFO)的线性表,只允许在表的一端进行插入(队尾,Rear),在另一端删除元素(队头,Front)。
-
顺序队列
template <class T> class SqQueue { private:T *base;T *front;T *rear;int queuesize; public:SqQueue();~SqQueue();Status EnQueue(T e);Status DeQueue(T &e);int Length();Status IsEmpty();void DispQueue(); };
-
算法关注:
-
入队
template <class T> Status SqQueue<T>::EnQueue(T e) {if (rear - base >= queuesize)return FULL;*rear++ = e; // 先赋值,再加指针return OK; }
-
出队
template <class T> Status SqQueue<T>::DeQueue(T &e) {if (rear == front)return EMPTY;e = *front++; // 先减指针,再取值return OK; }
-
-
顺序队列存在的问题:
-
真溢出:当front=0, rear=M时,再有元素入队发生溢出。
-
假溢出:当front≠0, rear=M时,再有元素入队发生溢出。
-
解决办法:循环队列,注意可以留一个空间不存放元素,这样判满更方便
- 入队:
sq[rear] = x; rear = (rear + 1) % M;
- 出队:
x = sq[front]; front = (front + 1) % M;
- 队空:
front == rear
- 队满:
(rear + 1) % M == front
- 入队:
-
-
链式队列
template <class T> struct Node {T data;Node<T> *next; };template <class T> class LinkedQueue { private:Node<T> *front;Node<T> *rear;public:LinkedQueue();~LinkedQueue();Status EnQueue(T e);Status DeQueue(T &e);int Length();Status IsEmpty();void DispQueue(); };
-
算法关注:
-
入队
template <class T> Status LinkedQueue<T>::EnQueue(T e) {Node<T> *p = new Node<T>;p->data = e;p->next = NULL;rear->next = p;rear = p;return OK; }
-
出队
template <class T> Status LinkedQueue<T>::DeQueue(T &e) {Node<T> *p;if (front == rear)return EMPTY;p = front->next;e = p->data;front->next = p->next;if (rear == p)rear = front;delete p;return OK; }
-
第四章 串
1.串的定义和实现
-
串的概念和类型定义
- 定义:串(String)是零个或多个字符组成的有限序列,记作 S="a0a1a2...an−1"S="a0a1a2...a**n−1",其中 aia**i 可以是字母、数字或其他字符,串的长度是 nn。
- 空串与空白串:空白串(例如“ ”)长度为1,空串(例如“”)长度为0。
- 子串:任意串是其自身的子串,空串是任意串的子串
-
串的操作
- 串的联接(
Concat
):将两个串连接成一个新的串。 - 求子串(
Sub
):从当前串中提取从指定位置开始的指定长度的子串。 - 串的插入(
Insert
):在串的指定位置插入另一个串。 - 串的删除(
Delete
):删除串中从指定位置开始的指定长度的字符序列。 - 串比较(
Compare
):比较两个串是否相等。
- 串的联接(
-
定长顺序串:
class SString { public:char data[MaxSize];int length;public:SString(const char *str = ""); // 构造函数~SString(void);void Assign(char cstr[]);void Copy(SString str); // 拷贝bool Compare(SString str); // 判断相等int Length(); // 求长度void Display();Status Concat(SString S1, SString S2);SString Sub(int pos, int len);Status Insert(int pos, SString t);Status Delete(int pos, int len);int Index(int pos, SString t);int Index_KMP(int pos, SString t);private:void get_next(SString t, int *next);void get_nextval(SString t, int *nextval); };
-
动态顺序串
class HString { public:char *data;int length;public:HString(const char *str = ""); // 构造函数~HString(void);void Assign(char cstr[]);void Copy(HString str); // 拷贝bool Compare(HString str); // 判断相等int Length(); // 求长度void Display();Status Concat(HString S1, HString S2);HString Sub(int pos, int len);Status Insert(int pos, HString t);Status Delete(int pos, int len);int Index(int pos, HString t);int Index_KMP(int pos, HString t);private:void get_next(HString T, int *next);void get_nextval(HString T, int *nextval);public:HString &operator=(HString str);HString operator+(HString str);bool operator==(HString str);char operator[](int index);public:// 重载输入运算符friend std::istream &operator>>(std::istream &is, HString str){char c;int i = 0;while (1){is >> std::noskipws; // 设置is 读取空白符is >> c;if (c == '\n'){break;}else{str.data[i] = c;i++;}}str.data[i] = '\0';return is;}// 重载输出运算符friend std::ostream &operator<<(std::ostream &os, HString str){for (int i = 0; i < strlen(str.data); i++){os << str.data[i];}return os;} };
2.串的模式匹配算法
-
定义:模式匹配(Pattern Matching)或串匹配(String Matching)是指在主串(目标串)中查找模式串(子串)的位置。
-
BF算法(简单匹配算法)
-
时间复杂度最坏可能为O(nm)
-
匹配失败设i回溯到x位置,则x=i-j,下一个位置即为x+1=i-j+1
int SString::Index(int pos, SString t) /*从串s的pos序号起,串t第一次出现的位置*/ {int i, j;if (t.length == 0 || pos < 0)return 0;i = pos;j = 0;while (i < length && j < t.length)if (data[i] == t.data[j]){i++;j++;}else{i = i - j + 1;j = 0;}if (j >= t.length)return (i - j);elsereturn 0; }
-
-
KMP算法
- 时间复杂度为O(n+m)
- 注意:PPT内的KMP算法默认字符串从0开始,且定义next[0]=-1,因此next[i]表示子串0~i-1中最长前后缀的长度
/*求模式串T的next函数值并存入数组next */ void SString::get_next(SString t, int *next) {int i = 0;next[0] = -1;int j = -1;while (i < t.length){// 从头开始比较或者当前匹配if (j == -1 || t.data[i] == t.data[j]){++i;++j;next[i] = j;} // 第一种情况elsej = next[j]; // 第二种情况} }/* KMP算法是利用next函数求T在主串s中第pos个字符之后的位置。 其中,T非空,0≤pos≤S.length*/ int SString::Index_KMP(int pos, SString t) {int i = pos;int j = 0;int *next;next = new int[t.length];get_next(t, next);while (i < length && j < t.length){if (j == -1 || data[i] == t.data[j]) // P0都不匹配的情况或者当前字符刚好匹配{++i;++j;} /*继续比较后继字符*/elsej = next[j]; /*模式串向右移动*/}if (j > t.length)return (i - j); /*匹配成功*/elsereturn 0; }
- nextval数组即对next进一步修正,相同字母不用再比较
第五章 数组
1.数组的定义和顺序存储
-
数组元素位置计算公式
-
注:以下例子为以a11为数组第一个元素,不包含第0行第0列
-
行优先:Loc(aij)=Loc(a11)+[(i−1)n+(j−1)]×l
-
列优先:Loc(aij)=Loc(a11)+[(j−1)m+(i−1)]×l
-
其中,l为每个元素所占用的存储单元。
-
2.特殊矩阵的压缩存储
- 定义:特殊矩阵是指非零元素或零元素分布有一定规律的矩阵,例如对称矩阵、三角矩阵、对角矩阵等。
- 目的:为了节省存储空间,对这类矩阵进行压缩存储,即对多个相同的非零元素只分配一个存储空间,对零元素不分配空间。
- 区分对称矩阵,对角矩阵,三角矩阵的存储
3.稀疏矩阵的压缩存储
- 定义:稀疏矩阵是指非零元素数量 s远远小于矩阵元素总数 m×n 的矩阵
- 存储方法:由于稀疏矩阵中非零元素分布一般没有规律,需要记录非零元素的值及其所在的行和列位置。常见的存储方法有三元组法、行逻辑连接顺序表和链式结构。
- 三元组法:用一个三元组 (i,j,aij)表示稀疏矩阵的每一个非零元素,所有非零元素构成三元组线性表。
- 我觉得不会考这个
第六章 树
1.树的定义和基本概念
-
树的定义:树是n个结点的有限集,其中:
-
有且仅有一个特定的称为根的结点。
-
其余结点分为m个互不相交的子集,每个子集又是一棵树。
-
-
树的基本术语
- 结点(Node):表示树中的元素,包括数据项及若干指向其子树的分支。
- 结点的度(Degree):结点拥有的子树数。
- 树的度:一棵树中最大的结点度数。
- 叶子结点(Leaf):度为0的结点。
- 分支结点:度不为0的结点。
- 孩子(Child):结点子树的根称为该结点的孩子。
- 双亲(Parent):孩子结点的上层结点。
- 兄弟(Sibling):同一双亲的孩子。
- 结点的层次(Level):从根结点算起,根为第一层,它的孩子为第二层,直到叶子结点。
- 树的深度(Depth):树中结点的最大层次数。
- 路径(Path):从树中某结点出发,能够通过树中结点到达另一个结点的序列。
- 树的路径长度:从根到每个结点的路径长度之和。
- 有序树与无序树:树中各结点的子树从左至右是否有次序。
- 森林(Forest):m棵互不相交的树的集合。
-
树的性质
2.二叉树
-
二叉树的定义和特点
- 定义:二叉树是由n个结点的有限集构成,或为空树,或由一个根结点和两棵互不相交的左子树和右子树构成。
- 特点:
- 定义是递归的。
- 每个结点至多有两颗子树(度不大于2)。
- 子树有左、右之分,其次序不能颠倒。
-
二叉树与树的区别
-
二叉树并不是一种特殊的树
-
二叉树结点的子树要区分左子树和右子树,即使只有一棵子树也要进行区分。
-
-
两种特殊的二叉树
-
满二叉树:深度为k且具有2^k-1个结点的二叉树,每一层上的结点数都是最大结点数。
-
完全二叉树:深度为k,有n个结点的二叉树,其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应。
-
-
二叉树的性质
-
二叉树的存储结构
-
顺序存储结构:用一组连续的存储单元存储二叉树的数据元素,适用于满二叉树和完全二叉树,但可能浪费空间。
- 注意非完全二叉树,需要将左右孩子留空,以保证满足顺序存储的性质
-
链式存储结构:用一个链表来存储二叉树,每个结点用链表中的一个节点来存储,包含三个域:数据域和两个指针域(左孩子、右孩子)。
struct Node {char data;Node *lchild;Node *rchild; };struct PostNode {Node *ptr;int tag; };class BTree { public:Node *root;public:BTree();~BTree();void CreateBTree(char *str);void DispBTree(Node *t);void PreOrderDispTree(Node *t);void InOrderDispBTree(Node *t);void PostOrderDispBTree(Node *t);int getBTreeHeight(Node *t);void DispLeaf(Node *t);void PreOrderDispTree2(Node *t);void InOrderDispBTree2(Node *t);void PostOrderDispBTree2(Node *t);Node *CreateBTreeFromPreMid(char *preorder, char *inorder, int length);Node *CreateBTreeFromPostMid(char *postorder, char *inorder, int length); };
-
3.遍历二叉树和线索二叉树
-
遍历方法:
-
先序遍历:根左右
void BTree::PreOrderDispTree(Node *t) {if (t != NULL){cout << t->data;if ((t->lchild != NULL) || (t->rchild != NULL)){PreOrderDispTree(t->lchild);PreOrderDispTree(t->rchild);}} }void BTree::PreOrderDispTree2(Node *t) {SqStack<Node *> s;Node *p = t;while (p != NULL || !s.IsEmpty()){while (p != NULL) // 遍历左子树{cout << p->data;s.Push(p);p = p->lchild;}// 通过下一次循环中的内嵌while实现右子树遍历if (!s.IsEmpty()){s.Pop(p);p = p->rchild;}} // endwhile }
-
中序遍历:左根右
void BTree::InOrderDispBTree(Node *t) {if (t != NULL){InOrderDispBTree(t->lchild);cout << t->data;InOrderDispBTree(t->rchild);} }void BTree::InOrderDispBTree2(Node *t) {SqStack<Node *> s;Node *p = t;while (p != NULL || !s.IsEmpty()){while (p != NULL) // 遍历左子树{s.Push(p);p = p->lchild;}if (!s.IsEmpty()){s.Pop(p);cout << p->data; // 访问根结点p = p->rchild; // 通过下一次循环实现右子树遍历}} }
-
后序遍历:左右根
void BTree::PostOrderDispBTree(Node *t) {if (t != NULL){PostOrderDispBTree(t->lchild);PostOrderDispBTree(t->rchild);cout << t->data;} }void BTree::PostOrderDispBTree2(Node *t) {SqStack<PostNode> s;PostNode x;Node *p = t;do{//tag表示一个点是否已经访问了右子树while (p != NULL) // 遍历左子树{x.ptr = p;x.tag = 0; // 标记为左子树s.Push(x);p = p->lchild;}while (!s.IsEmpty() && (s.GetTop().tag == 1)){ // tag为R,表示右子树访问完毕,故访问根结点s.Pop(x);p = x.ptr;cout << p->data;}if (!s.IsEmpty()){s.Pop(x);x.tag = 1;s.Push(x);p = s.GetTop().ptr->rchild;}} while (!s.IsEmpty()); }
-
层序遍历:从上至下一层一层遍历
-
-
掌握中序后序求树,中序前序求树
-
线索二叉树
-
定义:线索二叉树是一种特殊的二叉树,其中每个结点有多一个指向其前驱和后继的线索(指针)。
-
线索化:将二叉树的某些空指针域替换为指向该结点在某种遍历序列中的前驱或后继的指针。
-
类型:
-
中序线索二叉树:最常用的线索二叉树,方便查找每个结点的前驱和后继。
-
先序线索二叉树:线索指向结点的后继,查找前驱需要双亲信息。
-
后序线索二叉树:线索指向结点的前驱,查找后继需要双亲信息。
-
-
4.完全二叉树顺序存储
-
完全二叉树的定义和特点
-
定义:深度为k,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时,称为完全二叉树。
-
特点:叶子结点只可能在层次最大的两层上出现。对任一结点,若其右分支下子孙的最大层次为L,则其左分支下子孙的最大层次必为L或L+1。
-
5.树和森林
-
树的存储结构
-
双亲表示法:使用数组存储树的结点,每个结点包含数据域和双亲域,双亲域指示结点的双亲在数组中的位置。
- 特点:找双亲容易,找孩子难。
-
孩子表示法:
- 结点同构:每个结点包含指向所有孩子结点的指针,空间浪费严重。
- 结点不同构:结点指针个数不等,为该结点的度d,节约空间但操作不便。
-
孩子链表表示法:每个结点的孩子结点用单链表存储,再用数组指向每个孩子链表。
- 特点:找孩子容易,找双亲难。
-
带双亲的孩子链表:结合孩子链表和双亲信息,容易找到孩子结点和双亲结点。
-
孩子兄弟表示法(二叉树表示法):用二叉链表作为树的存储结构,结点的两个链域分别指向第一个孩子结点和下一个兄弟结点。
-
特点:操作容易,破坏了树的层次结构。
-
-
-
掌握树、森林、二叉树之间的转换
-
树的遍历:先根遍历,后根遍历
-
树的前序遍历 = 转化的二叉树的先序遍历
-
树的后序遍历 = 转化的二叉树的中序遍历
-
-
森林的遍历:先序遍历,中序遍历
6.哈夫曼树
-
定义:在一棵二叉树中,若带权路径长度达到最小,则称这样的二叉树为最优二叉树,也称为哈夫曼树。
-
基本术语
-
路径和路径长度:从一个结点到其孩子或子孙结点的通路称为路径,通路中分支的数目称为路径长度。
-
结点的权及带权路径长度:结点的权是赋予结点的一个数值,结点的带权路径长度为从根结点到该结点的路径长度与该结点的权的乘积。
-
树的带权路径长度:所有叶子结点的带权路径长度之和。
-
掌握构造哈夫曼树
-
第七章 图
1.图的定义和术语
-
图的基本概念
- 图(Graph):由顶点集V和弧集R构成的数据结构,表示为Graph = (V, R)。
- 有向图:弧有方向,由顶点集和弧集构成的图。
- 无向图:若<v, w>∈VR,则必有<w, v>∈VR,表示顶点v和顶点w之间存在一条边。
-
图的基本术语
-
完全图:无向图中每两个顶点之间都存在一条边;有向图中每两个顶点之间都存在方向相反的两条边。
-
稠密图与稀疏图:稠密图边数接近完全图,稀疏图边数较少。
-
权(Weight):与图的边或弧相关的数,带权的图为网(Network)。
-
子图:图G'是图G的子图,如果V'⊆V且VR'⊆VR。
-
邻接点(Adjacent):如果顶点v和顶点w之间存在一条边,则称v和w互为邻接点。
-
度(Degree):与顶点关联的边的数目。
-
入度(In-degree)*与*出度(Out-degree):对于有向图,入度是以顶点为弧头的弧的数目,出度是以顶点为弧尾的弧的数目。
-
路径(Path):从一个顶点到另一个顶点的边或弧的序列。
-
回路(Cycle):路径中第一个和最后一个顶点相同的路径。
-
简单路径(Simple Path):路径中顶点不重复出现。
-
连通图:任意两个顶点之间都有路径相通的图。
-
连通分量:非连通图中极大连通子图。
-
强连通图:任意两个顶点之间都存在一条有向路径的有向图。
-
强连通分量:非强连通图中极大强连通子图。
-
生成树(Spanning Tree):包含图中全部顶点的极小连通子图,只有n-1条边。
-
生成森林(Spanning Forest):非连通图的生成树集合。
-
2.图的存储结构
-
邻接矩阵
-
无向图:
- 矩阵是对称的,可压缩存储。
- 第i行或第i列中1的个数为顶点i的度。
- 矩阵中1的个数的一半为图中边的数目。
-
有向图:
- 矩阵不一定是对称的。
- 第i行中1的个数为顶点i的出度。
- 第i列中1的个数为顶点i的入度。
- 矩阵中1的个数为图中弧的数目。
-
网(带权图):邻接矩阵中的元素wij表示边(vi,vj)或弧〈vi,vj〉的权。
typedef struct ArcCell {int adj; // 无权图,用1或0表示相邻否;// 对带权图,则为权值。 } ArcCell, AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];class MGraph { public:char vexs[MAX_VERTEX_NUM]; // 顶点信息AdjMatrix arcs; // 弧的信息int vexnum, arcnum; // 顶点数,弧数char *dfs;char *bfs;private:int visited[MAX_VERTEX_NUM];public:MGraph() {};~MGraph() {};// 创建无向图图void CreateUDG(int vnum, int anum, char data[], ArcInfo arcList[]);// 创建有向图图void CreateDG(int vnum, int anum, char data[], ArcInfo arcList[]);void DispGraph(); // 展示图char *DFSTraverse(char v); // 深度优先遍历char *BFSTraverse(char v); // 广度优先遍历MinTree prim(char v); // 最小生成树--PRIM算法MinTree Kruskal(); // 最小生成树--KRUSKAL算法SqList<ShortPath> Dijkstra(char v); // 单源点最短路径算法--Dijkstra算法SqList<ShortPath> Floyd(); // 多源点最短路径算法--Floyd算法char AddressByFloyd();private:int LocateVex(char v); // 根据顶点信息,返回顶点的坐标void DFS(char cVex);void BFS(char cVex);void CreateGraph(int vnum, int anum, char data[], ArcInfo arcList[], int Kind);void PrintMatrix(int a[MAX_VERTEX_NUM][MAX_VERTEX_NUM]); // 打印Floyd矩阵string GetFloydPath(int u, int v, int Path[MAX_VERTEX_NUM][MAX_VERTEX_NUM]); };
-
-
邻接表
-
定义:顺序分配与链式分配相结合的存储方法,包括单链表(存放边的信息)和数组(存放顶点数据)。
-
无向图
- 第i个链表中结点数目为顶点i的度。
- 所有链表中结点数目的一半为图中边数。
- 占用的存储单元数目为n+2e。
-
有向图
- 第i个链表中结点数目为顶点i的出度。
- 所有链表中结点数目为图中弧数。
- 占用的存储单元数目为n+e。
-
逆邻接表:对于有向图,建立逆邻接表以求出每个顶点的入度。
typedef struct ArcNode {int adjvex; // 临接点位置int weight; // 权值struct ArcNode *nextarc; } ArcNode;typedef struct VNode {char data;int in;ArcNode *firstarc; } VNode, AdjList[MAX_VERTEX_NUM];class ALGraph { public:int vexnum, arcnum;AdjList vertices;public:ALGraph();~ALGraph();// 创建无向图图void CreateUDG(int vnum, int anum,char data[], ArcInfo arcList[]);// 创建有向图图void CreateDG(int vnum, int anum,char data[], ArcInfo arcList[]);void DispGraph(); // 展示图TopResult TopOrder();void critical_path(CriticalResult &criticalResult);private:void CreateGraph(int vnum, int anum, char data[],ArcInfo arcList[], int kind); // 创建图int LocateVex(char v); // 根据顶点信息,返回顶点的坐标void cal_indegree(); // 统计每个顶点的入度void GetVe(CriticalResult &a);void GetVl(CriticalResult &a);void MarkCritical(CriticalResult &a); };
-
3.图的遍历
-
深度优先搜索(DFS)
-
主要思想:从某个顶点出发,尽可能深地搜索图的分支。
-
过程:访问一个顶点,然后选择一个未访问的邻接顶点继续搜索,直到所有邻接顶点都被访问过,然后回溯。
-
特点:递归的过程,类似于树的先根遍历。
-
-
广度优先搜索(BFS)
-
主要思想:从某个顶点出发,先访问最近的邻接点,然后是次近的邻接点,依此类推。
-
过程:使用队列来记录待访问的顶点,每次从队列中取出一个顶点,访问其所有未访问的邻接点并入队。
-
特点:分层的搜索过程,不是递归的。
-
-
遍历算法分析
- 邻接表表示:遍历图的时间复杂性为O(n+e)。
- 邻接矩阵表示:遍历图的时间复杂性为O(n^2)。
4.图的生成树和最小生成树
-
生成树:一个连通图的生成树是一个极小连通子图,包含所有顶点和n-1条边。
-
深度优先生成树和广度优先生成树:通过深度优先搜索和广度优先搜索得到的生成树。
-
最小生成树:在连通网的众多生成树中,各边权值之和最小的生成树称为最小生成树。
-
掌握Kruskal和prim求最小生成树的过程
5.图的拓扑排序和关键路径
-
有向无环图(DAG):一个无环的有向图,是描述工程或系统进行过程的有效工具。
-
AOV网:顶点表示活动,弧表示活动间的先后关系。
-
拓扑排序的序列是不唯一的
-
掌握拓扑排序
-
关键路径
-
相关问题
- 完成整个工程至少需要多少时间?
- 哪些活动是关键活动,影响整个工程进度?
-
相关概念
- 路径长度:AOE网中一条路径的长度是该路径上各个活动所需时间的总和。
- 事件的最早发生时间(Ve):从开始顶点到事件的最长路径长度。
- 事件的最迟发生时间(Vl):不推迟整个工程完成的前提下,事件最迟必须发生的时间。
- 活动的最早开始时间(ee):活动的起点所表示的事件最早发生时间。
- 活动的最迟开始时间(el):活动的终点所表示的事件最迟发生时间与该活动所需时间之差。
- 时间余量:活动的最迟开始时间与最早开始时间之差。
- 关键活动:时间余量为0的活动。
- 关键路径:完成整个工程所需的时间取决于从起始点到终止点的最长路径长度。
-
掌握如何求关键路径以及求各个元素
-
(vi,vk)表示aj,el(j)=vl(k)-aj ee(j)=ve(i)
-
6.图的最短路径
- 掌握dij和floyed算法
第八章 查找
1.静态查找
-
查找概述
- 查找是给定信息集上寻找特定信息元素的过程,是数据结构上的重要运算之一。
-
查找表
- 查找表(Search Table)是面向查找操作的数据结构。
- 静态查找表仅进行查找操作,而动态查找表在查找过程中会进行更新操作。
-
顺序查找
-
基本思路:从表的一端开始,顺序扫描线性表,依次将扫描到的关键字和给定值k相比较。
-
性能分析:平均查找长度(ASL)为O(n),查找效率最低,但算法简单且适应面广。
-
折半查找
-
基本思路:对给定值k,逐步确定待查记录所在区间,每次将搜索空间减少一半。
-
性能分析:效率比顺序查找高,查找成功的平均查找长度为O(log2N),但只适用于有序表且限于顺序存储结构。
-
-
斐波那契查找
- 基本思路:利用斐波那契数列来确定黄金分割点,进行查找。
- 性能分析:时间复杂度为O(log2N),理论上运行时间比折半查找小,只适用于顺序存储的有序表,且表中元素个数为某个斐波那契数减1。
-
查找算法比较
- 顺序查找:ASL为O(N),适用于无序表,适应面广。
- 折半查找:ASL为O(log2N),适用于有序表,效率较高。
- 斐波那契查找:ASL为O(log2N),性能优于折半查找,适用于有序表。
2.二叉排序树
-
二叉排序树(Binary Search Tree, BST)
-
定义:空树或具有特定性质的非空二叉树,左子树的所有结点均小于根的值,右子树的所有结点均大于根的值,左右子树也分别为二叉排序树。
-
特点:
- 任一结点x,其左(右)子树中任一结点y(若存在)的关键字必小(大)于x的关键字。
- 各结点关键字唯一。
- 按中序遍历该树所得到的中序序列是一个递增有序序列。
-
掌握二叉排序树的插入删除查找
-
3.二叉平衡树(1)
- 定义:平衡二叉树(AVL树)是一种自平衡二叉查找树,其中每个结点的左子树和右子树的深度之差的绝对值不超过1。
- 性质:任何节点的两个子树的高度(深度)差被限制为1,因此它也被称为高度平衡树。
- 掌握平衡二叉树的调整
4.二叉平衡树(2)
-
B-树是一棵m阶的多路平衡查找树,具有以下性质:
- 每个结点的子树数目不超过m。
- 除非根结点,否则至少有两棵子树。
- 所有非叶结点至少有⌈m/2⌉个子树。
- 非叶结点包含key和指向子树的指针,key和子树的key满足排序性。
- 所有叶结点在同一层上。
-
掌握B-树的插入、删除
-
B+树是B-树的变型,所有叶子结点包含全部关键字信息,非终端结点仅包含子树中的最大(或最小)关键字作为索引。
5.哈希查找
-
哈希表概述
-
哈希表(散列表)是一种通过哈希函数将关键字映射到一个有限的连续地址集上的数据结构。
-
哈希表通过解决冲突(不同关键字映射到同一个地址)来实现高效的数据查找。
-
哈希表的定义
-
哈希表根据哈希函数H(key)和处理冲突的方法将关键字key映射到一个地址集上,以关键字在地址集中的“像”作为记录的存储位置。
-
掌握线性探查法
-
ASL分析
下图中有错误ASLsuccess为4个1+3个2+1个3 = 13/8
第九章 排序
本单元需要了解各个排序的具体流程,详细代码实现附在最后
1.插入排序
-
排序概述
- 定义:将一组杂乱无章的数据按一定的规律顺次排列起来。
- 分类:排序算法可以分为内部排序和外部排序。
-
插入排序
- 主要思想:每次将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止。
-
直接插入排序
- 基本原理:
- 顺次地从无序表中取出记录Ri,与有序表中记录的关键字逐个进行比较,找出其应该插入的位置。
- 将此位置及其之后的所有记录依次向后顺移一个位置。
- 将记录Ri插入到空出的位置。
- 基本原理:
-
直接插入排序的算法分析
- 时间性能:
- 最好情况(已排序):O(n)。
- 最坏情况(逆序):O(n^2)。
- 平均情况:O(n^2)。
- 空间性能:O(1)。
- 稳定性:稳定。
- 时间性能:
-
折半插入排序
- 主要思想:在插入Ri时,通过折半查找确定插入位置,减少比较次数。
-
希尔排序
- 基本思想:将整个待排记录序列分割成若干小组(子序列),分别在组内进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。
- 步骤:
- 取一个整数d1<n,称之为增量,将待排序的记录分成d1个组,在各组内进行直接插入排序。
- 设置另一个新的增量d2<d1,采用相同的方法继续进行分组和排序。
- 继续取di+1<di,重复步骤,直到增量d=1。
-
希尔排序的算法分析
- 时间性能:时间复杂度约为O(n^1.3),具体取决于增量的取值。
- 稳定性:不稳定。
2.交换排序
-
交换排序概述
- 主要思想:两两比较待排序记录的关键码,如果发生逆序,则交换之,直到所有记录都排好序为止。
-
冒泡排序
- 基本思路:每趟不断将记录两两比较,并按“前小后大”(或“前大后小”)规则交换。
- 优点:每趟结束时,不仅能挤出一个最大值到最后面位置,还能同时部分理顺其他元素;一旦下趟没有交换发生,还可以提前结束排序。
- 前提:顺序存储结构。
-
冒泡排序的算法分析
- 最好情况:初始排列已经有序,只执行一趟起泡,做 n-1 次关键码比较,不移动对象。
- 最坏情形:初始排列逆序,算法要执行n-1趟起泡,第 i 趟做了n- i 次关键码比较,执行了n-i 次对象交换。
- 时间效率:O(n^2) —因为要考虑最坏情况。
- 空间效率:O(1) —只在交换时用到一个缓冲单元。
- 稳定性:稳定。
-
快速排序
- 基本思想:从待排记录序列中任取一个记录作为基准,将待排序记录分成两个部分,分别对两个部分重复上述过程,直到所有记录排在相应的位置上为止。
-
快速排序的实现
- 选取基准:常用的方法包括选取序列中第一个记录的关键字值作为基准关键字,选取序列中间位置记录的关键字值作为基准关键字,或比较序列中始端、终端及中间位置上记录的关键字值,并取这三个值中居中的一个作为基准关键字。
- 算法过程:设置两个变量low和high,其初值分别是n个待排序记录中第一个记录的位置号和最后一个记录的位置号。在扫描过程中,变量low,high的值始终表示当前所扫描分组序列的第一个和最后一个记录的位置号。
-
快速排序的算法分析
- 最好情况:每一次划分都正好将数组分成长度相等的两半。
- 最坏情况:每一次划分都将数组分成0和n-1两部分,快速排序变成“慢速排序”。
- 平均情况:任意一种划分情况出现的概率相等。
3.选择排序
-
选择排序概述
- 基本思想:不断从待排记录序列中选出关键字最小的记录插入已排序记录序列的后面,直到所有记录都排好序。
-
简单选择排序
-
操作思想 :
- 每次从待排记录序列中选出关键字最小的记录。
- 将它与待排记录序列第一位置的记录交换后,再将其“插入”已排序记录序列。
- 重复以上过程,直到待排记录序列为空。
-
简单选择排序的算法分析
- 时间性能:所以时间复杂度为 O(n2)。
- 空间性能:只需要一个临时单元用作交换,空间复杂度为 O(1)
- 稳定性:不稳定,因为存在不相邻记录之间的互换,可能会改变具有相同关键字记录的相对位置。
-
树形选择排序(锦标赛排序)
- 主要思想:首先对n个记录的关键进行两两比较,然后在其中的n/2个较小者之间再进行两两比较,如此重复,直至选出最小关键字的记录。
- 时间性能:O(nlogn)
-
堆排序
-
基本思想:借助于完全二叉树结构进行的排序,是一种树型选择排序。
-
堆的定义:满足特定性质的完全二叉树,其中每个节点的值都小于等于(小顶堆)或大于等于(大顶堆)其子节点的值。
-
堆的根节点是最小或最大的元素,从根到叶子的路径是递增或递减的
-
-
算法步骤
- 构建初始堆。
- 反复筛选:将堆顶元素与末尾元素交换,然后调整堆,再将堆顶元素与末尾元素交换,直到全部记录有序。
-
堆排序的算法分析
-
初始化堆部分:将无序序列建成一个堆的过程。
-
反复重建堆部分:将堆顶元素与末尾元素交换后,调整堆以保持堆的性质。
-
堆排序也是不稳定排序
4.归并和基数排序
-
归并排序的基本思想
- 将两个或两个以上的有序表合并成一个新的有序表。
-
二路归并排序的基本思想
- 将有n个记录的待排序列看作n个有序子表,每个有序子表的长度为1。
- 两两合并相邻的有序子表,得到n/2个长度为2或1的有序子表。
- 反复合并,直到得到一个长度为n的有序表。
-
归并排序的时间复杂度和空间复杂度
-
时间复杂度:O(nlogn)
-
空间复杂度:O(n)
-
稳定性:稳定排序。
-
基数排序的基本思想
- 借助于多关键字排序的思想对单逻辑关键字进行排序。
- 将关键字分解成若干部分,通过对各部分关键字的分别排序,完成对全部记录的排序。
-
基数排序的方法
- 最高位优先法(MSD):先对最高位关键字排序,然后对次关键字排序,依次重复。
- 最低位优先法(LSD):从最低位关键字起进行排序,然后再对高一位的关键字排序。
-
基数排序的实现
- 借助“分配”和“收集”对单逻辑关键字进行排序。
- 链式基数排序:用链表作存储结构的基数排序。
-
基数排序的时间复杂度和空间复杂度
- 时间复杂度:O(d(n+r))
- 空间复杂度:O(r+n)
- 稳定性:稳定排序。
//直接插入排序
void insertionSort(int arr[], int n)
{for (int i = 1; i < n; ++i){int key = arr[i];int j = i - 1;// 将当前元素与之前的元素进行比较,如果前面的元素大于当前元素,则将前面的元素后移while (j >= 0 && arr[j] > key){arr[j + 1] = arr[j];j--;}// 将当前元素插入到合适的位置arr[j + 1] = key;}
}// 使用二分查找找到插入位置
int binarySearch(int arr[], int item, int low, int high)
{while (low <= high){int mid = (low + high) / 2;if (item == arr[mid])return mid + 1;else if (item > arr[mid])low = mid + 1;elsehigh = mid - 1;}return low;
}// 折半插入排序
void binaryInsertionSort(int arr[], int n)
{for (int i = 1; i < n; ++i){int key = arr[i];// 找到插入位置int j = binarySearch(arr, key, 0, i - 1);// 将所有元素后移以空出插入位置for (int k = i; k > j; --k){arr[k] = arr[k - 1];}arr[j] = key;}
}//希尔排序
void shellSort(int arr[], int n) {// 开始以最大的间隔对数组进行排序,逐渐减小间隔for (int gap = n / 2; gap > 0; gap /= 2) {// 对每个间隔进行插入排序for (int i = gap; i < n; ++i) {int temp = arr[i];int j;// 将间隔内的元素进行插入排序for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {arr[j] = arr[j - gap];}arr[j] = temp;}}
}//冒泡排序
void bubbleSort(int arr[], int n)
{// 外层循环遍历整个数组for (int i = 0; i < n - 1; ++i){// 内层循环比较相邻的元素for (int j = 0; j < n - i - 1; ++j){// 如果前一个元素大于后一个元素,则交换它们if (arr[j] > arr[j + 1]){int temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;}}}
}// 分区函数
int partition(int arr[], int low, int high)
{int pivot = arr[low]; // 选择第一个元素作为基准int left = low + 1;int right = high;while (true){// 找到左侧第一个大于基准的元素while (left <= right && arr[left] <= pivot){left++;}// 找到右侧第一个小于基准的元素while (left <= right && arr[right] >= pivot){right--;}// 如果左右指针交错,退出循环if (left > right){break;}// 交换左侧和右侧的元素swap(arr[left], arr[right]);}// 将基准放到正确的位置swap(arr[low], arr[right]);return right;
}//快速排序
void quickSort(int arr[], int low, int high)
{if (low < high){// pi是分区索引,arr[pi]已经排好序int pi = partition(arr, low, high);// 分别对左右子数组进行快速排序quickSort(arr, low, pi - 1);quickSort(arr, pi + 1, high);}
}//选择排序
void selectionSort(int arr[], int n)
{// 遍历数组中的每个元素for (int i = 0; i < n - 1; ++i){// 假设当前元素是最小值int minIndex = i;// 查找未排序部分中的最小值for (int j = i + 1; j < n; ++j){if (arr[j] < arr[minIndex]){minIndex = j;}}// 交换找到的最小值和当前元素if (minIndex != i){int temp = arr[i];arr[i] = arr[minIndex];arr[minIndex] = temp;}}
}// 将子树调整为最大堆
void heapify(int arr[], int n, int i)
{int largest = i; // 初始化最大值为根节点int left = 2 * i + 1; // 左子节点int right = 2 * i + 2; // 右子节点// 如果左子节点大于根节点if (left < n && arr[left] > arr[largest]){largest = left;}// 如果右子节点大于目前的最大值if (right < n && arr[right] > arr[largest]){largest = right;}// 如果最大值不是根节点if (largest != i){swap(arr[i], arr[largest]);// 递归地对受影响的子树进行堆化heapify(arr, n, largest);}
}// 堆排序
void heapSort(int arr[], int n)
{// 建立最大堆for (int i = n / 2 - 1; i >= 0; i--){heapify(arr, n, i);}// 一个一个从堆中取出元素for (int i = n - 1; i > 0; i--){// 将当前根节点移到数组末尾swap(arr[0], arr[i]);// 调整最大堆// 这里的i表示了只对剩余部分堆化,因为n改变了heapify(arr, i, 0);}
}// 合并两个子数组
void merge(int arr[], int left, int mid, int right)
{int n1 = mid - left + 1;int n2 = right - mid;// 创建临时数组int L[n1], R[n2];// 拷贝数据到临时数组for (int i = 0; i < n1; ++i)L[i] = arr[left + i];for (int j = 0; j < n2; ++j)R[j] = arr[mid + 1 + j];// 合并临时数组到原数组int i = 0, j = 0, k = left;while (i < n1 && j < n2){if (L[i] <= R[j]){arr[k] = L[i];++i;}else{arr[k] = R[j];++j;}++k;}// 拷贝L[]剩余元素while (i < n1){arr[k] = L[i];++i;++k;}// 拷贝R[]剩余元素while (j < n2){arr[k] = R[j];++j;++k;}
}// 归并排序函数
void mergeSort(int arr[], int left, int right)
{if (left < right){int mid = left + (right - left) / 2;// 递归排序左半部分mergeSort(arr, left, mid);// 递归排序右半部分mergeSort(arr, mid + 1, right);// 合并已排序的部分merge(arr, left, mid, right);}
}