一、概述
1、算法的时间复杂度
先空一下
2、算法的空间复杂度
先空一下
二、线性表——数组
三、线性表——链表(Chain或Linked List)
- 链表:Chain或LinkList
- 结点:ChainNode
- 第一个结点:FirstNode
- 头节点(只有特殊的才有,头结点不存放数据):HeadNode
1、单链表
要表示一个单链表时,只需要声明一个头指针。LNode和LinkList其实都表示结点,只是第一种更强调结点,第二种更强调是表示整个链表的FirstNode
①定义结点
struct LNode
{
ElemType data;
LNode* next;
};
struct LinkList
{
ElemType data;
LNode* next;
};
②初始化一个链表
bool InitList(LinkList&L){//无头指针
L=NULL;
return true;
}
bool InitList(LinkList&L){//有头指针
L=new LinkList();
if(L==NULL) return false;
L->next=NULL;
return true;
}
③判断是否为空
bool Empty(LinkList L){//无头结点
return(L==NULL);
}
bool Empty(LinkList L){
if(L->next==NULL){return true;}
else {return false;}
}
④插入
⑤删除
⑥反转
2、双向链表
①定义结点
template<typename T>
struct ChainDNode
{
T data;
ChainDNode* prior, * next;
};
②插入结点
bool insert(ChainDNode* p,ChainDNode* s){
if (p==NULL||s==NULL){return false;}
s->next=p->next;
p->next->prior=s;
p->next=s;
s->prior=p;
}
③删除结点
bool deleteNextNode(ChainDNode* p){
if (p==NULL||p->next==NULL) return false;
if (p->next->next==NULL){
delete p->next;
p->next=NULL;
return true;
}
ChainDNode* temp1=p->next;
p->next=p->next->next;
temp1->next->prior=p;
delete temp1;
return true;
}
3、循环链表
循环链表分为循环单链表和循环双链表
①定义结点
template<typename T>
struct ChainNode{
T data;
ChainNode* next;
}
②初始化一个循环单链表
ChainNode* initCircularLinkedList() {
// 创建头节点
ChainNode *head = new ChainNode(0);
// 头节点的 next 指针指向自身,作为循环链表的起始节点
head->next = head;
return head;
}
③初始化一个循环双链表
ListNode* initCircularDoublyLinkedList() {
// 创建头节点
ListNode* head = new ListNode(0);
// 头节点的 prev 和 next 指针都指向自身
head->prev = head;
head->next = head;
return head;
}
反转链表(照片已记录)
5、广义表
广义表的深度就看嵌套了几个括号,例如
五、树与二叉树 (二叉树可以为空)
1、基本概念
任何一个节点(除了根节点)有且仅有一个前驱,树是一种递归定义的数据结构
二叉树是一种特殊的树,非空树有且仅有一个根节点,也存在前驱(除了根节点)和后继(除了叶子结点)的概念。
2、基本术语
(1)结点之间的关系描述
祖先结点:有很多祖先节点
子孙结点:有很多子孙结点
双亲结点(父结点):除了根结点,只有一个父节点,即前驱。
孩子结点:除了叶子结点,只有一个孩子结点,即后继
兄弟结点
堂兄弟结点:主要为了表述他们在同一层。
根节点从1开始
两个结点之间的路径:只能从上往下
路径长度:即经过几条边
(2)属性表述
结点的层次:从上往下数
结点的高度:从下往上数
树的高度:总共多少层
结点的度:有几个孩子
树的度:各节点的度的最大值
(3)有序树和无序树
有序树从逻辑上看,指各子树从左至右是有次序的,不能交换。
有序树从逻辑上看,指各子树从左至右是无次序的,可以交换。
(4)森林
森林是m(m>=0)棵互不相交的树的集合
3、树的性质
(1)结点树=总度数+1
(2)m叉树指每个结点最多能有m个孩子的树
(3)度为m的树的第i层最多有m^i-1个结点(i>=1)
(4)
(5)
4、二叉树
(1)概念
二叉树指一个结点最多有两个孩子,二叉树要么是空二叉树要么是由一个根节点和两个互不相交的根的左子树和右子树组成。递归定义。
(2)满二叉树(Full Binary Tree)
一棵高度为h,且含有2^h-1个结点的二叉树。
特点:
- 只有最后一层有叶子结点
- 不存在度为1的结点
- 按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1,结点i的父节点为[i/2](如果有的话)
(3)完全二叉树(Complete Binary Tree)
当且仅当其每个结点都与高度为h的满二叉树的编号1-n的结点一一对应时,称为满二叉树。可以简单的理解成把序号最高的几个去掉也是完全二叉树(按层序编号)
满二叉树是完全二叉树,完全二叉树不一定是满二叉树。
特点:
- 只有最后两层有可能有叶子结点
- 最多只有一个度为1的结点
- 按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1,结点i的父节点为[i/2](如果有的话)
- i<=[n/2]为分支结点,i>[n/2]为叶子结点
- 如果某个结点只有一个孩子,那么一定是左孩子。
(4)二叉排序树
左子树上所有的结点的关键字均小于根节点的关键字,右子树上所有的结点的关键字均大于根节点的关键字,左子树和右子树又各是一棵二叉排序树。易于进行二分查找。
(5)平衡二叉树(与排序二叉树联想)
树上任一结点的左子树和右子树的深度之差不超过1。为了能有更高效的搜索效率,尽可能使树往宽处生长。
(6)二叉树常考性质
- 设非空二叉树中度为0、1、2的结点分别为
- 对于任何非空二叉树T,如果n0为叶节点数,n2为2度节点数,则n0= n2+1。(证明:n=n2+n1+n0,n-1=2n2+n1)
- (结点编号顺序:)
(叶子结点存储数字,其他结点存储符号)
(7)二叉树的存储结构
二叉树的存储结构分为顺序存储和链式存储
①顺序存储(一定要把二叉树的结点编号与完全二叉树对应起来)
顺序存储从1开始编号,按照完全二叉树进行层次编号,顺序存储比较浪费空间,实际应用中很少使用。
#define MaxSize 100
struct TreeNode{
ElemType value;//结点中的数据元素
bool isEmpty;//判断结点是否为空,最开始需要初始化为True
};
TreeNode t[MaxSize];
//i的左孩子:2i
//i的右孩子:2i+1
//i的父节点:[i/2]
//i的所在层次:[Log2(n+1)或。。。]
常考操作:
- i的左孩子:2i
- i的右孩子:2i+1
- i的父节点:[i/2]
- i的所在层次:[Log2(n+1)或。。。]
②链式存储
链式存储比较直观。n个结点的二叉链表共有n+1个空链域。如果想要找某个结点的孩子结点会非常容易,但寻找父节点只能从根节点开始遍历,所以有时候我们使用三叉链表,方便寻找父结点。
type struct BiTNode{
ElemType data;
struct BiTNode* *lchild,*rchild;
}BiTNode,*BiTree;
创建一个树
//定义一棵空树
BiTree root=NULL;
//插入根节点
root=new BiTree();
root->data={1};
root->lchild=NULL;
root->rchild=NULL;
(8)二叉树的遍历
由于树的特殊性,二叉树的遍历分为先序、中序、后序。
①先序遍历(根左右)(Preorder)
void PreOrder(BiTree T){
if(T!=NULL){
visit(T);//访问根结点
PreOrder(T->lchild);//递归遍历左子树
PreOrder(T->rchild);//递归遍历右子树
}
}
②中序遍历(左根右)(Inorder)
void PreOrder(BiTree T){
if(T!=NULL){
PreOrder(T->lchild);//递归遍历左子树
visit(T);//访问根结点
PreOrder(T->rchild);//递归遍历右子树
}
}
中序遍历转表达式,需要加括号,即在访问一个子树之前加左括号,在访问完一个子树加右括号。
用栈的思想进行中序遍历,只需要先将树的左链存进栈中,需要访问时,再出栈处理,示意图和代码(重点理解)如下:
template <class T>//非递归中序遍历
void Tree<T>::NonrecInorder()
{ // Nonrecursive inorder traversal using a stack
Stack<TreeNode<T>*> s; // declare and initialize a stack
TreeNode<T>* currentNode=root;
while (1) {
while (currentNode) { // move down leftChild
s.Push(currentNode); // add to stack
currentNode=currentNode→leftChild;
}
If (s.IsEmpty()) return;
currentNode=s.Top();
s.Pop(); // delete from stack
Visit(currentNode);
currentNode=currentNode→rightChild;
}
}
③后序遍历(左右根)(Postorder)
void PreOrder(BiTree T){
if(T!=NULL){
PreOrder(T->lchild);//递归遍历左子树
PreOrder(T->rchild);//递归遍历右子树
visit(T);//访问根结点
}
}
后序求深度:
练习1:使用逐层展开算(重点!!!认真体会)
练习2
求树的深度:
int treeDepth(BiTree T){
if (T==NULL){
return 0;
}
else{
int l=treeDepth(T->lchild);
int r=treeDepth(T->rchild);
return l>r?l+1:r+1;
}
}
④层序遍历(Level-Order)
算法思想为:初始化一个辅助队列;根节点入队;若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话)
void LevelOrder(BiTree T){
LinkQueue Q;
Init(Q);
BiTree p;
EnQueue(Q,T);//将根节点入队
while(!IsEmpty(Q)){
DeQueue(Q,p);//队头结点出队
visit(p);//访问出队结点
if (p->lchild!=NULL)
EnQueue(Q,p->lchild);//左孩子入队
if (p->rchild!=NULL)
EnQueue(Q,p->rchild);//右孩子入队
}
}
层次遍历重要考点:由遍历序列构造二叉树
一个前序遍历序列可能对应多种二叉树形态,即若只给出一棵二叉树的前/中/后/层序遍历序列中的一种,不能唯一确定一棵二叉树。
只给一种遍历方法的结果,无法确定唯一的二叉树。但是如果给定(中序遍历+其他遍历)的结果,则可以确定唯一的一棵二叉树(必考)
①前序+中序遍历序列
②后序+中序遍历序列
③层序+中序遍历序列
(9)线索二叉树
5、树的存储结构
(1)双亲表示法
每个结点中保存指向双亲的“指针”
6、树和森林的遍历
树的遍历分为先根遍历、后根遍历、层序遍历。方式均与二叉树的遍历一致。
先根遍历的伪代码如下(深度优先)
void PreOrder(TreeNode* R){
if(R!=NULL){
visit(R);//访问根结点
while(R还有下一棵子树)
PreOrder(T);//先序遍历下一棵子树
}
}
后根遍历的伪代码如下(深度优先)
void PreOrder(TreeNode* R){
if(R!=NULL){
while(R还有下一棵子树)
PreOrder(T);//后序遍历下一棵子树
visit(R);//访问根结点
}
}
层次遍历(用队列实现,广度优先)
森林的遍历分为先序遍历、中序遍历
7、哈夫曼树
(1)前置知识
结点的权:有某种现实含义的数值(如:表示结点的重要程度等)
结点的带权路径长度:从树的根节点到该节点的路径长度(经过的边数)与该结点上权值的乘积。
树的带权路径长度(WPL):树中所有叶结点的带权路径长度之和,是唯一的,因为父节点(双亲结点)是唯一的
(2)概念
在含有n个带权叶结点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树。