赞
踩
顺序表的实现–静态分配
#include "stdio.h" #define MAX_SIZE (100) typedef struct { int data[MAX_SIZE]; int length; }SqList; //初始化线性表 void initList(SqList & L) { for(int i = 0; i < MAX_SIZE; i++) { L.data[i] = 0 ; } L.length = 0; }
顺序表的实现–动态分配
#include "stdio.h" #define INIT_SIZE (10) typedef struct { int *data; int MaxSize; int length; }SqList; //初始化列表 void initList(SqList &L) { L.data = (int *)malloc(sizeof(int) * INIT_SIZE);//开辟内存 L.MaxSize = INIT_SIZE; L.length = 0; } //增加动态数组长度 void increaseSize(SqList &L,int len) { //实现原理:分三步 //1、保存原来数组指针,后续释放 //2、开辟新的内存空间 //3、拷贝原始数组数据,并释放原始数组内存空间 int *p = L.data; L.data = (int *)malloc(sizeof(int) * (INIT_SIZE + len)); for(int i = 0;i < L.length; i++) { L.data[i] = p[i]; } L.MaxSize = p.MaxSize + len; free(p); }
顺序表基本操作——静态链表插入
/*基于静态链表的实现*/ #include "stdio.h" #define MAX_SIZE (10) typedef struct { int data[MAX_SIZE]; int length; }SqList; bool insertElement(SqList &L,int i; int e) { //注意:i代表的是位序,所以位序不能超过length+1 //第3位对应数据下标为2的数据 if(i < 1 || i > L.length+1) return false;//插入超范围 if( L.length >= MAX_SIZE) return false;//内存空间已满 //实现逻辑: //1、插入位置元素后移,空出一个位置进行插入 //2、赋值,链表长度自增1 for(int j = L.length; j >= i; j-- ) { L.data[j] = L.data[j - 1]; } L.data[i - 1] = e; L.lenght++; return true; }
分析时间复杂度:
关注最深层循环语句——L.data[j]=L.data[j-1]的执行次数与问题规模n——L.length的关系;
最好情况:插入表尾,不需要移动元素,i=n+1,循环0次;最好时间复杂度 = O(1)
最坏情况:插入表头,需要将原有的n个元素全都向后移动,i=1,循环n次;最坏时间复杂度 = O(n)
平均情况:假设新元素插入到任何一个位置的概率p(=1/n+1)相同
平均循环次数 = np + (n-1)p + (n-2)p + … + 1×p = [ n(n+1)/2 ]×[ 1/(n+1) ] = n/2
平均时间复杂度 = O(n)
顺序表基本操作——静态链表删除
/*基于静态链表的实现*/ #include "stdio.h" #define MAX_SIZE (10) typedef struct { int data[MAX_SIZE]; int length; }SqList; bool deleteList(SqList &L,int i,int &e) { //注意:按位删除,即删除第i位元素 = delete data[i-1] if(i <1 || i > L.length) return false; e = L.data[i-1];//保存这个元素 //实现逻辑 //1、除元素后面元素位置上移 //2、链表长度减1 for(int j = i; j < L.length; j++) { L.data[j - 1] = L.data[j] ; } L.length--; }
分析时间复杂度:
关注最深层循环语句——L.data[j-1]=L.data[j]的执行次数与问题规模n——L.length的关系;
最好情况:删除表尾元素,不需要移动元素,i=n,循环0次;最好时间复杂度 = O(1);
最坏情况:删除表头元素,需要将后续的n-1个元素全都向前移动,i=1,循环n-1次;最坏时间复杂度 = O(n);
平均情况:假设删除任何一个元素(1,2,3,…,length)的概率相同 p=1/n
平均循环次数 = (n-1)p + (n-2)p + … + 1×p = [ n(n-1)/2 ]×[ 1/(n) ] = n-1/2
平均时间复杂度 = O(n)
顺序表基本操作——查找(按位和按值)
1、按位查找,没啥好说的,分两步 //1、 判断i的值是否合法 //2、 return L.data[i-1]; //注意是i-1 // 算法时间复杂度为O(1) 2、按值查找 #include "stdio.h" #define INIT_SIZE (10) typedef struct { ElemType *data; int length; }SqList; bool findList(SqList L,ElemType e) { //实现原理,遍历查找 //可想而知,算法时间复杂度为O(n) for(int i = 0; i < L.length;i++) { if(L.data[i] == e) return i+1;//返回的是位序 else return 0; } }
链式存储
每个节点存储:数据元素自身信息和指向下一个节点的地址
优点:不要求大片连续区域,改变容量方便
缺点:不可随机存储,要耗费时间修改指针
结构定义
typedef struct LNode { ElemType data;//数据域 //结构体自引用指针,只能用指针,不然大小无法确定,会无限循环 struct LNode *next;//指针域 }LNode,*LinkList; //LNode --- 指代一个节点 //LinkList -- 表示这是一个单链表 LinkList LReal;//创建链表 InitList(LReal);//初始化 /***************************************************************************** 这里LinkList本身就是指针,为何传引用? 1、如果不传地址,则会产生一个形参副本,并不改变LReal本身 2、所以, 想改变LReal的值,必须传入地址。此时LTemp保存的是 LReal的地址,对LTemp的操作即是对LReal的操作 (LinkList <emp = LReal) 3、其实也可以直接创建赋值: LNode * head=(Node *)malloc(sizeof(LNode)); head->data=0; head->next=NULL; LReal = head; Note:typedef struct LNode * LinkList--意思是LinkList的类型是struct LNode * LinkList看成struct Lnode *的别名就行 *****************************************************************************/ //初始化一个空的单链表(不带头节点) bool InitList(LinkList <emp) { LTemp = NULL; //空表,暂时还没有任何结点; return true; } //初始化带头节点的单链表 bool initList(LinkList L) { L = (LNode*)malloc(sizeof(LNode)); if(L == NULL) return false;//内存空间不足 L->next = NULL; return false; } //判断链表是否为空 bool isEmpty(LinkLsit L) { //不带头结点 return (L==NULL) ? true:false; //带头结点 return (L->nexrt == NULL) ? true:false; }
单链表的插入
a、按位插入----带头结点
//L->head->a1->a2->a3>a4->a5->NULL 头结点看成0节点 (不存在数据) //第i个位置插入数据元素e //实现原理: //1、先判断节点是否合理 //2、第i位插入,即找到i-1那个节点(前一个节点) //3、申请新节点进行插入 bool insertList(LinkList &L,int i,ElemType e) { if(i < 1) return false; LNode*p = L;//指针p进行扫描 int j = 0;//p指向第几个节点,带头节点,头节点可看成第0个节点 //循环遍历,找到前一个节点,即i-1,同时得不能为NULL while(p!= NULL && j< i-1) { p = p->next; j ++; } if(p == NULL) return false;//i值不合法 //申请新节点进行插入 LNode *s = (LNode *)malloc(sizeof(LNode)); s->data = e; s->next = p->next; p->next = s; return true; } /******************************* 时间复杂度分析: 最好情况:插入第1个位置 O(1) 最坏情况:插入表尾 O(n) 平均时间复杂度 = O(n) *******************************/
b、按位插入----不带头结点
// L->a1->a2->a3>a4->a5->NULL bool insertList(LinkList &L,int i,ElemType e) { if(i < 1) return false; //如果插入到第一个节点 //插入第一个节点需要改变L的指向,所以要单独写 //不带头结点,链表为空,头指针指向NULL if(i == 1) { LNode * s = (LNode *)malloc(sizeof(LNode)); s->data = e; s->next = L; L = s; return true; } LNode *p = L; //p指向第几个节点,i位插入即要找到i-1那个节点后进行插入 //注意,这和带头结点不一样,因为第一个节点插入已实现 int j = 1;//侧面表示从第一个节点开始的 while(p!=NULL && j < i-1) { p = p->next; j++; } if(p == NULL) return false; LNode * s = (LNode *)malloc(sizeof(LNode)); s->data = e; s->next = p->next; p->next = s; return true; } /******************************* 时间复杂度分析: 最好情况:插入第1个位置 O(1) 最坏情况:插入表尾 O(n) 平均时间复杂度 = O(n) *******************************/
c、指定节点的后插操作
//给定一个节点p,在其后插入一个元素e(单链表智能向后查找,所以p之前节点无法得知) bool insertNextNode(LNode *p,ElemType e) { if(p == NULL) return false; LNode *s = (LNode *)malloc(sizeof(LNode)); if(s == NULL) return false; s->data = e; s->next = p->next; p->next = s; return true; } //显而易见,平均时间复杂度为O(1)
d、指定节点的后插操作
//如果给定头指针,之前查找p的前驱q,对q进行后插操作即可-------平均时间复杂度O(n) //如果没给头指针?直接把新节点插到p后面,然后交换数据域 -------平均时间复杂度O(1) bool insertPriorNode(LNode *p,ElemType e) { if(p == NULL) return false; LNode *s = (LNode *)malloc(sizeof(LNode)); if(s == NULL) return false; //后插并交换数据域 == 前插 s->next = p->next; p->next = s; s->data = p->data; p->data = e; }
单链表的删除
按位删除----带头结点
//删除表L中第i个位置的元素,并用e返回删除元素的值;头结点视为“第0个”结点 //实现原理: //1、找到i-1个节点,与i后面节点进行相连 //2、删除i节点 //3、平均时间复杂度为O(n) bool deleteNode(LinkList &L,int i,ElemType &e) { LNode *p = L; int j = 0;//从“第0个”节点开始扫描 while(p!=NULL && j < i-1) { p = p->next; j ++; } if(p == NULL) return false;//不合法 if(p->next == NULL) return false;//说明i-1后面无节点 LNode *q = p->next;//指向删除节点,便于后续释放 e = q->data;//获取要删除节点数据 p->next = q->next; free(q); return true; }
单链表的查找
a、按位查找----带头结点
//查找表中第i个元素的值 //平均时间复杂度 = O(n) LNode* getElem(LinkList L,int i) { if(i < 1) return NULL; LNode *p = L; int j = 0; while(p != NULL && j < i) { p = p->next; j++; } return p; }
b、按值查找----带头结点
//查找值为e的节点
//平均时间复杂度 = O(n)
LNode* locateElem(LinkList L,ElemType e)
{
LNode *p = L->next;
while(p!=NULL && p->data != e)
{
p = p->next;
}
return p;
}
单链表的长度
//从头到位遍历循环
//平均时间复杂度 = O(n)
int length(LinkList L)
{
LNode *p = L;
int length = 0;
while(p->next != NULL)
{
p = p->next;
length++;
}
return length;
}
单链表的建立
a、尾插法
//从尾部依次插入每个元素e //简单思路: //1、初始化单链表 //2、设置变量length记录链表当前的长度 //3、每插入一个元素都得循环遍历一次链表,所以时间复杂度位O(n*n) while { //每次取一个数据元素e; ListInsert(L, length+1, e)插到尾部; length++; } //解决方案:增加一个尾指针 LinkList List_TailInsert(LinkList &L) { int X;//假设ElemType为整数型 L = (LNode*)malloc(sizeof(LNode));//创建头结点 if(L == NULL) return NULL; LNode *s, LNode *rear = L;//节点和尾指针 scanf('%d',%&X); while(X!=111) { //插入 s = (LNode*)malloc(sizeof(LNode));//创建结点 s->data = X; rear->next = s;//将s串起来 rear = s;//指向尾节点 scanf('%d',%&X); } rear->next = NULL;//尾指针置空 return L; }
b、头插法
//每次从头部插入元素 LinkList List_HeadInsert(LinkList &L) { int X;//假设ElemType为整数型 L = (LNode*)malloc(sizeof(LNode));//创建头结点 if(L == NULL) return NULL; L->next = NULL;//尾部置空,不可或缺 LNode *s; scanf('%d',%&X); while(X!=111) { //插入 s = (LNode*)malloc(sizeof(LNode));//创建结点 s->data = X; s->next = L->next; L->next = s; scanf('%d',%&X); } return L; }
c、头插法应用----链表逆置
LinkList reverseList(LinkList &L) { LNode *p,*q; p = L->next;//获取链表第一个节点 q = p; L->next = NULL;//链表断开 while(p! = NULL) { q = p; p = p->next; q->next = L->next; L->next = q; } return L; }
双链表的结构
typedef struct DNode
{
ElemType data;
struct DNode *prior,*next;//前驱和后继指针
} DNode,*DLinkList;
双链表的初始化
//即创建一个头结点,头结点的前驱始终为NULL,后继节点暂时为NULL
bool initDLinkList(DLinkList &L)
{
L = (DNode *)malloc(sizeof(DNode));
if(L == NULL) return false;
L->prior = NULL;
L->next = NULL;
return true;
}
//判断链表是否为空
if(L->next == NULL )
双链表的插入
//在p节点之后插入s节点 //注意:双链表比较简单,注意插入或删除的节点是否是最后一个节点即可 bool insertNextNode(DNode *p,DNode *s) { if(p == NULL || s == NULL) return false; p->next = s->next; if(p->next != NULL) { //如果是最后一个节点插入,就没有前驱 p->next->prior = s; } s->prior = p; p->next = s; return true; }
双链表的删除
//删除p节点的后继节点 //注意:双链表比较简单,注意插入或删除的节点是否是最后一个节点即可 bool deleteNextNode(DNode *p) { if(p == NULL) return false; DNode *q = p->next; if(q == NULL) return false;//没有后继节点可删除 p->next = q->next; if(q->next != NULL) { //连接后继节点的前驱 //如果是最后一个节点,就没有后继节点 q->next->prior = p; } free(q); } //循环释放各个数据节点 void destoryList(DLinkList &L) { while(L->next != NULL) { deleteNextNode(L->next); } //释放头结点(即头指针指向的内存空间清零) free(L); //但是free后头指针仍然指向垃圾内存,所以要将头指针置为NULL //这也是delete和free的一个区别 L = NULL; }
双链表的遍历
//遍历的目的是什么?对节点p做相应处理 //后向遍历 while(p != NULL) { p = p->next; } //前向遍历 while(p!=NULL) { p = p->prior; } //前向遍历(跳过头结点) while(p->prior != NULL) { p = p->prior; }
循环单链表
//初始化时,头指针指向自己 L->next = L; //判断结点p是否为循环单链表的表尾结点 bool isTail(LinkList L,LNode *p) { if(p->next == L) return true; else return false; } //判断循环单链表是否为空 bool Empty(LinkList L) { if(L->next == L) return true; else return false; }
循环双链表
//初始化时,头指针指向自己 L->prior = L; L->next = L; //判断结点p是否为循环双链表的表尾结点 bool isTail(DLinklist L,DNode *p) { if(p->next == L) return true; else return false; } //判断循环双链表是否为空 bool Empty(DLinklist L) { if(L->next == L) return true; else return false; }
//因为链表的空间是离散的,所以静态链表的空间是连续的,类似数组
//没啥可研究的
#define MaxSize 10 //静态链表的最大长度
struct Node{ //静态链表结构类型的定义
ElemType data; //存储数据元素
int next; //下一个元素的数组下标
}SLinkList[MaxSize];
//主要是内存空间和增删改查时间复杂度的比较
//偷个懒,后续补上......
定义:栈是只允许在一端进行插入或者删除操作的线性表(类似羊肉串)
1、栈顶:允许插入和删除的一端
2、栈底:不允许插入和删除的一端
3、特点:后进先出(LIFO--Last In First Out)
定义
//顺序栈由静态数组和栈顶指针组成,其中栈顶指针存放的是数组下标
#define MAX_SIZE (100)
typedef struct
{
ElemType data[MAX_SIZE];
int top;//栈顶指针
}SqStack; //Sq--sequence 顺序的意思
初始化&&判空
//初始化
void initStack(SqStack &S)
{
S.top = -1;
}
//判空
bool isEmptyStack(SqStack S)
{
return ( S.top == -1) ? true : false;
}
进栈
bool push(SqStack &S,ElemType e)
{
if(S.top == MAX_SZIE - 1) return false;//栈满
S.top++;//指针+1
S.data[S.top] = e;//新元素入栈
return true;
}
出栈
//出栈只是从逻辑上删除了栈顶元素(指针下移),但实际上数据还在内存中
bool pop(SqStack &S,,ElemType &e)
{
if(S.top == -1) return false;//栈空
e = S.data[S.top];
S.top--;
return true;
}
读取栈顶元素
bool getTop(SqStack S,ElemType &x)
{
if(S.top == -1) return false; //栈空,报错
x=S.data[S.top];//x记录栈顶元素
return true;
}
共享栈
//两个栈共享同一片内存空间 #define MaxSize 10 //定义栈中元素的最大个数 typedef struct { ElemType data[MaxSize]; //静态数组存放栈中元素 int top0; //0号栈栈顶指针 int top1; //1号栈栈顶指针 }SqStack; //初始化栈 void InitStack(ShStack &S) { S.top0=-1; //初始化栈顶指针 S.top1=MaxSize; } //栈满条件 top0+1 = top1;
//链栈的建立类似于头插法建立链表
typedef struct Linknode
{
ElemType data; //数据域
struct Linknode *next; //指针域
}*LiStack
//即输入()、{}、[]进行匹配 #define MaxSize 10 typedef struct { ElemType data[MAX_SIZE]; int top; }SqStack; bool bracketCheck(char []str,int length) { SqStack S; initStack(S);//初始化栈 for(int i = 0; i <length; i++) { if(str[i] == '(' || str[i] == '{}' || str[i] == '[]') { push(S,str[i]);//入栈 } else { if(isEmptyStack(S)) return false;//栈空,匹配失败 //取栈顶元素进行匹配 char topElem; pop(S,topElem); if(str[i] == ')' && topElem != '(') return false; if(str[i] == ']' && topElem != '[') return false; if(str[i] == '}' && topElem != '{') return false; } } return isEmptyStack(S);//检查是否全部匹配完成(即是否为空栈) }
//中缀、后缀、前缀表达式 1、中缀表达式:运算符在两个表达式之间 如:a+b 2、前缀表达式:运算符在两个表达式前面 如:+ab 3、后缀表达式:运算符在两个表达式后面 如:ab+ /********************************************************************************************* * 利用计算机计算算数表达式,只包含加减乘除四则运算,比如:34+13*9+44- 12/3 * * 思路:通过两个栈来实现的。其中一个保存操作数的栈,另一个是保存运算符的栈。 * 1、我们从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈;当遇到运算符, * 2、就与运算符栈的栈顶元素进行比较。如果比运算符栈顶元素的优先级高,就将当前运算符压入栈; * 3、如果比运算符栈顶元素的优先 级低或者相同,从运算符栈中取栈顶运算符,从操作数栈的栈顶取 2 个操作数, * 4、然后进行 计算,再把计算完的结果压入操作数栈,继续比较。 *********************************************************************************************/ //to be continue
//to be continue
定义:只允许在一端进行插入,另一端进行删除的线性表(类似食堂打饭,ETC)
1、队首、队尾、空队列
2、特点:先入先出(FIFO--First In First Out)
定义与初始化
#define MAX_SIZE (100) typedef struct { ElemType data[MAX_SIZE]; //一般队头指向第一个元素,队尾指向最后一个元素的下一个位置 int front,rear;//队首和队尾指针 }SqQueue; void InitQueue(SqQueue &Q) { //初始时队头、队尾指针指向0 Q.rear=Q.front=0; } //计算队列长度 int length = (rear + MAX_SIZE - front )%MAX_SIZE; //判空(即队首和队位指向同一个元素,牺牲一个元素的空间) bool isEmptyQueue(SqQueue Q) { return (Q.front == Q.rear) ? true : false; }
入队和判满
//队尾入队
//需要牺牲一个空间的元素的,因为队列为空的条件就是队首和队尾指向同一个元素
bool enterQueue(SqQueue &Q,ElemType e)
{
if((Q.rear + 1)% MAX_SIZE == Q.front) return false;//判断队满
Q.data[Q.rear] = e;
Q.rear = (Q.rear + 1)%MAX_SIZE;
return true;
}
出队
//队首出队
//删除一个队首元素
bool deleteQueue(SqQueue &Q,ElemType &e)
{
if(Q.rear == Q.front) return false;//队空
e = Q.data[Q.front];
Q.front = (Q.front + 1)% MAX_SIZE;
return true;
}
获取队头元素值
//和出队相比,不改变队头指针
bool GetHead(SqQueue Q,ElemType &e)
{
if(Q.rear == Q.front) return false; //队空
e = Q.data[Q.front];
return true;
}
不浪费空间的定义方式
//按照以上方式,会造成一个空间的浪费,所以如下方式可进行改进 //方法一初始 #define MaxSize 10 typedef struct { ElemType data[MaxSize]; int front,rear; //初始化时rear=front=0;size=0; //插入成功size++;删除成功size--; //队满条件:size==MaxSize; int size; //队列当前长度 }SqQueue; //方法二 #define MaxSize 10 typedef struct { ElemType data[MaxSize]; int front,rear; //初始化时,rear=front=0;tag=0; //队空条件:front==rear && tag==0 //队满条件:front==rear && tag==1 int tag; }SqQueue;
定义与初始化
//队列的一个节点 typedef struct LinkNode { ElemType data; struct LinkNode *next; } //队列链表 typedef struct { LinkNode *front,*rear; }LinkQueue; //初始化队列 void InitQueue(LinkQueue &Q) { //初始时,front、rear都指向头结点 Q.front = Q.rear = (LinkNode *)malloc(sizeof(LinkNode)); Q.front->next = NULL; } //判空 bool IsEmptyQueue(LinkQueue Q) { if(Q.front==Q.rear) return true; //当队头指针和队尾指针指向同一个元素时,队列为空 else return false; }
入队
void enterQueue(LinkQueue &Q,ElemType e)
{
//实现步骤:
//1、创建一个新节点,尾部指向NULL
//2、节点插入,尾指针后移
LinkNode *s = (LinkNode*)malloc(sizeof(LinkNode));
s->data = s;
s->next = NULL;
Q.rear->next = s;
Q.rear = s;
}
出队
void deleteQueue(LinkQueue &Q,ELemType &e)
{
//实现步骤
//1、删除一般需要释放内存空间,所以先用临时指针指向
//2、删除节点,进行链接,注意判断是不是最后一个节点
LinkLode *p = Q.front->next;
x = p->data;//取出数据
Q.front->next = p->next;
if(p = Q.rear) Q.rear = Q.front;//判断是否为最后一个数据
free(p);
}
//to be continue
1、数组和链表的区别(初级面试必问) 常见回答:链表适合插入、删除,时间复杂度O(1);数组适合查找,查找时间复杂度为O(1) 但是!实际上,这种表述是不准确的。数组是适合查找操作,但是查找的时间复杂度并不为O(1) 即便是排好序的数组,你用二分查找,时间复杂度也是O(logn) 所以,正确的表述应该是,数组支持随机访问,根据下标随机访问的时间复杂度为O(1) 2、什么是数组 数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据 3、数组的内存开辟(可以研究下C++的vector容器,很有代表性) #define MAX_SIZE 栈:ElemType array[MAX_SIZE];//数组类型 数组名 数组大小 堆: //C语言实现 ElemType *p = (ElemType *)malloc(MAX_SIZE * sizeof(ElemType)); free(p);//释放,释放后p置空(p == NULL),因为free释放的是p指向的内存空间 //C++实现 ElemType *p = new ElemType[MAX_SIZE]; delete []p;//数组删除需要delete[] //此外 int *p = new int;//表示开辟大小为1个int型空间,并将地址赋值给p int *p = new int(10);//同上,并把int的值赋为10
树的特点(文件夹、省市图):
①每个节点有零个或多个子节点;
②没有父节点的节点称为根节点;
③每一个非根节点有且只有一个父节点;
④除了根节点外,每个子节点可以分为多个不相交的子树;
基础术语:度,叶子节点,根节点,父节点,子节点,深度,高度。
结点的层次(深度):从上往下数(默认从1开始)
结点的高度:从下往上数
树的高度(深度):总共有多少层
重点:
结点的度:有几个孩子(分支)-- 结点拥有的子树个数称为结点的度
树的度:各结点度的最大值
二叉树是n(n>=0)个结点的有限集合: ①或者为空二叉树,即n=0 ②或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树。 特点:①每个节点至多两棵子树 ②有序树(左右子树不能颠倒) 1 、满二叉树 ①只有最后一层有叶子结点 ②不存在度为1的结点 ③按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1;结点i的父结点为[i/2](如果有的话) 2、完全二叉树(相当于去掉满二叉树的大编号) ①只有最后两层有叶子结点 ②最多只有一个度为1的结点 ③按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1;结点i的父结点为[i/2](如果有的话) ④i<=[n/2]为分支结点,i>n/2为叶子结点 3、二叉排序树(BST) ①左子树上所有结点的关键字都比根节点小 ②右子树上所有结点的关键字都比根节点大 ③左子树和右子树又是一棵二叉树 4、平衡二叉树(AVL树) ①树上任一结点的左子树和右子树深度之差不超过1
//只适合存储完全二叉树,不然会造成大量空间浪费
#define MAX_SIZE (100)
struct TreeNode
{
ElemType value;//结点的数据元素
bool isEmpty;//结点是否为空
}
TreeNode t[MAX_SIZE] ;
//链式存储----n个结点的二叉链表共有n+1个空链域 typedef struct BiTNode { ElemType data; struct BiTNode *lchild,*rchild; /*struct BiTNode *parent; //父结点指针,三叉链表,方便找父节点*/ }BiTNode,*BiTree; //定义一棵空树 BiTree root = NULL; //插入根节点 root = (BiTree)malloc(sizeof(BiTNode)); root->data = {1}; root->lchild = NULL; root->rchild = NULL; //插入新节点 BiTNode *p = (BiTNode*)malloc(sizeof(BiTNode)); p->data = {2}; p->lchild = NULL; p->rchild = NULL; root->lchild = p;
//先序遍历----根左右(NLR) void preOrder(BitTree T) { if(T != NULL) { visit(T);//访问根结点 preOrder(T->lchild);//递归访问左子树 preOrder(T->rchild);//递归访问右子树 } } //中序遍历----左根右(LNR) void inOrder(BitTree T) { if(T != NULL) { inOrder(T->lchild);//递归访问左子树 visit(T);//访问根结点 inOrder(T->rchild);//递归访问右子树 } } //后序遍历----左右根(LRN) void postOrder(BitTree T) { if(T != NULL) { postOrder(T->lchild);//递归访问左子树 postOrder(T->rchild);//递归访问右子树 visit(T);//访问根结点 } } //空间复杂度O(h+1)--O(h)
算法思想: 1、初始化一个辅助队列 2、根结点入队 3、如队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话) 4、重复3直至队列为空 //二叉树的结点(链式存储) typedef struct BiTNode { char data; struct BiTNode *lchild,*rchild; }BiTNode,*BitTree; //链式队列结点 typedef struct LinkNode { BiTNode *data;//ElemType struct LinkNode *next; }LinkNode; typedef struct { LinkNode *front,*rear; }LinkQueue; //层次遍历 void levelOrder(BiTree T) { LinkQueue Q; InitQueue(Q);//初始化辅助队列 enterQueue(Q,T);//根结点入队 while(!isEmptyQueue(Q)) { BiTree p;//保存出队的元素(ElemType &e) deleteQueue(Q,p);//队头结点出队 visit(p); //左右子结点入队,如果存在的话 if(p->lchild != NULL) enterQueue(Q,p->lchild); if(p->rchild != NULL) enterQueue(Q,p->rchild); } }
//如果只给出一个二叉树的 前/中/后/层序遍历中的一种, 不能唯一确定一棵二叉树
//二叉树的结点 typedef struct BiTNode { ElemType data; struct BiTNode *lchild,*rchild; /*struct BiTNode *parent; //父结点指针,三叉链表,方便找父节点*/ }BiTNode,*BiTree; //线索二叉树的结点----保存前驱+后继 typedef struct BiTNode { ElemType data; struct BiTNode *lchild,*rchild; //tag == 0 表示指针指向孩子 //tag == 1 表示指针指向"线索" int ltag,int rtag; //左右线索标记 }BiTNode,*BiTree; //to be continue
//先根遍历:若树非空,先访问根结点,再依次对每棵子树进行先根遍历;(与对应二叉树的先序遍历序列相同) void PreOrder(TreeNode *R) { if(R!=NULL) { visit(R); //访问根节点 while(R还有下一个子树T) PreOrder(T); //先根遍历下一个子树 } } //后根遍历:若树非空,先依次对每棵子树进行后根遍历,最后再返回根节点;(与对应二叉树的中序遍历序列相同) void PostOrder(TreeNode *R) { if(R!=NULL) { while(R还有下一个子树T) PostOrder(T); //后根遍历下一个子树 visit(R); //访问根节点 } } //层次遍历(队列实现) 若树非空,则根结点入队; 若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队; 重复以上操作直至队尾为空;
二叉排序树:又称二叉查找树(BST, Binary Search Tree)
①左子树上所有结点的关键字都比根节点小
②右子树上所有结点的关键字都比根节点大
③左子树和右子树又是一棵二叉树
二叉排序树查找
//二叉排序树结点 typedef sruct BSTNode { int key; struct BSTNode *lchild,*rchild; }BSTNode,*BSTree; //在二叉树查找值为key的结点 //最坏空间复杂度O(1) BSTNode *BST_Search(BSTree T,int key) { while(T != NULL && key != T->key) { if(key < T->key) T = T->lchild; else T= T->rchild; } return T; } //在二叉树查找值为key的结点(递归实现) //最坏空间复杂度O(n) BSTNode *BST_Search(BSTree T,int key) { if(T == NULL) return NULL; if(key == T->key) return T;//查找成功 else if(key < T->key) return BST_Search(T->rchild,key);//左子树中查找 else return BST_Search(T->rchild,key);//右子树中查找 }
二叉排序树插入
//在二叉排序树插入关键字为key的新结点(递归实现) int BST_Insert(BSTree &T,int k) { //原树为空,新插入的结点为根结点 if(T == NULL) { T = (BSTree)malloc(sizeof(BSTNode)); T->key = k; T->lchild = T->rchild = NULL; return 1; } else if(k = T->key) { rerurn 0;//树中存在相同关键字的结点,插入失败 } else if(k < T->key) { return BST_Insert(T->lchild,k);//插到T的左子树 } else if(k > T->key) { return BST_Insert(T->rchild,k);//插到T的右子树 } }
二叉排序树构造、删除
//按照str[]中的关键字序列建立二叉排序树 //str = {10,20,30,44,55,59,23,15,18} void Create_BST(BSTree &T,int str[],int n) { T = NULL;//初始时T为空树 int i = 0; while(i < n) { BST_Insert(T,str[i]); i++; } } //删除 //1、如果结点Z是叶子结点,直接删除 //2、如果结点Z是一棵左子树或者右子树,则让Z的子树称为父结点的子树,替代Z的位置 //3、如果结点Z有一棵左子树&&右子树(可以研究下) //查找效率ASL(Average Search Length) //对比关键字次数不会超过二叉树高度,反映了时间复杂度 //最好情况:n个结点的二叉树最小高度为log2n+1,平均查找长度为O(logn+1) //最坏情况:每个结点只有一个分支,n个结点的二叉树高度=结点数n,平均查找长度为O(n) //所以需要 /*---平衡二叉树!!!----*/
平衡二叉树的产生是为了解决二叉排序树在插入时发生线性排列的现象。由于二叉排序树本身为有序,当插入一个有序程度十分高的序列时,生成的二叉排序树会持续在某个方向的字数上插入数据,导致最终的二叉排序树会退化为链表,从而使得二叉树的查询和插入效率恶化。
平衡二叉树的出现能够解决上述问题,但是在构造平衡二叉树时,却需要采用不同的调整方式,使得二叉树在插入数据后保持平衡。主要的四种调整方式有LL(左旋)、RR(右旋)、LR(先左旋再右旋)、RL(先右旋再左旋)。这里先给大家介绍下简单的单旋转操作,左旋和右旋。LR和RL本质上只是LL和RR的组合。
平衡二叉树(Balanced Binary Tree):简称平衡树(AVL树)--树上任一结点的左子树和右子树高度差不超过1
结点的平衡因子=左子树高-右子树高
在插入一个结点后应该沿搜索路径将路径上的结点平衡因子进行修改,当平衡因子大于1时,就需要进行平衡化处理。从发生不平衡的结点起,沿刚才回溯的路径取直接下两层的结点,如果这三个结点在一条直线上,则采用单旋转进行平衡化,如果这三个结点位于一条折线上,则采用双旋转进行平衡化
时间复杂度:
普通二叉树----树高为h,查找一个关键字最多需要比对h次,即查找的时间复杂度不超过O(h)
平衡二叉树----含有n个结点的平衡二叉树最大深度为O(log2 N),所以平均查找长度为O(log2 N)(n表示结点数)
平衡二叉树(AVL)为了追求高度平衡,需要通过平衡处理使得左右子树的高度差必须小于等于1。高度平衡带来的好处是能够提供更高的搜索效率,其最坏的查找时间复杂度都是O(logN)。但是由于需要维持这份高度平衡,所付出的代价就是当对树种结点进行插入和删除时,需要经过多次旋转实现复衡。这导致AVL的插入和删除效率并不高。
红黑树具有五个特性:
1、每个结点要么是红的要么是黑
2、根结点是黑的
3、每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的
4、如果一个结点是红的,那么它的两个儿子都是黑的
5、对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点
//1、结点的权
//2、结点的带权路径长度:从根节点到该结点的路径长度(经过的边数)与该结点上权值的乘积
//3、树的带权路径长度:树中所有 叶子结点 带权路径长度之和
哈弗曼树:在含有n个带权叶结点的二叉树中,其中带权路径长度(WPL)最小的二叉树,也称最优二叉树
//删除&&插入
//即层序遍历--Breadth First Search //1、找到一个与顶点相邻的所有顶点 //2、标记哪些顶点被访问过 //3、需要一个辅助队列 bool bVisited[MAX_VERTEX_NUM];//访问标记数组 FirstNeighbor(G,x) -----求图G中顶点的第一个邻接点,如果有返回顶点号, -----若X没有邻接点或图中不存在x,则返回-1 NextNeighbor(G,x,y) -----假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点的下一个邻接点的顶点号, -----若y是x的最后一个邻接点,则返回-1 //从顶点v出发,广度遍历图G void BFS(Graph G,int v) { visit(v);//访问初始顶点v bVisited[v] = true;//进行标记 enterQueue(Q,v);//入队 while(isEmptyQueue(Q)) { deleteQueue(Q,v); for(w = FirstNeighbor(G,v); w >= 0;w = NextNeighbor(G,v,w)) { //检测v所有邻接点 if(!bVisited[v]) { visit(w);//访问顶点w bVisited[w] = true;//进行标记 enterQueue(Q,w);//顶点w入队列 } } } } //但是如果是非连通图,则无法遍历完所有顶点 //所以增加一个对标记数组的访问 void BFSTraverse(Graph G) { for(int i = 0; i < G.Vexnum; i++) { bVisited[i] = false;//初始化 } initQueue(Q);//初始化辅助队列 //从0号顶点开始遍历 for(int i = 0; i < G.Vexnum; ++i) { if( !bVisited[i] ) BFS(G,i);//vi没访问过,从vi开始BFS } } //空间复杂度取决于辅助队列 ----- 最坏情况,辅助队列大小为(|V|) //1、领接矩阵----时间复杂度 O(|V|^2) //2、领接表----时间复杂度 O(|V|+|E|)
//深度优先遍历--Depth First Search void DFS(Graph G,int v) { visit(v); bVisited[v] = true; for(w = FirstNeighbor(G,v); w >= 0;w = NextNeighbor(G,v,w)) { if( !bVisited[w] ) DFS(G,w);//递归调用 } } //同理,针对非连通图作一下改进 void DFSTraverse(Graph G) { for(int v = 0; v < G.Vexnum;v++) { bVisited[i] = false;//初始化 } initQueue(Q);//初始化辅助队列 //从0号顶点开始遍历 for(int v = 0; v < G.Vexnum; ++v) { if( !bVisited[v] ) DFS(G,v);//vi没访问过,从vi开始BFS } }
//连通图的生成树是包含图中全部顶点的一个极小连通子图
MST--Minimum Spannine Tree
1、Prim算法(普里姆)--适合于边稠密图
从某个顶点开始构建生成树木,每次将代价最小的新顶点纳入生成树木,直到所有顶点都纳入为止
2、Kruskal算法(克鲁斯卡尔)--适合于边稀疏图
每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的都不选),直到所有顶点连通
单源最短路径
BFS算法(无权图)
Dijkstra算法(带权图、无权图)
每对顶点间的最短路径
Floyd算法(带权图、无权图)
1、BFS算法
//适合不带权的图 时间复杂度:O(n^2) //广度优先算法 bool bVisited[MAX_VERTEX_NUM];//访问标记数组 //从顶点v出发,广度遍历图G void BFS(Graph G,int u) { for(int i = 0;i < G.vexnum;i++) { d[i] = ∞;//初始化路径长度 path[i] = -1;//最短路径从哪个顶点过来 } d[u] = 0; visit(u);//访问初始顶点u bVisited[u] = true;//进行标记 enterQueue(Q,u);//入队 while(isEmptyQueue(Q)) { deleteQueue(Q,u); for(w = FirstNeighbor(G,u); w >= 0;w = NextNeighbor(G,u,w)) { //检测v所有邻接点 if(!bVisited[u]) { d[w] = d[u] + 1; path[w] = u; bVisited[w] = true;//进行标记 enterQueue(Q,w);//顶点w入队列 } } } }
2、Dijkstra算法–迪杰斯特拉
//会手动测算即可---不适合带负权值的图
时间复杂度:O(n^2)
3、Floyd算法
//使用动态规划思想,将问题的求解分为多个阶段---不适合带负权值的图 //......准备工作,根据图的信息初始化矩阵A和path 空间复杂度:O(n^2) 时间复杂度:O(n^3) for(int k = 0; k < n; k++) { //考虑以Vk作为中转点,遍历矩阵,i,j分别为行号、列号 for(int i = 0; i < n; i++) { for(int j = 0; j < n; j++) { //是否以Vk作为中转点路径更短 if(A[i][j] > A[i][k] + A[k][j]) { A[i][j] = A[i][k] + A[k][j];//更新最短路径 path[i][j] = k;//更新中转点 } } } }
4、拓扑排序
bool TopologicalSort(Graph G) { initStack(s);//初始化栈,存放度为0的顶点 for(int i = 0; i < G.vexnum; i++) { if(indegree[i] == 0) push(S,i);//将所有入度为0的顶点进栈 int count = 0;//计数,记录当前已经输出的顶点数 while(isEmpty(S)) { pop(S,i); print(count++) = i;//输出顶点i for(p = G.vertices[i].firstarc; p; p= p->nextarc) { //将所有i指向的顶点的入度减1,并且将入度减为0的顶点压入栈S v = p->adjvex; if(! (--indegree[v]) ) push(S,v);//入度为0,则入栈 } } //如;排序失败---有向图中有回路 return (count < G.vexnum) ? false : true; } } 时间复杂度:O(|V|+|E|) 采用领接矩阵:O(V^2)
***ASL(Average Search Length)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。