第一章 绪论
数据结构
是一门研究非数值计算的程序设计问题中计算机的操作对象以及它们之间的关系和操作的学科。 1.掌握数据、数据元素、抽象数据类型、数据结构、数据的逻辑结构与存储结构等概念
数据(Data) :
是客观事物的符号表示。在计算机科学中指的是所有能输入到计算机中并被计算机程序处理的符号的总称。
数据元素(Data Element) :
是数据的基本单位,在程序中通常作为一个整体来进行考虑和处理,有时也称之为结点、顶点或记录
数据项(Data ltem):
是数据元素的组成部分,是对客观事物某一方面特性的数据描述,是数据结构中讨论的最小单位,一个数据元素是由若干个数据项组成,简单数据项在处理时不能再分割;组合数据项在处理时可进一步分割
数据对象(Data object) :
是性质相同的数据元素的集合,是数据的个子集。如字符集合C ={A,B,C,...} 。
三方面的关系
( 1)数据的逻辑结构独立于计算机,是数据本身所固有的。
(2)存储结构是逻辑结构在计算机存储器中的映像,必须依赖于计算机
(3)运算是指所施加的一组操作总称
ADT定义形式
第二章 线性表
单链表的操作(超详细),保证你看完不后悔_单链表的基本操作-CSDN博客^v99^pc_search_result_base6&utm_term=%E5%8D%95%E9%93%BE%E8%A1%A8%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C&spm=1018.2226.3001.4187
1.线性表的抽象数据类型定义
线性表具有如下的特点:
(1)存在唯一的一个被称为“第一个”的数据元素;
(2)存在唯一的一个被称为“最后一个”的数据元素;
(3)除第一个元素外,集合中的每个元素均只有一个前驱;
(4)除最后一个元素外,集合中的每个元素均只有一个后继。
2.两种存储结构(顺序存储、链式存储)
1.顺序存储:
-
把线性表的数据元素按逻辑顺序依次存放在一组地址连续的存储单元里。用这种方法存储的线性表简称顺序表。
-
顺序表是一种随机存取结构。
-
结构类型
-
2.链式存储:
用一组任意的存储单元存储线性表中的数据元素。用这种方法存储的线性表简称线性链表这组任意的存储单元可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的。也就是说链表中元素的逻辑顺序和物理顺序不一定相同
data数据域,存放元素的值
next : 指针域,存放当前结点的直接后继结点的地址。
结点:
结点是通过动态分配内存和释放内存来的实现
1.动态分配 L.data=(ElemType)malloc(sizeof(ElemType)MaxSize)
malloc(m):开辟m字节长度的地址空间,并返回这段空间的首地址
sizeof(x):计算变量x的长度
L.data=(int *)malloc(sizeof(int)×MaxSize)
(int *) 表示强制类型转换,转换为int类型的指针
2.动态释放 free(p) ;
系统回收由指针变量p所指向的内存区。
结点的赋值
3.顺序表
1.顺序表初始化
静态分配
#include <stdio.h> #include <stdlib.h> #define MAX_SIZE 100 typedef struct { //定义了一个名为SeqList的结构体 int data[MAX_SIZE]; //data数组用于存储顺序表的元素 int length; //length表示顺序表的当前长度 } SeqList; void initSeqList(SeqList *list) { list->length = 0; // 将线性表的长度设为0,表示为空表 } //initSeqList函数用于初始化顺序表,将其长度设置为0,表示为空表 int main() { SeqList myList; initSeqList(&myList); // 在此可以进行其他操作,已初始化的顺序表可以进行插入、删除、访问等操作 return 0; }
动态分配
#include <stdio.h> #include <stdlib.h> typedef struct { int* data; //将data成员变量的类型改为指向整数的指针(int*) int length; int capacity; //顺序表的当前容量 } SeqList; void initSeqList(SeqList* list, int initialCapacity) { list->data = (int*)malloc(initialCapacity * sizeof(int)); // 动态分配内存 list->length = 0; list->capacity = initialCapacity; }//initSeqList函数使用malloc函数为data指针动态分配内存空间,大小为初始容量乘以整数的字节数 int main() { SeqList myList; int initialCapacity = 10; // 初始容量 initSeqList(&myList, initialCapacity); // 在此可以进行其他操作,已初始化的顺序表可以进行插入、删除、访问等操作 free(myList.data); // 释放动态分配的内存 return 0; }
2.顺序表插入元素
`void Insert_List(SeqList *L, int i, datatype x)` `{` `int j;` `if(L->last == MAXSIZE - 1){` `printf("表已满,无法插入!\n");` `return;` `}` `if(i < 1 || i > L->last + 2){` `printf("输入的位置有误,无法插入!\n");` `return;` `}` `//向后移动节点` `for(j = L->last; j >= i-1; j--)` `L->data[j+1] = L->data[j];` `//插入新元素,指针移动到最后一个元素` `L->data[i-1] = x;` `L->last++;` `return;` `}`
3.顺序表删除元素
`void delete_data_List(SeqList *L, datatype x)` `{` `int i, j;` `if(-1 == L->last){` `printf("表空!\n");` `return;` `}` `//找到X元素后,跳出循环,得到元素的下标i;否则正常结束i=last+1` `for(i = 0; i <= L->last; i++)` `if(x == L->data[i])` `break;` `if(i == L->last + 1){` `printf("未找到所查找的元素!\n");` `return;` `}` `else{` `for(j = i; j <= L->last; j++)` `L->data[j] = L->data[j+1]` `L->last--;` `return;` `}` `}`
4.查找元素
int Search_List(SeqList *L, datatype x) { int i = 0; while(x != L->data[i] && i <= L->last) i++; if(i > L->last) return -1; return i; }
5.代码:
//头文件包含 #include <stdio.h> #include <stdlib.h> //函数结果状态代码 #define TRUE 1 #define FALSE 0 #define OK 1 #define ERROR 0 #define INFEASIBLE -1 #define OVERFLOW -2 //Status 是函数的类型,其值是函数结果状态代码 typedef int Status; typedef char ElemType; //顺序表的定义 #define MAXSIZE 100 typedef struct { ElemType* elem; int length; } SqList; //顺序表的初始化函数 Status InitList_Sq(SqList* L) { L->elem = (ElemType*)malloc(MAXSIZE * sizeof(ElemType)); if (!L->elem) exit(OVERFLOW); L->length = 0; return OK; } //销毁线性表 void DestroyList_Sq(SqList* L) { if (L->elem) free(L->elem); L = NULL; } //清空线性表 void ClearList(SqList* L) { L->length = 0; } //求线性表的长度 int GetLength(const SqList* L) { return L->length; } //判断线性表是否为空 Status IsEmpty(const SqList* L) { if (L->length == 0) return TRUE; else return FALSE; } //线性表取第i个值 Status GetElem(const SqList* L, int i, ElemType e) { if (i<1 || i>L->length) return ERROR; else { e = L->elem[i - 1]; return OK; } } //线性表按值顺序查找 Status LocateElem(const SqList* L, const ElemType e) { int i; for (i = 0; i <= L->length - 1; i++) { if (L->elem[i] == e) return i + 1; //查找成功返回元素位置 } return 0; //查找失败返回0 } //顺序表的插入 Status InsertList_Sq(SqList* L, int n, const ElemType e) { int i; if (n >= 1 && n <= L->length + 1) //判断插入位置是否合法 { if (L->length == MAXSIZE) //判断存储空间是否已满 return ERROR; for(i=L->length-1; i>=n-1; i--) //插入位置及之后元素后移 { L->elem[i+1] = L->elem[i]; } L->elem[n - 1] = e; L->length += 1; return OK; } return ERROR; } //顺序表删除 Status DeleteElem(SqList* L, int n) { int i; if (n >= 1 && n <= L->length) { L->elem[n - 1] = 0; //删除指定元素 for (i = n - 1; i <= L->length - 1; i++) //剩余元素移位 { L->elem[i] = L->elem[i + 1]; } L->length--; //顺序表长度-1 return OK; } return ERROR; } //顺序表显示 void ShowList_Sq(const SqList* L) { if (L->length == 0) puts("The SqList is empty!"); else { int i; for (i = 0; i < L->length; i++) { printf("%d ", L->elem[i]); } putchar('\n'); printf("The length of SqList is %d\n", L->length); } } //合并两个顺序表,将L2合并到L1中 Status MergeList_Sq(SqList* L1, const SqList* L2) { if (L1->length == 0 || L2->length == 0) { puts("Length must be non-zero!"); return ERROR; } else if (L1->length + L2->length > MAXSIZE) { puts("Overflow"); return OVERFLOW; } else { int i; for (i = 0; i <= L2->length - 1; i++) { L1->elem[i + L1->length] = L2->elem[i]; } L1->length += L2->length; return OK; } }
4.单链表
(1)头插入法
(2)尾插入法
(3)单链表的查找
按序号查找
按值查找
(4)单链表的插入
(5)单链表的删除
基本思想就是
①查找操作将所要删除的结点定位
②建立其前结点和后结点的连接;
③之后删除该位置即可 free()
需要注意的几点
需要定位两个结点指针p ,q,q->next = p; 一个定位该结点前的结点,一个定位该结点。
需要加几个判断条件,因为无法预知链表的长度,所以在定位的时候直接需要防止操作出链表, 设单链表长度为n,则删除第i个结点仅当1≤i≤n时是合法的。则当i=n+1 时,虽然被删结点不存在,但其前趋结点却存在(即终端结点)。
5.销毁与清空的区别
销毁:是先销毁了链表的头,然后接着一个一个的把后面的销毁了,这样这个链表就不能再使用了,即把包括头的所有节点全部释放。
清空:是先保留了链表的头,然后把头后面的所有的都销毁,最后把头里指向下一个的指针设为空,这样就相当与清空了,但这个链表还在,还可以继续使用;即保留了头,后面的全部释放。
6.双向链表的插入和删除
第三章 栈和队列
1.栈和队列
栈和队列是在 程序设计中被广泛使用的两种特殊的线性表它们的特殊性在于对栈和队列的插入和删除操作被限制为只能在表的两端(或一端)进行
线性表:在表的任意位置进行插入和删除
栈:只能在表尾进行插入和删除,“后进先出”
队列: 只能在表尾进行插入,而在表头删除,“先进先出”
和线性表相比,栈和队列的插入和删除操作受更多的约束和限定,故又称为操作受限的线性表结构
2.线性表、栈与队的异同 :
相同点: 逻辑结构相同,都是线性的;都可以用顺序存诸或链表存储。
不同点 : 运算规则不同。线性表可以在表头、表中、表尾任意位置做插入删除操作,而栈是只允许在一端进行插入和删除运算,因而是后进先出表LIFO:队列是只允许在一端进行插入、另一端进行删除运算,因而是先进先出表FIFO 用途不同。线性表比较通用;栈用于函数调用、递归等:队列用于离散事件模拟、多道作业排队处理等
3.栈的定义和特点
4.顺序栈的基本操作实现
5.队列的定义和特点
6.两种栈(顺序栈、链式栈)的入桟、出栈
1.顺序栈的定义
2.静态和动态数组
3.顺序栈的入栈、出栈、取栈顶操作
4.链栈的定义及其入栈操作
5.栈的应用举例
例一、数制转换
例二、括号匹配的检验
例三、 行编辑程序问题
例四、 迷宫求解
例五、实现递归
递归和迭代的关系
7.两种队列(循环队列、链式队列)的入队、出队
1.链队列
2.链队列的基本操作
3.顺序队列
4.循环队列
1.入队
2.出队
第四章 串
1.理解串的基本概念。
1.串相等、串变量、串常量
2.掌握串的一些基本操作及其实现。
3.了解串的三种基本存储结构。
1.串的定长顺序存储表示
2.串的堆分配存储表示
3.串的块链存储表示
(1)节点大小
(2)串的块链存储结点结构
(3)串的链表结构
(4)存储密度
4.KMP算法
1.一种用于字符串匹配的高效算法,它通过利用已经匹配过的部分信息,避免了回溯的操作,从而提高了匹配的效率
2.KMP算法的核心思想是构建一个辅助数组(通常称为next数组或失配函数),用于指导模式串的滑动匹配过程。
3.KMP算法的匹配过程分为两个指针:i指针(指向文本串)和j指针(指向待匹配字符串)。算法的思路如下:
-
预处理:根据模式串构建辅助数组next。next数组存储模式串(子串)的最长相等前后缀的长度(next[i]=j)
最长相等前后缀: 字符串 abcdab 前缀的集合:{a,ab,abc,abcd,abcda} 后缀的集合:{b,ab,dab,cdab,bcdab} 那么最长相等前后缀就是ab (next[i]=j): 每一个字符前的字符串都有最长相等前后缀,而且最长相等前后缀的长度是我们移位的关键,所以我们单独用一个next数组存储子串的最长相等前后缀的长度。而且next数组的数值只与子串本身有关。 所以next[i]=j,含义是:下标为i 的字符前的字符串最长相等前后缀的长度为j。 我们可以算出,子串t= "abcabcmn"的next数组为next[0]=-1(前面没有字符串单独处理) next[1]=0;next[2]=0;next[3]=0;next[4]=1;next[5]=2;next[6]=3;next[7]=0; | a | b |c |a |b | c | m |n | |–|–|–|–|–|–|–|–|–|–| |next[0] |next[1] | next[2] | next[3] |next[4] |next[5] | next[6] | next[7] | |-1 | 0 |0 | 0 | 1 | 2 |3 | 0 |
-
匹配过程:从头开始遍历文本串和模式串,比较对应位置的字符。
-
若当前字符匹配成功,将i和j指针同时后移1位。
-
若当前字符匹配失败,根据next数组的值调整j指针的位置。将j指针移动到next[j]的位置,即模式串中当前位置之前的最长可匹配前缀的下一个位置。
-
-
重复步骤2,直到匹配成功或文本串遍历完
代码实现:
#include <stdio.h> #include <string.h> // 构建next数组 void buildNext(char* pattern, int* next) { int len = strlen(pattern); int i, j; next[0] = -1; i = 0; j = -1; while (i < len) { if (j == -1 || pattern[i] == pattern[j]) { i++; j++; next[i] = j;//下标为i 的字符前的字符串最长相等前后缀的长度为j。 } else { j = next[j]; } } } // KMP算法进行字符串匹配 int kmpSearch(char* text, char* pattern) { int n = strlen(text); int m = strlen(pattern); int i = 0; int j = 0; int* next = (int*)malloc(sizeof(int) * m); buildNext(pattern, next); while (i < n && j < m) { if (j == -1 || text[i] == pattern[j]) { i++; j++; } else { j = next[j]; } } free(next); if (j == m) { return i - j; // 返回匹配的起始位置 } else { return -1; // 匹配失败 } } int main() { char text[] = "ABABCABABCDABABCABAB"; char pattern[] = "ABABCABAB"; int position = kmpSearch(text, pattern); if (position != -1) { printf("Pattern found at position: %d\n", position); } else { printf("Pattern not found.\n"); } return 0; }
第五章 数组和广义表
1.理解多维数组的行优先、列优先存储
1.数组的抽象数据类型
2.数组的顺序表示和实现
2.理解特殊矩阵 (对称矩阵、三角矩阵、稀疏矩阵)的压缩存储
1.矩阵的压缩存储
2.对称矩阵
3.三角矩阵
4.对角矩阵
5.稀疏矩阵
(1)三元组顺序表
3.了解广义表的相关概念及表示方法。
1.广义表的定义
2.广义表的术语
3.广义表的深度
4.广义表的重要结论
5.首尾链表结点的类型定义
第六章 树和二叉树
1.树和二叉树(满二叉树、完全二叉树)的基本概念、术语和性质
1.树的定义
2.树的基本术语
3.树的抽象数据定义
4.二叉树的性质
性质1:在二叉树的第i 层上至多有2^(i-1)个结点。(i>=1)
性质2: 深度为 k 的二叉树至多有 (2^k )-1 个结点(k>=1)。
性质3: 对任何一棵二叉树,若它含有n个叶子结点、n2个度为 2的结点,则必存在关系式: n= n2+1
性质4: 具有 n 个结点的完全二叉树的深度为( log2 n) +1 。
性质5:
5.满二叉树
6.完全二叉树(不一定是满的)
2.二叉树的顺序存储结构
3.二叉树的二叉链表存储结构及其实现
二叉树的结构:
typedef struct BinaryTreeNode { BTDataType data; struct BinaryTreeNode* left; struct BinaryTreeNode* right; }BTNode;
初始化节点:
BTNode* BuyNode(BTDataType x) { BTNode* node = (BTNode*)malloc(sizeof(BTNode)); if (node == NULL) { perror("malloc fail"); return NULL; } node->data = x; node->left = NULL; node->right = NULL; return node; }
4.二叉树的先序、中序、后序和层次遍历算法
1.二叉树的遍历
2.遍历方式(先序、中序、后序)
(1)前序遍历
void PreOrder(BTNode* root) { if (root == NULL) { printf("NULL "); return; } printf("%d ", root->data); PreOrder(root->left); PreOrder(root->right); }
(2)中序遍历
if (root == NULL) { printf("NULL "); return; } InOrder(root->left); printf("%d ", root->data); InOrder(root->right); }
(3)后序遍历
void PostOrder(BTNode* root) { if (root == NULL) { printf("NULL "); return; } PostOrder(root->left); PostOrder(root->right); printf("%d ", root->data); }
2.三种遍历方法的不同之处
3.层次遍历
5.使用先序和中序(或中序和后序)两个遍历序列及标明空子树的先序遍历序列构造二叉树的方法
1.统计二叉树中结点的个数
2.以标明空子树的先序遍历序列构造一棵二叉树
3.由二叉树的先序和中序遍历序列创建一颗二叉树
6.二叉树中序遍历的非递归算法
1.中序遍历的非递归算法思路 :
1.初始化一个空栈,指针P从二叉树的根结点开始
2.如果p不空或栈不空,循环执行以下操作。
(1)如果p不空,表示到达了一个结点,将结点p入栈并进入其左子树。
(2)如果p为空而栈不空,表明已经沿着某条路径走出了二又树,此时需返回访问这条路径最后经过的结点而该结点正好被保存在栈的栈顶,因此弹出栈顶元素并让p指向它,接下来访问结点p,然后进入其右子树。
3.算法结束
7.了解线索二叉树。
1.什么是线索二叉树
2.线索链表中结点的约定
8.建立哈夫曼树和哈夫曼编码的方法及带权外路径长度(WPL)的计算方法
1.相关术语
★2.huffman树
(1)哈夫曼树的数据类型定义
typedef struct { int weight; int parent,lchild,rchild; //每个结点的双亲、左右孩子的数组下标 }HTNode,*HuffmanTree; //由哈夫曼树的构造过程得知,n个权重结点构造出的哈夫曼树具有2*n-1个结点 //通常哈夫曼树的顺序存储结构下标从1开始计数,因此,如果我们使用数组实现的话 //那么数组的长度应该是2*n //使用的数组下标是1~2n-1
(2)哈夫曼树的初始化
void InitHuffmanTree(HuffmanTree &HT, int n) //HT为空指针 { //传递指向哈夫曼树的指针,必须用引用 //哈夫曼树一共有n个叶子结点 //初始化一个空的 Huffman 树 int m=2*n-1; //实际使用2n-1个结点 HT=new HTNode[m+1]; //创建一个数组,数组元素类型为HTNode。数组的地址为HT。第0个下标不用--》int *p =a[10]; p[0] for(i=1;i<=m;i++) { HT[i].parent=0; HT[i].lchild=0; HT[i].rchild=0; } //初始时将每个节点的父节点、左孩子和右孩子都设置为0,表示暂时没有父子关系。 for(i=1;i<=n;i++) { cin>>HT[i].weight; // or HT[i].weight=i; //依次输入权重 } }
(3)Select算法
//在HT[k](1≤k≤i-1)中选择两个其双亲域为0且权值最小的结点, // 并返回它们在HT中的序号s1和s2 void SelectHuffmanTree(HuffmanTree HT, int n, int &s1, int &s2) { int min1,min2; min1=min2=0x3f3f3f3f; //先赋予最大值 for(i=1;i<=n;i++) { if(HT[i].weight<min1 && HT[i].parent==0) //如果当前叶子结点的权重更小,且还没有父母,即还没有被使用 { min1=HT[i].weight; //如果当前叶子结点的权重更小,且还没有父母,就记录下当前结点的权重和下标 s1=i; //s1记录下标 } } temp=HT[s1].weight; //为了防止第一个最小结点被二次使用,直接将它的权重设为最大值 HT[s1].weight=0x3f3f3f3f; for(i=1;i<=n;i++) { if(HT[i].weight<min2 && HT[i].parent==0) //如果当前叶子结点的权重更小,且还没有父母,即还没有被使用 { min2=HT[i].weight; //如果当前叶子结点的权重更小,且还没有父母,就记录下当前结点的权重和下标 s2=i; } } HT[s1].weight=temp; //恢复原样 }
(4)哈夫曼树的构造
void CreateHuffmanTree(HuffmanTree &HT, int n) { InitHuffmanTree(HT, n); //初始化,传HT指针,和叶子结点个数 for(i=n+1;i<=m;i++) //m=2n-1。叶子节点一共n个,所以从第n+1个结点开始构造 { SelectHuffmanTree(HT,i-1,s1,s2); //挑选前i-1中最小的两个结点,第一轮是从前n个叶子结点中,找最小的2个。 HT[i].lchild=s1; //新节点的左右孩子 HT[i].rchild=s2; HT[i].weight=HT[s1].weight+HT[s2].weight; //合并 HT[s1].parent=i; HT[s2].parent=i; } }
(5)c知识补充-如何申请指针数组
char **pchar = NULL; pchar = (char **)malloc(n*sizeof(char *));/pchar其实就是一个char* []数组 //现在可以在这里给数组元素赋值 pchar[0] = (char *)malloc(SIZE * sizeof(char));//或*(p+0) pchar[1] = (char *)malloc(SIZE * sizeof(char))// 或*(p+1) .... int **p; cin>>n>>m; p=new int* [n];//先申请全部行首(n行)指针,再按行逐行申请 for(i=0;i<n;i++) p[i]=new int [m]; ------------------------------------------------------------------------- p是一个指针,它指向一个数组,数组名叫p,这个数组有n个元素,每一个元素p[i]都是 int *类型的指针。 这时又有n个不等长的int类型的数组,假设它们分别是int a[5], int b[4], int c[6], int d[3] ,int f。 那么,此时p[i]就指向每一个数组的首地址,即: p[1]=a,p[2]=b, p[3]=c, p[4]=d, p[5]=&f;
(6)哈夫曼编码的算法实现
(7)哈夫曼编码
void CreateHuffmanCode(HuffmanTree HT, char** &HC / char* &HuffmanCode[] / HuffmanCode* &HC[], int n) { HC = new char* [n]; //分配一个指针数组 HC,数组的长度为 n。每个数组元素都是一个 char* 类型的指针,用于存储哈夫曼编码 cd = new char [n]; //3个叶子结点需要2个码,分配一个长度为 n 的字符数组 cd,用于临时存储每个叶子节点的编码 cd[n-1]="\0"; // 将 cd 数组的最后一个元素设置为字符串结束符 "\0",表示编码的结束 for(i=1;i<=n;i++) //求n个叶子结点的码,因为是叶子结点,所以从1开始 { start=n-1;//码从最后一个位置开始,每次往cd[]里加元素时就start-- // 将变量 start 设置为 n-1,表示编码从 cd 数组的最后一个位置开始 c=i;//将变量 c 设置为当前叶子节点的编号 f=HT[c].parent;// 将变量 f 设置为当前叶子节点的父节点 while(!f) //只要不是根结点0,就执行下去 { start--; if(HT[f].lchild==c) {cd[start]=‘0’;} //如果当前结点是左孩子,码就是0 else {cd[start]=‘1’;} //如果当前结点是右孩子,码就是1 c=f; //将当前节点更新为父节点,继续向上回溯 f=HT[c].parent;//更新父节点为当前节点的父节点 } HC[i]=new char[n-start]; //为第i个字符串码分配空间 strcpy(HC[i], &cd[start]); //将编码字符串从 cd 数组的 start 位置复制到 HC[i] } delete cd;//释放 cd 数组的内存空间 } //这段代码的目的是根据给定的哈夫曼树生成每个叶子节点的编码,并将编码存储在 HC 数组中。每个叶子节点的编码由字符 '0' 和 '1' 组成,表示该叶子节点在哈夫曼树中的路径。编码的长度由 n - start 决定,其中 n 是叶子节点的个数,start 是编码的起始位置。
3.带权路径长度(Weighted Path Length)
用于衡量哈夫曼树的平衡性和编码效率的指标。它是通过对每个字符的编码长度与其出现的概率(权重)相乘,并对所有字符的结果求和得到的。 用于衡量哈夫曼树的平衡性和编码效率的指标。它是通过对每个字符的编码长度与其出现的概率(权重)相乘,并对所有字符的结果求和得到的。 计算带权路径长度的步骤如下: 1. 对于每个字符,计算其出现的概率或频率。 2. 对于每个字符的编码,计算其码长(编码的位数)。 3. 将每个字符的概率与对应的码长相乘,得到每个字符的权重路径长度。 4. 对所有字符的权重路径长度进行求和。 下面是一个计算带权路径长度的示例: 假设有以下字符及其频率: 字符:A, B, C, D, E 频率:0.4, 0.15, 0.2, 0.1, 0.15 对应的哈夫曼编码为: A: 0 B: 10 C: 110 D: 1110 E: 1111 计算步骤: 1. 计算每个字符的编码长度: A: 1 B: 2 C: 3 D: 4 E: 4 2. 计算每个字符的权重路径长度: A: 0.4 * 1 = 0.4 B: 0.15 * 2 = 0.3 C: 0.2 * 3 = 0.6 D: 0.1 * 4 = 0.4 E: 0.15 * 4 = 0.6 3. 对所有字符的权重路径长度进行求和: 带权路径长度 = 0.4 + 0.3 + 0.6 + 0.4 + 0.6 = 2.3 因此,这个示例中的带权路径长度为2.3。 带权路径长度越小,表示哈夫曼树越平衡,编码效率越高。哈夫曼树是一种根据字符出现频率构建的最优二叉树,它通过将出现频率高的字符放在树的较浅层,出现频率低的字符放在树的较深层,从而实现了较好的压缩效果和编码效率。
9.树或森林和二叉树之间的相互转换,树的存储、遍历,森林的遍历。
1.树的三中存储结构
2.树与二叉树的转换
3.将树转换成二叉树
4.森林转换为二叉树
10.小结
第七章 图
本张学习要点
1.了解图的基本概念(图的定义、有向图、无向图、完全图、带权图、邻接顶点、顶点的度、子图、路径、连通图等)
2.图的两种存储结构 (邻接矩阵表示法、邻接表表示法)
1.邻接矩阵
2.网的邻接矩阵(多了一个维度,用于表示边的权重)
3.图的邻接表存储表示
邻接表就是N行链表,每一行都相当于一个链表,N为顶点数量,链表头节点就是链表中的每一个顶点,first是指从该点出发的某一条边,next是指由该点出发的边 的另一条也从该顶点出发的边
#define MVNum 100 //弧结点或边结点 typedef struct ArcNode { int adivex; //邻接顶点在头结点表中的位置/下标 int info/weight; //弧的权重 struct ArcNode* nextarc; //指向下一条边的指针 }ArcNode; //头结点 typedef struct VNode { VetexType data; //顶点的数据,比如'A' ArcNode *firstarc; //弧结点或边结点的定义,指向第一条依附该顶点的边的指针,边的顺序可以互换 }VNode, AdjList[MVNum]; //AdiList-->邻接表, AdiList[MVNum]表示VNode结点构成的数组类型 // VNode v[MVNum] = AdjList v,但是后者不好理解,现在大多已经不用了 //图的定义 typedef struct ALGraph { //VNode vexs[MVNum]; AdjList vertices; //vertices是vertex的复数 int vexnum,arcnum; //图的当前顶点数和弧数 }ALGraph; ----------------------- ALGraph G; //定义了邻接表表示的图G G.vexnum=5; //图G包含5个顶点和5条边 G.arcnum=5; G.vertices[1].data='b'; //图G中第2个顶点是b p=G.vertices[1].firstarc; //指针pz指向顶点b的第一条边结点 p->adjvex=4; //p指针所指边结点是到下标为4的结点的边
4.邻接表与邻接矩阵的区别
3.图的两种遍历算法 (深度优先搜索遍历、广度优先搜索遍历)、能采用这两种遍历算法得到图的生成树
1.图的遍历
2.深度优先搜索遍历
3.广度优先搜索遍历
4.DFS与BFS比较
4.两种最小生成树 (MST) 算法思想 (Prim、Kruskal)
1.MST最小生成树
(1)最小生成树是一个连通无向图的子图,它包含了图中的所有顶点,并且是所有生成树中具有最小总权重的树
对于一个具有n个顶点的连通无向图,其最小生成树将包含n-1条边,连接所有的顶点,并且没有形成环路
(2)最小生成树的性质:
-
最小生成树是一棵树,它不存在环路。
-
最小生成树包含图中的所有顶点。
-
最小生成树的边的数量为顶点数减一。
-
最小生成树的总权重(边的权重之和)是所有生成树中最小的。
2.Prim算法
-
Prim算法是一种贪心算法,从一个初始顶点开始,逐步扩展最小生成树的顶点集合,直到包含了图中的所有顶点。
-
算法过程:
-
创建一个空的最小生成树集合,将初始顶点加入集合。
-
重复以下步骤,直到最小生成树包含了所有顶点:
-
从当前最小生成树集合的顶点中,选择一个顶点v,该顶点到最小生成树集合的距离最小(即边的权重最小)。
-
将顶点v加入最小生成树集合。
-
更新最小生成树集合中所有顶点到非最小生成树集合的距离(权重)。
-
-
-
Prim算法主要适用于稠密图(边数较多)的情况,时间复杂度为O(V^2),其中V为顶点数。
3.Kruskal算法
-
Kruskal算法也是一种贪心算法,按照边的权重从小到大逐步构建最小生成树,直到包含了图中的所有顶点。
-
算法过程:
-
创建一个空的最小生成树集合。
-
将图中的所有边按照权重从小到大排序。
-
遍历排序后的边,如果当前边不会导致形成环路,则将其加入最小生成树集合,否则就舍弃掉这条边
-
重复步骤3,直到最小生成树包含了所有顶点或者遍历完所有边。
-
-
Kruskal算法主要适用于稀疏图(边数较少)的情况,时间复杂度为O(ElogE),其中E为边数。
5.迪杰斯特拉算法---图的单源最短路径算法 (Dijkstra算法)
总结:Dijkstra算法就是最开始选离源点V00最近的点,然后选好点后,再从选好点的看其邻接点的距离dist[]是否减小,减小就修改dist[]和path[];否则就不进行修改操作。Dijkstra算法基于贪心策略,用邻接矩阵表示图时,来使用Dijkstra算法,其时间复杂度为O(n*n)。当边上带有负权值时,Dijkstra算法并不适用。
注:图的邻接表表示法要求边表结点按adivex域从小到大排。图遍历时按照邻接项点的席号从小到大搜索
1.单源点最短路径(一顶点到其余各顶点)
-
Dijkstra算法是一种用于求解图中最短路径的算法。它可以用于有向图或无向图,但是要求图中的边的权值必须为非负数。
下面是Dijkstra算法的步骤:
-
创建一个集合
S
,用于存储已经确定最短路径的顶点,初始时为空集。 -
创建一个数组
dist[]
,用于存储从起始顶点到每个顶点的最短路径长度,初始时将起始顶点的距离设为0,其他顶点的距离设为无穷大(或一个较大的值)。 -
选择一个起始顶点
start
。 -
对于起始顶点start的所有邻接顶点,更新它们的最短路径长度。具体步骤如下:
1)遍历起始顶点
start
的所有邻接顶点,记当前邻接顶点为v
2)计算从起始顶点
start
到顶点v
的路径长度new_dist
,即dist[start] + weight(start, v)
,其中weight(start, v)
表示边(start, v)
的权值3)如果
new_dist
小于dist[v]
,则更新dist[v]
为new_dist
-
从未确定最短路径的顶点中选择一个距离最小的顶点
min_v
,将其加入集合S
。 -
重复步骤4和步骤5,直到所有顶点都被加入集合
S
。 -
最终,
dist[]
数组中存储的就是从起始顶点到每个顶点的最短路径长度。
-
2.迪杰斯特拉算法例题:
2.所有顶点间的最短路径(任意两顶点之间)-Floyd(弗洛伊德)算法、
1.Floyd算法用于求解图中任意两个顶点之间的最短路径。
时间复杂度为 O(n^3),其中 n 是图中顶点的数量。通过三重循环来更新任意两个顶点之间的最短路径长度(每次循环迭代都会更新距离矩阵中的路径长度)
1.外层循环用于选择中间顶点
2.中间层循环用于遍历所有顶点作为起点
3.内层循环用于遍历所有顶点作为终点
空间复杂度为 O(n^2),因为需要使用一个二维矩阵来存储任意两个顶点之间的最短路径长度。
6.拓扑排序
1.拓扑排序是一种对有向无环图(DAG)进行排序的算法,它将图中的顶点按照一种线性的顺序进行排序,使得对于任意一条有向边 (u, v),顶点 u 在排序中出现在顶点 v 的前面.
(1)拓扑排序只适用于有向无环图,如果图中存在环,则无法进行拓扑排序
(2)有向无环图一定是拓扑序列,有向有环图一定不是拓扑序列
(3)如果是一个有向无环图,那么一定有一个点的入度为0,如果找不到一个入度为0的点,这个图一定是带环的。
2.拓扑排序的常见实现方法,使用深度优先搜索(DFS)和栈:
-
对于给定的有向无环图,首先初始化一个栈和一个集合,用于存储已访问的顶点和已完成排序的顶点。
-
从图中的任意一个未访问的顶点开始,进行深度优先搜索。
-
在深度优先搜索的过程中,对于当前访问的顶点 u,先将其添加到已访问的集合中。
-
然后递归地访问 u 的所有未访问的邻居顶点 v。
-
在访问完所有的邻居顶点之后,将 u 添加到栈中。
-
重复步骤 2-5,直到所有的顶点都被访问过。
-
最后,从栈中依次弹出顶点,即可得到拓扑排序的结果。
拓扑排序的思路:
首先记录各个点的入度
然后将入度为 0 的点放入队列
将队列里的点依次出队列,然后找出所有出队列这个点发出的边,删除边,同时边的另一侧的点的入度 -1。
如果所有点都进过队列,则可以拓扑排序,输出所有顶点。否则输出-1,代表不可以进行拓扑排序。
3.拓扑排序的时间复杂度为 O(V + E),其中 V 是顶点的数量,E 是边的数量。
第九章 查找
1.查找的基本概念 (查找表、查找、平均查找长度ASL)
2.静态查找表的查找算法 (顺序查找、折半查找、分块查找)
1.静态查找表的定义
2.顺序查找
平均查找长度 = (1/N) * (1 + 2 + 3 + ... + N)
其中,N表示表中元素的数量。
等差数列求和公式可以简化上述公式,将等差数列求和公式应用到 (1 + 2 + 3 + ... + N) 部分得到:
平均查找长度 = (1/N) * [N *(N+1) / 2]
3.折半查找(二分查找)
4.分块查找
3.二叉排序树的定义以及查找、插入、删除等操作,二叉排序树的平均查找长度,了解平衡二叉树。
1.二叉排序树的定义
2.二叉排序树的基本操作
(1)查找
(2)插入
(3)删除
3.二叉排序树的性能分析
4.平衡二叉树
4.哈希表以及解决冲突的两种方法 (开放定址法、链地址法)
1.哈希表
2.哈希函数
3.处理冲突
1.开放定址法
通用的再散列函数形式:Hi=(H(key)+di)% m i=1,2,…,n
(1)线性探测再散列 (dii=1,2,3,…,m-1)
(2)二次探测再散列 (di=1^2,-1^2,2^2,-2^2,…,k^2,-k^2 ( k<=m/2 ))
例如,已知哈希表长度m=11,哈希函数为:H(key)= key % 11,则H(47)=3,H(26)=4,H(60)=5,假设下一个关键字为69,则H(69)=3,与47冲突。
如果用线性探测再散列处理冲突,下一个哈希地址为H1=(3 + 1)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 + 2)% 11 = 5,还是冲突,继续找下一个哈希地址为H3=(3 + 3)% 11 = 6,此时不再冲突,将69填入5号单元。
如果用二次探测再散列处理冲突,下一个哈希地址为H1=(3 + 1^2)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 - 1^2)% 11 = 2,此时不再冲突,将69填入2号单元。
如果用伪随机探测再散列处理冲突,且伪随机数序列为:2,5,9,……..,则下一个哈希地址为H1=(3 + 2)% 11 = 5,仍然冲突,再找下一个哈希地址为H2=(3 + 5)% 11 = 8,此时不再冲突,将69填入8号单元。
(2)链地址法
4.哈希表的平均查找长度
5.负载因子
负载因子(Load Factor)是指散列表中已存储的元素数量与散列表总容量之间的比值,即 α = N / M,其中 N 是已存储的元素数量,M 是散列表的总容量
(1)负载因子 α 小于 1 时,表示散列表中存储的元素数量相对于总容量较少,有足够的空间来避免碰撞(冲突)
第十章 排序
1.排序的基本概念
1.排序分类
(1)内部排序:数据元素全部放在内存中的排序。 (2)外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
2.排序算法(插入、交换、选择、归并)
1.插入排序
(1)直接插入排序(顺序查找插入)
void InsertSort( SqList &L ){ int i, j; for (i=2; i<=L.length; ++i){ if (L.r[i].key < L.rli-1].key){// 若"<",需将L.r[i]插入有序子表 L.r[0]=L.r[i];// 复制为哨兵 for (j=i-1; L.r[0].key<L.r[j].key; --j) { L.r[j+1]=L.r[j];// 记录后移 } L.r[j+1]=L.r[0];// 插入到正确位置 } }
书本内容:
void InsertSort(SquLish &L){} //对顺序表L作直接插入排序 for(i=2;i<=L.length;++i) if( LT(L.r[i].key,L.r[i-1].key) ){//若当前元素 L.r[i] 小于前一个元素 L.r[i-1],需将L.r[i]插入有序子表 L.r[0]=L.r[i]; //复制为哨兵 L.r[i]=L.r[i-1]; for( j=i-2; LT(L.r[0].key,L.r[j].key); --j){ //内部循环从 j = i-2 开始,一直进行直到哨兵的关键字(L.r[0].key)大于或等于当前元素 L.r[j] 的关键字 L.r[j+1]=L.r[j];//元素 L.r[j] 都向右移动一位 } L.r[j+1]=L.r[0]; //插入到正确位置 } }//end
(2)折半插入排序(二分查找插入)
(3)希尔排序(缩小增量(gap)插入)
1.希尔排序:
增量(Gap)是指将待排序序列划分为多个子序列时,子序列之间的间隔。希尔排序的核心思想是通过不断缩小增量的方式,先对子序列进行局部排序,然后逐渐减小增量,最终使整个序列有序。
-
固定增量序列:指事先定义好的固定数值序列,例如{5, 3, 1}。这种增量序列较为简单,易于实现,但可能无法最优地提高排序性能。
-
动态增量序列:根据待排序序列的长度动态确定增量序列。例如,可以使用希尔提出的增量序列,也称为希尔增量(Shell's increments),通过逐步减半的方式生成增量序列,直到增量为1。希尔增量序列通常被认为具有较好的排序性能。
2.交换排序
(1)起泡排序
(2)快速排序(冒泡排序的一种改进)
主要思想:它基于分治的思想,通过递归地将数组分割成较小的子数组
然后对子数组进行排序,最终将整个数组排序
算法思路:
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
快速排序算法通过多次比较和交换来实现排序,其排序流程如下:
1、首先设定一个分界值,通过该分界值将数组分成左右两部分。
2、将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。
3、然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
4、重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。
概括来说为 挖坑填数 + 分治法。
代码实现:
(1)数组实现:
#include <stdio.h> using namespace std; void swap(int& a, int& b) { //交换函数 int temp = a; a = b; b = temp; } int partition(int arr[], int low, int high) { //划分函数 int pivot = arr[high]; // 选择基准元素 int i = low - 1; // i 是小于基准的元素的最右边界索引 for (int j = low; j <= high - 1; j++) { if (arr[j] < pivot) { i++; swap(arr[i], arr[j]); } } swap(arr[i + 1], arr[high]); return i + 1; // 返回基准元素的索引 } void quickSort(int arr[], int low, int high) { if (low < high) { int pivotIndex = partition(arr, low, high); // 划分 quickSort(arr, low, pivotIndex - 1); // 对左子数组进行快速排序 quickSort(arr, pivotIndex + 1, high); // 对右子数组进行快速排序 } } int main() { int i,n; int arr[10] ; printf("请输入要排序的数据个数:\n"); scanf("%d",&n); printf("请输入要排序的数据:\n"); for(i=0;i < n;i++) { scanf("%d",&arr[i]); } quickSort(arr, 0, n - 1); printf("快速排序后的结果为:\n"); for (int i = 0; i < n; i++) { printf("%d ",arr[i]); } }
3.选择排序
(1)选择排序
(2)★堆排序(Heap Sort)(直接选择排序的一种改进):
1.堆的概念:
堆是具有下列性质的完全二叉树:
大顶堆(大堆):每个节点的值都大于或等于其左右孩子节点的值
小顶堆(小堆):每个节点的值都小于或等于其左右孩子节点的值
2.构造堆
由此,我们可以归纳出堆排序算法的步骤 1.把无序数组构建成二又堆 2.循环删除堆顶元素,移到集合尾部,调节堆产生新的堆顶。
3.
4.归并排序
5.基数排序(桶排序)
1.分配+收集
选取k个关键范围,依次按关键范围分配m个桶,排序后收集,用收集后的数据按下一下关键范围进行排序和收集
关键字的取值范围一定时或范围较小时,基数排序效率较高
2.时间效率 O(k*(n+m))
3.根据实际情况选择适当的排序算法解决问题
1.总结图
2.排序比较和分类
1.稳定性:
(1)稳定排序:插入、冒泡、归并、基数排序
(2)不稳定排序:希尔、快速、选择、堆排序
2.时间复杂度:
(1)O(n):基数排序
(2)O(nlogn):快速排序、堆排序、归并排序,其中快速排序最好
(3)O(n^2):直接插入排序、冒泡排序、简单选择排序,其中直接插入最好
(4)特殊情况:
1.当待排序列有序时,直接插入和冒泡达到O(n)
2.快速排序退化为O(n^2)
3. 简单选择排序、堆排序、归并排序的时间复杂度不随序列的变化而变化
3.空间复杂度:
(1)O(1):直接插入、冒泡、简单选择、堆排序
(2)O(nlogn):快速排序,为栈所需的辅助空间
(3)O(n):归并排序
(4)O(rd):链式技术排序需要附设队列首尾指针