栈 ————先进后出
栈的顺序实现
typedef int Position;
typedef int ElementType;
typedef struct SNode * PtrToSNode;
struct SNode{ //封装成结构体
ElementType *Data; //data数组 下标从0--MaxSize-1
Position Top; //top的位置 一开始初始化成-1
int MaxSize; //容量
};
typedef PtrToSNode Stack;
Stack CreateStack(int Maxsize){ //创建
Stack S = (Stack)malloc(sizeof(struct SNode)); //申请整个结构体的空间
S->Data = (ElementType*)malloc(Maxsize*sizeof(ElementType)); //结构体内数组的空间
S->Top = -1;
S->MaxSize = Maxsize;
return S;
}
bool IsFull(Stack S){ //满:下标
return (S->Top==S->MaxSize-1);
}
bool Push(Stack S,ElementType X){
if(IsFull(S)){
printf("Full\n");
return 0;
}
else{
S->Data[++(S->Top)] = X;
return 1;
}
}
bool IsEmpty(Stack S){
return (S->Top==-1);
}
ElementType Pop(Stack S){
if(IsEmpty(S)){
printf("null\n");
return error;
}
else
return S->Data[(S->Top)--];
}
一维数组实现双栈
两个栈分别从数组的两头开始向中间生长
栈的链式实现
typedef int ElementType;
typedef struct SNode *PtrToSNode;
struct SNode{
ElementType Data;
PtrToSNode Next;
};
typedef PtrToSNode Stack;
Stack CreateStack(){
Stack S; //链表头指针
S = (Stack)malloc(sizeof(SNode));
S->Next = NULL;
return S;
}
bool IsEmpty(Stack S){
return (S->Next==NULL);
}
bool Push(Stack S,ElementType X){
PtrToSNode TmpCell;
TmpCell = (PtrToSNode)malloc(sizeof(struct SNode));
TmpCell->Data = X;
TmpCell->Next = S->Next;
S->Next = TmpCell;
//s本身没有数,永远是指向的下一个是top元素,tmp插入s后面
return 1;
}
ElementType Pop(Stack S){
PtrToSNode FirstCell;
ElementType TopElem;
if(IsEmpty(S)){
printf("NULL\n");
return error;
}
else{
FirstCell=S->Next;
//s指向的下一个,是top元素
TopElem = FirstCell->Data;
S->Next = FirstCell->Next;
free(FirstCell);
return TopElem;
}
}
应用:后缀表达式
队列
队列的顺序实现
#define ERROR 0x3f3f3f3f
typedef int ElementType;
typedef int Position;
struct Node{
ElementType *Data;
Position Front,Rear;
int Maxsize;
};
typedef Node * Queue;
Queue CreateQueue(int Maxsize){ //初始化
Queue q = (Queue)malloc(sizeof(Node));
q->Data = (ElementType*)malloc(sizeof(ElementType)*Maxsize);
q->Maxsize = Maxsize;
q->Front = q->Rear = 0; //不能是-1⚠️⚠️
return q;
}
bool IsFull(Queue q){ //判断是否满
return ((q->Rear+1)%q->Maxsize) == q->Front;
}
bool IsEmpty(Queue q){ //判断是否为空
return q->Front==q->Rear;
}
bool Pop(Queue q){ //从队首删除
if(IsEmpty(q)) return 0;
q->Front = (q->Front+1)%q->Maxsize;
return 1;
}
bool Push(Queue q,ElementType x){ //从队尾添加
if(IsFull(q))
return 0;
q->Rear =(q->Rear+1)%q->Maxsize;
q->Data[q->Rear] = x;
return 1;
}
ElementType GetFront(Queue q){ //获得队首第一个
if(IsEmpty(q)){
puts("isempty");
return ERROR;
}
q->Front = (q->Front+1)%q->Maxsize;
return q->Data[q->Front];
}
void Print(Queue q){
if(IsEmpty(q)) cout << "空的" << endl;
else{
for(Position i=q->Front+1; i<=q->Rear; i++)
cout << q->Data[i] << " ";
cout << endl;
}
}
队列的链式实现
#define ERROR 0x3f3f3f3f
typedef int ElementType;
struct Node{ //单个元素
ElementType Data;
struct Node * next;
};
typedef struct Node * PtrToNode;
struct QNode{ //队列头尾指针
PtrToNode Front, Rear;
};
typedef struct QNode * PtrToQNode;
typedef PtrToQNode Queue;
Queue CreateQueue(){ //初始化
Queue q = (Queue)malloc(sizeof(QNode));
q->Front = q->Rear = (PtrToNode)malloc(sizeof(struct Node)); //带头节点
q->Front->next = NULL;
return q;
}
bool Isempty(Queue q){ //判断是否为空
return q->Front==q->Rear;
}
void Push(Queue q,ElementType x){ //从队尾入队
PtrToNode t = (PtrToNode)malloc(sizeof(struct Node));
t->Data = x;
t->next = NULL;
q->Rear->next = t;
q->Rear = t;
}
bool Pop(Queue q){ //从队首删除
if(Isempty(q)) return 0;
if(q->Front->next==q->Rear) q->Rear = q->Front;
//删除到rear位置--只有一个元素
q->Front->next = q->Front->next->next;
return 1;
}
ElementType GetFront(Queue q){ //取出第一个元素
if(Isempty(q))
return ERROR;
return q->Front->next->Data;
}
void Print(Queue q){
if(q->Front->next==NULL) printf("空的\n");
else{
PtrToNode t = q->Front->next;
while(t){
cout << t->Data << " ";
t = t->next;
}
cout << endl;
}
}
二叉树 BT
⚠️二叉树的基本概念
完全二叉树
一棵深度为k,n个结点的二叉树,从上到下从左到右编号,如果编号和满二叉树相同:完全二叉树
即叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。需要注意的是,满二叉树肯定是完全二叉树,而完全二叉树不一定是满二叉树。
完全二叉树判定编辑
算法思路
判断一棵树是否是完全二叉树的思路 [3]
1>如果树为空,则直接返回错
2>如果树不为空:层序遍历二叉树
2.1>如果一个结点左右孩子都不为空,则pop该节点,将其左右孩子入队列;
2.1>如果遇到一个结点,左孩子为空,右孩子不为空,则该树一定不是完全二叉树;
2.2>如果遇到一个结点,左孩子不为空,右孩子为空;或者左右孩子都为空;则该节点之后的队列中的结点都为叶子节点;该树才是完全二叉树,否则就不是完全二叉树;
!!!注意if判断的顺序 flag和都不为空的要在都不为空的判断前面
bool checkisComplete(Bintree T){
if(!T) return 0;
queue<Bintree>q;
q.push(T);
Bintree t;
bool flag = 0;
while(!q.empty()){
t = q.front(); q.pop();
if(t->right && !t->left) //右有 左无
return 0;
else if(flag && (t->right || t->left))
return 0;
else if(!flag && !t->right) //两种情况 左右空或者右空 那么之后的都要是叶子
flag = 1;
else if(t->left && t->right){
q.push(t->left);
q.push(t->right);
}
}
return 1;
}
满二叉树(完美二叉树)
所有的分支节点都存在左右子树并且所有叶子结点在同一层上
一个二叉树层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树
链式存储
typedef struct TNode *Position;
typedef Position BinTree;
typedef int ElementType;
struct TNode{
ElementType Data;
BinTree Left;
BinTree Right;
};
创建
仅中序:不可
先序创建—递归
void preCreateBinTree(BinTree &T){
cin >> ch;
if(ch=='0') T = NULL;
else{
T = (BinTree)malloc(sizeof(TNode));
T->Data = ch; //根
preCreateBinTree(T->Left); //左
preCreateBinTree(T->Right); //右
}
}
层序创建—队列
BinTree CreateBinTree(){ //层序创建
ElementType a;
cin >> a;
if(a==-1) return NULL;
BinTree T = (BinTree)malloc(sizeof(TNode));
T->Data = a; T->Right = T->Left = NULL;
queue<BinTree>q;
BinTree t;
q.push(T);
while(!q.empty()){
t = q.front(); q.pop();
cin >> a;
if(a=='0') t->Left = NULL;
else{
t->Left = (BinTree)malloc(sizeof(TNode));
t->Left->Data = a;
t->Left->Left = t->Left->Right = NULL;
q.push(t->Left);
}
cin >> a;
if(a=='0') t->Right = NULL;
else{
t->Right = (BinTree)malloc(sizeof(TNode));
t->Right->Data = a;
t->Right->Left = t->Right->Right = NULL;
q.push(t->Right);
}
}
return T;
}
二叉树的遍历
前序遍历 根左右
递归
void Preorder(BinTree T){ //先序遍历
if(T){
cout << T->Data << " ";
Preorder(T->Left);
Preorder(T->Right);
}
}
堆栈
void Preorder(BinTree T){
BinTree t = T;
stack<BinTree>st;
while(t || !st.empty()){
while(t){ //左子树全放入
cout << t->Data << " "; //先输出根 每次往左 都输出
st.push(t);
t = t->Left;
}
t = st.top(); //取出来转右边
st.pop();
t = t->Right;
}
}
中序遍历 左根右
递归
void Inorder(BinTree T){ //中序遍历
if(T){
Inorder(T->Left);
cout << T->Data << " " ;
Inorder(T->Right);
}
}
堆栈
void Inorder(BinTree T){
BinTree t = T;
stack<BinTree>st;
while(t || !st.empty()){
while(t){ //左子树全放入
st.push(t);
t = t->Left;
}
t = st.top(); //取出来
cout << t->Data << " "; //输出根
st.pop();
t = t->Right; //转右边
}
}
后序遍历 左右根
递归
void Postorder(BinTree T){ //后续遍历
if(T){
Postorder(T->Left);
Postorder(T->Right);
cout << T->Data << " ";
}
}
堆栈
void Postorder(BinTree T){
BinTree cur = T;
stack<BinTree>st;
BinTree last=NULL,top;
while(cur || !st.empty()){
while(cur){ //左子树全放入
st.push(cur);
cur = cur->Left;
}
top = st.top(); //取出来
if(!top->Right || top->Right == last){
st.pop();
cout << top->Data << " " ;
last = top;
}
else
cur = top->Right;
}
}
层序遍历
void Levelorder(BinTree T){ //层序遍历
if(!T) return ;
queue<BinTree>q;
BinTree t;
q.push(T);
while(!q.empty()){
t = q.front(); q.pop();
cout << t->Data << " " ;
if(t->Left) q.push(t->Left);
if(t->Right) q.push(t->Right);
}
}
基础遍历方法的应用
只遍历叶子结点
前中后序都能实现 且输出顺序相同:即语句123交换顺序 输出叶子节点的顺序不变
因为前中后序走遍树的路径时一样的,只是输出各个节点顺序不同
所以途径叶子结点的顺序是一样的,输出顺序也是一样的
void Printleaves(BinTree T){ //只遍历叶子结点:前中后序都能实现 且输出顺序相同
static int cnt = 0;
if(T){
if(!T->Left && !T->Right){
cnt++;
cout << T->Data;
} //1
Printleaves(T->Left); //2
Printleaves(T->Right); //3
}
}
线存储
int T[1005];
void Create(){
int N,a,b,c;
cin >> N;
memset(T,-1,sizeof(T));
T[1] = 1;
while(N--){
cin >> a >> b >> c;
T[a*2] = b;
T[a*2+1] = c;
}
}
void Preorder(int *T,int index){
if(T[index] != -1){
cout << T[index];
Preorder(T,index*2);
Preorder(T,index*2+1);
}
}
void Inorder(int *T,int index){
if(T[index]!=-1){
Inorder(T,index*2);
cout << T[index];
Inorder(T,index*2+1);
}
}
void Postorder(int *T,int index){ //左右根
if(T[index]!=-1){
Postorder(T,index*2);
Postorder(T,index*2+1);
cout << T[index];
}
}
恢复二叉树
思路:递归
找到根位置
递归去创建子树
跳到需要的先序/中序序列起始位置
先序+中序
BinTree Create(char* pre,char* mid,int len){
if(len<=0) return NULL;
BinTree T = (BinTree)malloc(sizeof(struct TNode));
T->data = pre[0];
int i;
for(i=0; i<len; i++)
if(pre[0]==mid[i]) break;
//当前的根 是pre【0】在mid中找到一致的部分,然后对于这个根来说,
T->left = Create(pre+1,mid,i);
T->right = Create(pre+i+1,mid+i+1, len-1-i);
return T;
}
后序+中序
BinTree Create(int post[],int mid[],int len){
if(len<=0) return NULL;
BinTree T = (BinTree)malloc(sizeof(struct TNode));
T->data = post[len-1];
int i;
for(i=0; i<len; i++)
if(post[len-1]==mid[i]) break;
//左右根
//左根右
T->left = Create(post,mid,i);
T->right = Create(post+i,mid+1+i,len-i-1);
return T;
}
深度
递归实现!
注意不能是static h,因为每次都是从0开始走,系统自动创建新的h,然后走出来一个深度之后跟当前的这个比较,更新第一个h,
最后返回第一个h
int getheight(BinTree T){
int h = 0;
if(T)
h = max(getheight(T->left),getheight(T->right))+1;
return h;
}
镜像反转
递归遍历的模板➕change
void reverse(BinTree T){ //加不加引用都可以
if(T){
BinTree t = T->left;
T->left = T->right;
T->right = t;
reverse(T->left);
reverse(T->right);
}
}
二叉搜索树BST
概念
二叉排序树 二叉查找树
非空左子树所有键值小于其根节点
非空右子树所有键值大于其根节点
左右子树本身也都是二叉搜索树
查找元素X
递归的形式
Position Find(BinTree BST,ElementType x){
if(!BST) return NULL;
if(x > BST->Data)
return Find(BST->Right,x); //return 不要漏掉
else if(x < BST->Data)
return Find(BST->Left,x); //return 不要漏掉
else
return BST;
}
非递归:
Position Find(BinTree BST,ElementType x){
while(BST){
if(x>BST->Data)
BST = BST->Right;
else if(x<BST->Data)
BST = BST->Left;
else break;
}
return BST;
}
对于二叉搜索树进行查找的时间复杂度是由查找过程中的比较次数来衡量的,比较是从根结点到叶结点的路径进行的,取决于树的深度,最好情况是O(logN),所以最好的查找复杂度是O(logN)
但是 eg 单枝树,查找的时间复杂度就成了线性的O(N)
查找最小/大元素
Position FindMin(BinTree BST){
if(!BST) return NULL;
else if(!BST->Left) return BST;
else return FindMin(BST->Left); //不要忘记return
}
Position FindMax(BinTree BST){
if(!BST) return NULL;
else if(!BST->Right) return BST;
else return FindMax(BST->Right);
}
插入:成为叶子结点:可以用于创建
如果存在:插入失败
如果不存在 循环找 直到找到
Position Insert(BinTree BST,ElementType x){
if(!BST){
BST = (BinTree)malloc(sizeof(struct TNode));
BST->Data = x;
BST->Left = BST->Right = NULL;
}
else{
if(x>BST->Data)
BST->Right = Insert(BST->Right,x);//注意这里要 = 连起来
else if(x<BST->Data)
BST->Left = Insert(BST->Left,x);
}
}
删除
叶子结点:直接删,其父节点的指针置空
非叶子结点 只有一个孩子:孩子连到父亲的父亲上
非叶子结点 有左右两颗子树 那么要补上这个位置 保持有序性:用右子树中最小的元素或者左子树中最大的 :递归找到的这个结点一定是叶子结点或者是只有一个孩子结点的
BinTree Delete(BinTree BST,ElementType X){
BinTree t;
if(!BST) printf("no"); //空树
/*
if(!Find(BST,X)){
puts("Not Found");
return BST;
}
*/
else{
if(X>BST->Data)
BST->Right = Delete(BST->Right,X); //要等于 连起来
else if(X<BST->Data)
BST->Left = Delete(BST->Left,X);
else{
if(BST->Left && BST->Right){ //2个孩子节点
t = FindMin(BST->Right); //右找最小
//t = FindMax(BST->Left);
BST->Data = t->Data; //填充 注意不是直接等于t 物理位置不变!
BST->Right = Delete(BST->Right,BST->Data); //最小的删
}
else{ //0/1个子结点
t = BST;
if(BST->Right) //只有右孩子放上来
BST = BST->Right;
else
BST = BST->Left;
free(t);
}
}
}
return BST;
}
搜索树判断
bool IsBST ( BinTree T ){
static BinTree B = NULL;
if(T){
if(!IsBST(T->Left)) return false; //B及时记录T,T往下走,保证:左都是越来越小的
if(B && T->Data < B->Data )
return false;
B = T;
//非空左右子树的所有键值大于其根结点的键值,右侧子树小的部分不能小于根 要记录一下根
if(!IsBST(T->Right)) return false;
}
return true;
}
创建
for ( i=0; i<N; i++ ) {
scanf("%d", &X);
BST = Insert(BST, X);
}
lCA
int Find(Tree T,int x){
while(T){
if(x>T->Key) T = T->Right;
else if(x<T->Key) T = T->Left;
else break;
}
return T==NULL?0:1;
}
int LCA( Tree T, int u, int v ){
if(!T) return ERROR;
if(!Find(T,u) || !Find(T,v)) return ERROR;
if(T->Key>u && T->Key>v)
return LCA(T->Left,u,v);
if(T->Key<u && T->Key<v)
return LCA(T->Right,u,v);
return T->Key;
}
是否完全二叉搜索树
是否完全树+ 二叉搜索树
平衡二叉树
定义
平衡二叉树 AVL树
其插入删除查找操作均可在O(logN)下完成
定义:
(1)左右子树都是AVL
(2)根结点左右子树高度差不超过1 (0/1)
平衡因子
BF:balance factor
BF(T) = 左子树高度-右子树高度
AVL的BF只能从 -1 0 1取值
插入
插入新的节点时,有可能破坏树的平衡:四种 LL RR LR RL
因此:局部旋转
单旋
RR 向右倾斜型不平衡 —— 逆时针旋转——右单旋
LL 向左倾斜型不平衡 —— 顺时针旋转——左单旋
记忆
右单旋,以右为起点,往左转。解决向右倾斜的问题
理解:两侧深度差大于1就旋一下,使其相等
注意:三个节点的局部的旋转调整即可=
双旋
LR先左后右 ——左-右双旋 (先右单旋再左单旋)
为什么叫左右双旋emmm根据LR记住吧
RL
记忆
先左偏后右偏,右更靠近底层,所以先右单旋解决右偏,再往左单旋解决左偏
堆
定义概念
存储实现
原理:其实是线性的
角标当成树的结点
#define MAXDATA 0x3f3f3f3f
#define ERROR -1
typedef struct HNode * Heap;
typedef int ElementType;
struct HNode{
ElementType * Data;
int Size;
int Capacity;
};
typedef Heap MaxHeap;
typedef Heap MinHeap;
创建/ 初始化
MaxHeap CreateHeap(int MaxSize){
MaxHeap H = (MaxHeap)malloc(sizeof(struct HNode));
H->Data = (ElementType*)malloc(sizeof(ElementType)*(MaxSize+1));
H->Size = 0;
H->Capacity = MaxSize;
H->Data[0] = MAXDATA;
return H;
}
满了/空的
bool IsFull(MaxHeap H){
return H->Size==H->Capacity;
}
bool IsEmpty(MaxHeap H){
return H->Size==0;
}
插入 从最后位置往上比较到该放的位置
插入一个新的,先放在最后的位置(并不是真的放了),再往上一个一个比较,上一层的小于新的,上一层的放到下面,直到上一层的大于新的,新的就插在这个位置。
不是从n/2开始一直检查到1,这样会做很多无用功,从最后位置往上比较到该放的位置 ,顺着走一条就行了
每次/2是因为跟根比较即可 左右子树不用比较因为没有大小顺序
bool Insert(MaxHeap H,ElementType X){
if(IsFull(H))
return 0;
int i = ++H->Size;
for( ;H->Data[i/2]<X; i/=2)
H->Data[i] = H->Data[i/2];
H->Data[i] = X;
return 1;
}
删除
删除其实就是取出data[1],并且从下面找到一个最大值,填充上来。
堆的最后一个结点的位置需要操作
因为size-1了,最后一个节点先放到第一个的位置,再依次往下比较即可
ElementType Delete(MaxHeap H){
if(IsEmpty(H))
return ERROR;
ElementType MaxItem = H->Data[1];
ElementType X = H->Data[H->Size--];
int Parent,Child;
for(Parent = 1,Child; Parent*2<=H->Size; Parent=Child){
Child = Parent*2;
if((Child!=H->Size) && (H->Data[Child+1]>H->Data[Child]))
Child++;
if(X>=H->Data[Child])
break;
else
H->Data[Parent] = H->Data[Child];
}
H->Data[Parent] = X;
return MaxItem;
}
将已存在的数组转变为堆 不等于插入建堆
小顶堆改变比较的条件
void PercDown(MaxHeap H, int p){ //根结点往下筛一遍(三角形)
ElementType X = H->Data[p];
int Parent,Child;
for(Parent=p; Parent*2<=H->Size; Parent=Child){
Child = Parent*2;
if((Child!=H->Size) && (H->Data[Child+1]>H->Data[Child]))
Child++;
if(X>=H->Data[Child]){
break;
}
else
H->Data[Parent] = H->Data[Child];
}
H->Data[Parent] = X;
}
void BuildHeap(MaxHeap H){
for(int i=H->Size/2; i>0; i--) //每个根结点往下筛一遍
PercDown(H,i);
}
删除插入都能写成上下过滤
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
typedef int Elem;
struct heap{
Elem *data;
int capacity;
int size; //当前队列容量
};
using namespace std;
typedef struct heap* Heap;
Heap initHeap(int max){ //创建
Heap h;
h = (Heap)malloc(sizeof(struct heap));
if(!h)return NULL;
h->data = (Elem*) malloc(sizeof(Elem)*(max +1));
if(!h->data)return NULL;
h->capacity = max;
h->size = 0;
return h;
}
void destroy(Heap h)
{
if(h->data)free(h->data);
if(h)free(h);
}
void printHeap(Heap h){ //打印输出
if(h){
for(int i=1;i<=h->size;i++)
if(i==1)printf("%d",h->data[i]); //C语言有局限性
else printf(" %d",h->data[i]);
putchar('\n');
}
}
int isEmpty(Heap h)
{
return h->size==0;
}
int isFull(Heap h){
return h->capacity == h->size; //判断满
}
void percolateUp(int k,Heap h){ //上滤
Elem x;
x = h->data[k];
int i;
for( i=k;i>1 && x<h->data[i/2];i /= 2 )//最小堆 用x比较
{
h->data[i] = h->data[i/2];
}
h->data[i] = x ;
}
void percolateDown(int k,Heap h){ //下滤
Elem x;
x = h->data[k];
int i,child;
for(i=k;i*2<=h->size;i=child){
child = i*2;
if(child != h->size && h->data[child]> h->data[child +1])child++;
if(x > h->data[child])h->data[i]= h->data[child]; //跟最小的比,如果不是最小,儿子上移
else break;
}
h->data[i]=x;
}
// 插入堆
int insertHeap(Elem x,Heap h){
if(isFull(h))return 0;
h->data[++h->size] = x; //在数组最后一个位置插入
percolateUp(h->size, h); //上滤
return 1;
}
//删除堆元素
int removeHeap(Elem *px,Heap h){
if(isEmpty(h))return 0;
*px = h->data[1]; //取堆顶元素
h->data[1] = h->data[h->size--];
percolateDown(1,h); //下滤
return 1;
}
Heap buildHeap(Elem *a,int size,int max){
Heap h= initHeap(max);
if(!h)return NULL;
h->size = size;
for(int i=1;i<=size;i++)
h->data[i] = a[i-1];
for(int i=size/2;i>0;i--){
percolateDown(i,h); //从有子节点的开始往上走 每个父节点都下滤一次调整
}
return h;
}
int main()
{
int N,K,M;
cin >>N>>K;
Heap h;
h = initHeap(N);
int flag,tem;
for(int i=0;i<K;i++){
cin>>flag;
if(1==flag){
cin >> tem;
insertHeap(tem,h);
}
else if(-1 ==flag)
{
removeHeap(&tem,h);
}
printHeap(h);
}
cin >>M;
int a[M];
for(int i=0;i<M;i++)cin>>a[i];
Heap h2 = buildHeap(a,M,1000);
printHeap(h2);
return 0;
}
哈夫曼树
一棵树的路径长度是指从树根到其余各结点的路径长度之和
一个节点的带权路径长度是指从根结点到该节点之间的路径长度与该节点上的所带权值的乘积
带权路径长度 WPL
一棵树的带权路径长度是每个叶结点的带权路径长度之和
定义:
给定n个权值,构造具有n个叶子的二叉树,其中带权路径长度最小的的树就叫做哈夫曼树,也称为最优二叉树
注意
给定n个权值是叶子结点的值,只有叶子结点的权值*深度 需要计算
typedef struct HTNode * HuffmanTree;
struct HTNode{
int Weight; //权值
HuffmanTree Left;
HuffmanTree Right;
};
构造
目的:权值越大的叶子结点越靠近根结点
思路:贪心算法
过程
- 给定n个权值,每一个独立成为只有一个叶子结点的二叉树(不是根结点),总共n个T,集合F
- 选择最小的两个,最为左右子树合并,根结点为左根+右根
- F删除构造过的两个,并且将构造后的结果放入
- 重复直到F只有一个
核心
HuffmanTree Huffman(MinHeap H){
//小顶堆:权值小的放上面 元素类型是HuffmanTree
BuildHeap(H); //将H->data按照权值调整成小顶堆 每次拿最小的两个
HuffmanTree T;
int N = H->Size;
for(int i=0; i<N; i++){
T = (HuffmanTree)malloc(sizeof(struct HTNode));
T->Left = Delete(H);
T->Right = Delete(H);
T->Weight = T->Left->Weight + T->Right->Weight;
Insert(H,T); //将新的T插入到最小堆
}
return Delete(H);
}
完整
typedef struct HTNode * HuffmanTree;
struct HTNode{
int Weight; //权值
HuffmanTree Left;
HuffmanTree Right;
};
#define MAXDATA 0x3f3f3f3f
#define MINDATA -0x3f3f3f3f
#define ERROR -1
typedef struct HNode * Heap;
typedef HuffmanTree ElementType;
struct HNode{
ElementType * Data;
int Size;
int Capacity;
};
typedef Heap MaxHeap;
typedef Heap MinHeap;
void PercDown(MinHeap H, int p){ //根结点往下筛一遍(三角形)
ElementType X = H->Data[p];
for(int Parent=p,Child; Parent*2<=H->Size; Parent=Child){
Child = Parent*2;
if((Child!=H->Size) && (H->Data[Child+1]<H->Data[Child]))
Child++;
if(X<=H->Data[Child]){
H->Data[Parent] = X;
break;
}
else
H->Data[Parent] = H->Data[Child];
}
}
void BuildHeap(MinHeap H){
for(int i=H->Size/2; i>0; i--) //每个根结点往下筛一遍
PercDown(H,i);
}
bool IsFull(MaxHeap H){
return H->Size==H->Capacity;
}
bool IsEmpty(MaxHeap H){
return H->Size==0;
}
bool Insert(MinHeap H,ElementType X){
if(IsFull(H))
return 0;
int i = ++H->Size;
for( ;H->Data[i/2]>X; i/=2)
H->Data[i] = H->Data[i/2];
H->Data[i] = X;
return 1;
}
ElementType Delete(MinHeap H){
if(IsEmpty(H))
return NULL;
ElementType MinItem = H->Data[1];
ElementType X = H->Data[H->Size--];
for(int Parent = 1,Child; Parent*2<=H->Size; Parent=Child){
Child = Parent*2;
if((Child!=H->Size) && (H->Data[Child+1]<H->Data[Child]))
Child++;
if(X<=H->Data[Child]){
H->Data[Parent] = X;
break;
}
else
H->Data[Parent] = H->Data[Child];
}
return MinItem;
}
HuffmanTree Huffman(MinHeap H){
//小顶堆:权值小的放上面 元素类型是HuffmanTree
BuildHeap(H); //将H->data按照权值调整成小顶堆 每次拿最小的两个
HuffmanTree T;
int N = H->Size;
for(int i=0; i<N; i++){
T = (HuffmanTree)malloc(sizeof(struct HTNode));
T->Left = Delete(H);
T->Right = Delete(H);
T->Weight = T->Left->Weight + T->Right->Weight;
Insert(H,T); //将新的T插入到最小堆
}
return Delete(H);
}
哈夫曼编码
集合 并查集
集合是一种常用的数据表示方法,集合运算包括交并补差,判断元素是否属于等
为了有效的实现集合的操作,可以用树结构表示集合
注意:这里子结点指向父结点有利于判断元素属于哪个集合,也便于集合的归并运算:运算关注集合中的元素而不是集合本身
所以,集合名结构其实是没有必要存在的:直接用树的根结点的编号来代表一个集合
N个元素,标号0-N-1,顺序存放到数组里,
数组第8个元素存放3,表示其父结点是3 pre[x8] = 3
英文:Disjoint Set,即“不相交集合”
将编号分别为1…N的N个对象划分为不相交集合,
在每个集合中,选择其中某个元素代表所在集合。
常见两种操作:
合并两个集合
查找某元素属于哪个集合
定义
#define MAXN 1000 //集合最大元素的个数
typedef int ElementType;
typedef int SetName; //默认用根结点的下标来作为集合的名称
typedef ElementType SetType[MAXN];
合并查找两个操作的实现
简单暴力
按照深度不同合并
深度小的树合并到深度大的树
路径压缩
散列查找
之前学过的查找
顺序查找
二分查找
二叉搜索树
基本概念
-
符号表 散列表 哈希表 Table
-
装填因子
a = n/m
装填因子 = 填入表中的元素个数 / 散列表的空间大小
——空间大小 = 填入表中的元素个数/装填因子 -
散列
散列是一种重要的查找方法
根据数据对象的关键字为自变量,通过一个确定的函数关系h,计算出响应的hkey,这个值作为数据对象的存储地址
散列函数构造方法
数字关键词
直接定址法 | 除留余数法 | 数字分析法 |
---|---|---|
找到一个线性函数作为散列地址 | 散列表长TableSize = n/a ,p为≦TableSize的某个最大素数 | 如果关键词位数太多eg手机号,那么只留下四位 |
h(key) = a*key+b | h(key) = key mod p | h(key) = atoi(key+7) |
|TableSize|8|16|32|64|128|256|512|1024|
|–|–|–|–|–|–|–|–|–|–|–|–|–|–|–|
|P|7|13|31|61|127|251|503|1019
地址p到TableSize-1是不能通过映射函数直接映射到的
字符串关键词
ASCII码加和法 | 前三个字符移位法 | 移位法 |
---|---|---|
27进制:26字母+空格 ; 注意不够三位\0也计算 | n个字符都计算 每位字符占5位 这样做✖️法运算32(2^5 =32)的时候就可以左移了 | |
每个字符ASCII加和 | ( key0 + key1 * 27 + key2 * 27^2)mod TableSize | (keyn-1*32^ 0 + keyn-2 * 32^1 + keyn-3 *32 ^ 2 +……)mod TableSize |
int Hash(const char *Key, int TableSize){
unsigned int H = 0;
while(*Key!='\0')
H = (H<<5) + *Key++;
return H%TableSize;
}
处理冲突的办法
1⃣️ 开放地址法
- 线性探测法
1 2 3 4 5…… - 平方探测法
1方 - 1方 2方 -2方…… - 双散列探测法
- 再散列法
2⃣️ 分离链接法
所有关键词为同义词的数据对象通过结点链接存储在同一个单链表内
散列函数得到的散列地址做为指针指向一个链
图
树:分枝层次关系
图:邻接关系
图的定义和术语
相关术语 | |
---|---|
无向图 | |
有向图 | |
简单图 | |
邻接点 | |
路径 | |
简单路径 | |
回路 | |
无向完全图 | |
有向完全图 | |
度 | |
入度出度 | |
稠密图 | |
稀疏图 | |
权 | |
网图 | |
子图 | |
连通图 | |
强连通图 | |
生成树 | |
生成森林 |
存储结构
|||
|–|–|–|
|链表
|数组|一维
||二维
操作
图 | ||
---|---|---|
最小生成树 | Kruskal | 并查集 选min判断是否有回路 |
Prim | 当前树不断加入最近的边 | |
单源最短路径 | Dijkstra | dist path 每次选离起点距离最近的 更新 |
每一对顶点之间的最短路径 | 两次Dijkstra | |
Floyed | 水平和垂直的投影元素之和 | |
搜索 | BFS | |
DFS |
排序
除了基数其余都是基于比较的函数
排序 | ||
---|---|---|
选择排序 | 简单选择排序 | 选择最小的交换 |
堆排序 | 选择max交换 | |
插入排序 | 简单插入 | 未排序的逐一插入 |
希尔排序 | 分组插入 | |
交换排序 | 冒泡 | 交换相邻的 |
快速排序 | 交换low high | |
归并排序 | ||
基数排序 | 桶 | |
基数 |
选择
直接选择
思想:
在未排序的序列中选择最小的和首位元素交换
接下来在未排序的序列中选择最小的和序列的第二位元素交换
void SimpleSelectionSort(int A[],int n){
int min;
for(int i=0; i<n-1; i++){ //每次待交换的位置 从0到n-2
min = i;
for(int j=i+1; j<n; j++) //用min一起比较就从i+1开始 如果不用就从i开始
if(A[j]<A[min]) min = j;
swap(A[i],A[min]);
}
}
最好最坏都是一样的,需要走一遍才知道
所以任何情况都是
n + n-1 + n-2 …… = (1+n)*n/2
时间复杂度是O(N^2)
不稳定
eg 3 3 1
堆
1⃣️思想:
利用堆这种数据结构
堆:特殊的二叉树 每个节点的值都小于/大于其父节点
由于堆是完全二叉树:堆排序用数组实现
2⃣️简单实现:空间复杂度O(N)
利用最大堆输出堆顶元素,将剩余的其余元素重新生成最大堆 继续输出
这样需要一个辅助数组
3⃣️高级实现:空间复杂度O(1)
生成最大堆
把堆顶元素与最后的元素交换位置
剩余元素重新生成最大堆:自上而下过滤一次(注意个数-1,排除已经排好序的元素)
PercDown(A, 0, i);
A数组:每次过滤的范围:从0–i
I:每次放的位置:从n—1
4⃣️因为堆没有规定相同的元素应该放在左子树还是右子树,所以堆排序是不稳定的。
5⃣️两种方法时间复杂度同
总共n个元素 每次需要取出第一个然后从上往下筛一遍树深
O(NLogN)
void PercDown(int A[],int p,int n){ //总共n个元素:为了确定下标 A[p]为根开始调整
int parent,child;
int x;
x = A[p]; //根
for(parent=p; parent*2+1<n; parent=child){ //下标从0开始
child = parent*2+1;
if(child!=n-1 && A[child+1]>A[child])
child++;
if(x>=A[child])
break;
else
A[parent]=A[child];
}
A[parent]=x;
}
void HeapSort(int A[],int N){
for(int i=N/2; i>=0; i--) //调整成最大堆
PercDown(A,i,N);
for(int i=N-1; i>0; i--){
swap(A[0],A[i]);
PercDown(A,0,i);
}
}
java
public void percolateDown(int k, int[] arr,int end) {
int x = arr[k];
int parent, child;
for (parent = k; parent * 2 <= end; parent = child){
child = parent*2;
if(child!=end && arr[child+1]>arr[child]){
child++;
}
if(x>arr[child]){
break;
}
else{
arr[parent] = arr[child];
}
}
arr[parent] = x;
}
public void heapSort(int[] arr) {
int len = arr.length;
for(int i=arr.length/2; i>0; i--){
percolateDown(i,arr,len);
}
int pos = arr.length;
int temp;
for(int i=0; i<arr.length; i++){
temp = arr[pos];
arr[pos] = arr[0];
arr[0] = temp;
percolateDown(0,arr,--len);
}
}
插入
简单插入
1⃣️思想:已排好序部分和未排好序部分,依次比较 swap
初始状态已排序只有一个元素,未排序N-1
将未排序的元素逐一插入到已排序的 总共插N-1次
具体实现:对应第K个元素,之前的K-1个默认排好序,
将其与K-1比较,swap
接着与K-2比较
直到不swap或者已经是第一个了
2⃣️实现
void InsertSort(int A[],int N){
for(int p=1; p<N; p++){
int t = A[p]; //未排序的第一个
int i;
for(i=p; i>0&&A[i-1]>t; i--)
A[i] = A[i-1];
A[i] = t;
}
}
3⃣️稳定 :相同的元素 不会swap:相对位置不变
最好:每次走到一个数跟前面的比不用换就加入到已排好序部分 ON
最差:每次都要从未排好序的第一个走到已排好序的第一个 ON2
平均:n2
不需要递归 不需要辅助空间
希尔排序
1⃣️思想:
待排序的按照一定间隔(分成几组)分成若干序列 分别进行插入排序
最后间隔变成1
2⃣️实现
3⃣️
希尔排序会多次进行插入排序,一次插入排序是稳定的,但是因为希尔排序每次插入排序选择的步长不一样,导致希尔排序不稳定。
不需要递归 不需要辅助空间
时间复杂度跟其步长选择有关 目前没有最优 但是肯定小于N2
最差的:每一次的分组都要全部交换
最好:假如总共需要分组k次,每次都是不用交换 走一遍n即可,所以最好的是KN—N
public void shellSort(int[] arr) {
int len = arr.length;
int gap = len >> 1;
int i,j,temp;
while (gap != 0) {
for ( i = gap; i < len; i++) {
temp = arr[i];
//j大于i j放到后面
//temp是最小的 每次temp移到前面
//当gap=1时就可以有序了
for ( j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {
arr[j+gap] = arr[j];
}
//j跳出时 小于0 加上gap才是当前不长的分组的第一位
arr[j+gap] = temp;
}
gap >>= 1;
}
}
交换
冒泡
1⃣️思想:
i与i++判断是否swap , i++
每次最大的沉底
2⃣️
最坏:N+N-1+…… =(1+N)N/2 = N2
最好:flag标记
一次过后 都不用动:N
public void bubbleSort(int[] arr) {
int len = arr.length, temp;
boolean flag;
for (int i = 1; i < len; i++) { //n-1轮 从1开始表示次数 而不是数组下标
flag = false;
for (int j = 0; j < len - i; j++) {
if (arr[j] > arr[j + 1]) {
flag = true;
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
if(!flag){
break;
}
}
}
快速
1⃣️思想:
每次找到基准的正确位置
2⃣️实现:递归
java
public void quickSort(int[] arr, int begin, int end) {
if(begin>end){
return;
}
int left = begin;
int right = end;
int temp = arr[begin];
//没找到合适的 temp的位置
while (left != right) {
while (arr[left] <= temp && left < right) {
left++;
}
while (arr[right] >= temp && left < right) {
right--;
}
if (left < right) {
int t = arr[left];
arr[left] = arr[right];
arr[right] = t;
left++;
right--;
}
}
arr[begin] = arr[left];
arr[left] = temp;
quickSort(arr,begin,left-1);
quickSort(arr,right+1,end);
}
public void quickSort(int[] arr, int begin, int end) {
if(begin>end){
return;
}
int left = begin;
int right = end;
int temp = arr[begin];
//没找到合适的 temp的位置
while (left != right) {
while (arr[right] >= temp && left < right) {
right--;
}
while (arr[left] <= temp && left < right) {
left++;
}
if (left < right) {
int t = arr[left];
arr[left] = arr[right];
arr[right] = t;
// left++;
// right--;
}
}
arr[begin] = arr[left];
arr[left] = temp;
quickSort(arr,begin,left-1);
quickSort(arr,right+1,end);
}
注意顺序
3⃣️
最坏时间复杂度:每次都没有能够折半 N^2
最好和平均:可以折半或者类似折半 NlogN
快速排序的时间复杂度和是否有序无关,是看每次基准能否把递归的子列一分为二:递归的深度是LogN
所以一开始有序也不是NlogN
辅助空间
由于递归 也就是需要深度这么多LogN
归并
1⃣️思想:建立在归并操作上
归并操作:两个已经排好序的子序列合并成一个有序序列的过程
归并:长为N的序列看成N个长为1的子序列 接下来两两归并 成长度为2的
接下来继续2和2的归并
2⃣️
递归实现
3⃣️
每次左右一半要求有序 深度LogN
NlogN
每次都要遍历两个子序列 :不管好坏都一样
额外空间存放合并后的结果 O(N)
稳定
java
public int[] merge(int[] left, int[] right) {
int[] result = new int[left.length + right.length];
for (int index = 0, i = 0, j = 0; index < result.length; index++) {
if (i >= left.length) { //i没了
result[index] = right[j++]; //用j
} else if (j >= right.length) {
result[index] = left[i++];
} else if (left[i] < right[j]) {
result[index] = left[i++];
} else {
result[index] = right[j++];
}
}
return result;
}
public int[] mergeSort(int[] arr) {
if (arr.length < 2) {
return arr;
}
int mid = arr.length / 2;
int[] left = Arrays.copyOfRange(arr, 0, mid);
int[] right = Arrays.copyOfRange(arr, mid, arr.length);
left = mergeSort(left);
right = mergeSort(right);
return merge(left, right);
}
基数
N个待排序的 R个桶 D待排序的数按照基数分解的位数(需要几趟)
D(N+R)
好坏平均都一样
额外:链表实现 每个基数后面连着待排序的部分:N+R的
稳定