数据结构学习笔记

目录

C语言基础

大神的c语言笔记
i++与++i的差别
数据结构笔记

程序设计结构

在这里插入图片描述

//循环
	//while循环
	while(测试条件){
	循环行为1;
	循环行为2;
	……
	}
	
	int main(){
		int i=1;sum=0
		while(i<=100){
			sum = sum + i;
			i++;
		}
		printf("%d %d\n",i,sum);
		return 0;
	}

	
	//do while 
	do{
		循环行为1;
		循环行为2;
		……
	}while(测试条件);

	// 输入一个值,知道输入的是正数才输出结果
	int main{
	    int x ;
	    do 
	    {
	        scanf("%d",&x);
	    }while(x<0);
	    printf("%d\n",x);
	    return 0;
	}


	//for循环
	for(计数器设置初始值;循环条件l计数器更新){
	循环行为1;
	循环行为2;
	……
	}
	//v1.0 i是全局变量
	int main(){
		int i ,sum = 0 ;
	    for ( i = 1 ; i <= 100 ; i++){
	        sum = i + sum ;
	    }
	    printf("%d %d\n ",i,sum);
	    return 0;
	}
    //v1.1 i是局部变量
    int main(){
	    int sum = 0 ;
	    for ( int i = 1 ; i <= 100 ; i++){  //i非全局声明,只在循环里声明
	        sum = i + sum ;
	    }
	    printf("%d ",sum); //输出的时候就没法输出i,只能输出sum
	    return 0;
	}
		

//分支
	//if条件语句
	if(测试条件)
		条件为真的流程
 	int main(){
		int x;
		scanf("%d",n);
		if(n>=3 &&x<=10)
			print("yes\n");
		return 0;
	}

	if(测试条件)
		条件为真所执行的流程;
	else 
		条件为假所执行的流程;
	
	int main(){
		int x;
		scanf("%d",n);
		if(n>=3 && x<=10)
			print("yes\n");
		else 
			print("no\n");
		return 0;
	}

	测试条件 ?表达式1 : 表达式2
	c= x>=10 ? ‘yes’:'no'

	//多重选择语句switch case default
	int main(){
	    char letter; 
	    scanf("%c",&letter);
	    switch(letter)
	    {
	        case 'a':
	            printf("aaa");
	            break;
	        case 'b':
	            printf("bbb");
	            break;  //每个case语句后加break的原因是因为当匹配到对应的值,输出后,就结束了,除非如果需要持续输出后面所有的值,不然都要加break来中断循环
	        case 'c':
	            printf("ccc");
	            break;
	        case 'd':
	            printf("ddd");
	            break;
	        case 'e':
	            printf("eee");
	            break;
	        case 'f':
	            printf("fff");
	            break;
	        default:
            	printf("bad");
    	}
	    getchar(); //清楚未被吸收的输入
	    return 0;
    }

普通变量和指针变量引用的差异

普通变量指针变量
L.elem ; // L的元素L->elem ; // 指针L指向元素的元素
L.length ; // L的表长L->length ; // 指针L指向元素的表长

绪论

线性逻辑结构

线性表

基本概念

定义

  • 具有相同特征的数据元素的一个有限序列
  • 由n个数据元素 a1,a2,…,an组成的有限序列
    • n为数组长度
    • 当n=0是空表
    • 非空的表记作 (a1,a2,a3,……,an)
    • an是只是一个抽象符号,具体根据实际情况看

逻辑特征

  • 有且仅有一个开始结点a1,没有直接前趋,只有唯一后继a2
  • 有且仅有一个终端结点an,没有直接后继,只有唯一前趋an-1
  • 中间结点ai都有且只有一个直接前趋和直接后继
//线性表的抽象数据类型定义
ADT List {
	数据对象:
		D={ai | ai∈ElemSet,i=1,2,3,,n,n>=0}
	数据关系:
		R={ <ai-1,ai> | ai-1, ai ∈D,i=2,3,,n}
	基本操作:初始化,进栈,出栈,取栈顶元素等
}ADT List

基本类型
	初始化线性表
		InitList(&L)
	销毁线性表
		DestroyList(&L)
	清除线性表
		ClearList(&L) //重置清零,表为空表
	判断是否空表
		ListEmpty(L)
	判断表的长度n
		ListLength(L)
复杂类型
	查
		获取线性表中第i个元素值,并用e返回
			GetElem(L,i,&e)
		查找给定元素的前趋,并用pre_e返回前趋值
			PriorElem(L,cur_e,&pre_e)
		查找给定元素的后继,并用next_e返回前趋值
			NextElem(L,cur_e,&next_e)
		返回L中与e满足compare()的数据元素的位序,若不存在则返回0
			LocateElem(L,e,compare())
		遍历线性表中的元素,依次对线性表中每个元素调用visited()
			ListTraverse(&L,visited())
	增
		在线性表的指定位置i前插入一个元素e
			ElemInsert(&L,i,e)
	删
		删除线性表的指定位置i的元素,并用e返回其值
			ListDelete(&L,i,&e)
顺序表示及实现

在这里插入图片描述

  • 顺序表示:线性表的顺序表示又称为顺序存储结构或者顺序映像。
  • 顺序存储:把逻辑相邻元素存储在物理相邻存储单元中的存储结构
  • 首地址:线性表的第一个元素a1的存储位置,称为线性表的存储位置或者基地址。
  • 计算存储位置的函数:loc(ai)=loc(a1)+(i-1)*l,l是一个元素所占字节长度,loc(a1) 为基地址,该函数时间复杂度为o(1)
//定义线性表的顺序表示的结点的数据类型
	//方式一
	#define MAXSIZE 100
	typedef struct{
		ElemType elem[MAXSIZE];  //表元素,以数组形式存储的
		int length; //表长
	}Sqlist; // 定义顺序表类型 
	
	//方式二
	typedef struct{
		ElemType *elem;
		int length; 
	}Sqlist; // 顺序表类型
	L.elme = (ElemType*)malloc(sizeof(ElemType)*[MAXSIZE])

//定义变量
	SqList L; //定义一个表L,是一个类型为Sqlist的线性表,即一个顺序表示的线性表
	L.elem ; // L的元素
	L.length ; // L的表长
	L.elem[i] ; // L的一个元素的第i个值
// 5种简单运算

// 顺序表示的线性表的初始化
	status InitList_Sq(SqList  &L){  // 构造一个空的顺序表L
		L.elem = new ElemType[MAXSIZE] ; // 为顺序表分配空间
		if(!L.elem) exit(overflow);//存储分配失败
		L.length=0; //新表表长为0
		return OK;
	}
// 判断线性表是否为空
	int IsEmpty(Sqlist L){
		if(L.length==0) 
			return 1; 
		else 
			return 0;
	}
// 销毁线性表L
	void DestoryList(Sqlist &L){
		if(L.elem)  // L.elem=NULL 可以简写为L.elem
			delete L.elem; //释放储存空间
	}

// 清空线性表L
	void ClearList(Sqlist &L){
		L.length=0; //将长度置为0
	}

// 求线性表L长度
	int GetLength(Sqlist L){
		return(L.length); 
	}

-逻辑非运算,如果有内容,则假了,如果没有,则为真。所以非指的就是非空?

// 3种复杂运算

// 查 
	// 按序查找:根据给定位置i,查找相应位置的数据元素内容
	int GetElem(SqList L,int i,Elemtype &e){
		if(i<1 || i>L.length)return error; //判断值是否合理,不合理返回error
		e = L.elem[i-1]; //第i-1个单元存储着第i个元素
		return ok; 
	}
	// 按值查找:根据给定值,查找该值是否在表中,不存在返回0,存在则返回对应的位置
	int LocateElem(SqList L,Elemtype e){
		for(i=0,i<L.length,i++) //依次扫描
			if(L.elem[i]==Elemtype e) //扫描后的值跟e比对
			return i+1; // 成功,进入循环,返回序号
		return 0;
	}
// 增
	Status ListInsert_Sq(SqList &L,Elemtype e){
		if(i<1 || i>L.length+1)return error; //判断值是否合理,不合理返回error
		if(L.length == MAXSIZE)return error; //当前储存空间已满
		if(j=L.length-1,j>i-1,j-- ) // 腾位置依次往后挪 ,不能从头开始往后挪,这样后面的值会丢掉,应该从最后开始,反向挪
			L.elem[j+1] = L.elem[j];
		L.elem[i-1] = e ; // 赋值 
		//表长+1
		L.length = L.length + 1; //也可以写 L.length++
		return 0; 
	}
	// 时间复杂度o(n) = 数学期望 = 移动次数 * 出现概率 (((1+n)*n)/2)*1/(n+1) ~ n/2

// 删
	Status ListDelete_Sq(SqList &L,Elemtype e){
		if(i<1 || i>L.length+1)return error; //判断值是否合理,不合理返回error
		if(L.length == 0)return error; //当前储存空间已空
		e = L.elem[i-1] = e ; // 把i的位置元素存好
		if(j=i,j<L.length-1,j++ ) 
		// 腾位置依次往前挪 ,从头开始往后挪,直接覆盖i-1位置(i元素存储的地方)
			L.elem[j-1] = L.elem[j];
		//表长+1
		L.length = L.length - 1; //也可以写 L.length--
		return 0; 
	}
	// 时间复杂度o(n) = 数学期望 = 移动次数 * 出现概率 (((0+n-1)*n)/2)*1/n ~ n-1/2

链式表示及实现
基本特征

在这里插入图片描述

优点:

  • 灵活性:结点空间可以通过动态申请和释放
  • 时间复杂度小:数据元素的逻辑次序依靠结点的指针来指示,插入和删除元素不需要移动元素

缺点:

  • 空间复杂度大?空间浪费:存储密度小,每个结点的指针域需要额外增加储存空间。当每个结点的数据域所占字节不多时,指针域所占储存空间的比重显得很大。。。。有点get不到,显得很大,影响了什么呢?显得感觉就很虚
  • 算法负责度增加:非随机存储,对任意结点的修改,都要从头结点开始依次寻找到该结点,算法复杂度增加
单链表
  • 定义:有指示后继结点的指针域
  • 优点:有指示后继结点的指针域,查找某结点的后继结点很方便,执行时间为o(1)
  • 缺点:无指示前驱结点的指针域,找某个前驱结点难,从表头出发查找,执行时间为o(n)

基本定义

// 结点抽象数据类型定义
	typedef struct Lnode{
		Elemtype data;
		struct Lnode *next;
	} Lnode,*linkList;

//变量定义
	LinkList L; // 定义一个链表L
	Lnode *p; //定义一个链表的结点p

//重要操作
	p = L; // p指向头指针
	p = L->next; // p指向首元指针
	p = p->next; // p指向下一个结点

// 举例:存储学生学号,姓名,分数的单链表的结点类型定义
	// 方式一
	typedef struct student{
		char num[8];
		char name[8];
		int score;
		struct student*next;
	} Lnode,*LinkList;
	// 方式二,为保持单链表定义的统一性,一般用方式二来做结点类型定义
	typedef struct {
		char num;
		char name;
		int score;
	}Elemtype;
	typedef struct Lnode{
		Elemtype data;
		struct Lnode *next;
	} Lnode,*Linkist;

5个基础的操作实现

//5个简单操作

//表初始化
	status IntiList_L(LinkList &L){
		L = new Londe ; // 创建一个头结点,并赋值给头指针L
		L->next = NULL; // 把头指针指向的头结点的next域 赋值 为空
		return ok;
	}

//判空
	// 思路:判断头结点的指针域是否为空
	int ListEmpty(LinkList L){//若为空表返回1,否返回0
		if(L->next)  //头结点非空,则进入循环,返回1
			return 0 ;
		else //空,则进入else
			return 1;
	}

//销毁
	// 思路:从头指针开始,依次释放所有结点
	// p=L // 把L的赋值给P,那么p就会指向头结点,然后删掉p,就可以把头结点删掉了,
	// L=L->next 但头结点指针域存了首元结点的指针域,所以需要先把头结点的指针域copy出来后再删
	// free(p) //就会把头结点删掉
	// 循环条件 L!=NULL,L ,循环停止 L==NULL
	status DestroyList_L(LinkList &L){  //销毁单链表L
		Lnode *p; //定一个新结点
		while(L){ //L非空,则一个结点一个结点的移动,直到为空
			p = L;//p指向头结点
			L = L->next; // L指向下一个结点
			delete p; //删掉p当前指向的结点
		}
		return ok;
	}

//清空链表
	//思路:从首元结点开始,依次释放所有结点,并将头指针的指针域置空
	// p=L->next 把L的地址域赋值给p,那么p指向首元结点的指针,然后删掉p,就可以把首元结点删掉
	// q=p->next 但首元结点指针域存者下一结点的指针域,所以需要先把下一结点的指针域copy出来后再删
	// free(p) //就会把首元结点删掉
	// p=q ; //p被重新赋值,指向q
	// q=q->next // q被重新赋值,指向下一结点
	// free(p) //就会把第二个结点删掉
	// 循环停止 p==NULL  //不能设置q为null,这样最后一个结点就不会被删,必须p为null
	// 循环条件 p!=NULL,L 
	// L->next = NULL

	// 方式二
	status ClearList_L(LinkList &L) { 
		Lnode *p,*q; 
		p=L->next;  //p指向首元结点
		while(p){ //只要p没有指向空节点,就一直继续删掉p指向的结点
			q=p->next; // q指向当前p的地址域,也就是下一个结点的位置
			free(p); //把p当前指向的结点删掉,也就是从首元结点开始删
			p = q; //把p往后挪一个结点
		}
		L->next = NULL; //头指针置空
		return ok;
	}
	// 方式一 
	status ClearList_L(LinkList &L){  //
		Lnode *p,*q; 
		p=L->next;  //p指向首元结点
		q=p->next; // 把首元结点里存的下一个结点的地址域copy出来
		free(p); //就会把首元结点删
		if(q){ // q非空则一直执行
			p = q; //把当前结点赋值给p
			q = q->next;// 然后q移动,指向下一个指针
			free(p); //把当前的p又删掉
		}
		else 
			i = 0 
		L->next = NULL;
		return ok;
	}


//求单链表的表长
	// 思路:从首元结点开始,依次计数所有结点
	// p=L->next  // 把头结点指向的地址赋值给p,那么p就会指向首元结点的指针
	// p=p->next //把首元结点指针域中下一结点的指针域,所以需要先把下一结点的指针域copy出来后再删
	// i++ //然后计数+1
	// 特殊情况,非空表
	// 循环停止 p==NULL
	// 循环条件 p!=NULL,L
	
	int ListLength_L(LinkList L) { //求表长
		Lnode *p;
		p=L->next;  //p指向首元结点(0->1)
		int i=0; //初始个数为0
		where(p){ //首元非空,若一直非空,则循环计次
			i++; //判断一次非空,则计一次 ,每次计数,用的是前趋结点的指针计次
			p = p->next;//指向下一个结点,直至指向最后一个结点为空.(1->2)……(n-1->n),一共n次
		}
		printf("%\d",i);
		return i;
	} 

在这里插入图片描述

6个复杂操作的实现

取值:取第i个元素值,并赋值给e

//算法思路:从第一个结点顺序扫描,用指针p指向当前扫描结点

// 方式一 
	Status GetElem_L(LinkList L,int i,Elemtype &e){  //取第i个元素
		Lnode *p;
		p=L->next;  //p指向首元结点
		int j=1;  //此时扫描完1次,j记一次
		where(i>0){
			where(j<i && p!=NULL){ // 也可以 j<i && p
			p = p->next; //p指向下一个指针
			j++; // 扫描加一次 ++j
			}
		e= p->date;
		}
		return false;
	}

// 方式二
	Status GetElem_L(LinkList L,int i,Elemtype &e){  //取第i个元素
		Lnode *p;
		p=L->next;  //p指向首元结点
		int j=1;  //此时扫描完1次,j记一次
		where(j<i && p){ // 向后扫描,直至p指向的第i个元素(用的是第i-1个结点的指针,因此j不能等于i只能小于i)或者p为空
			p = p->next; //p指向下一个指针
			j++; // 扫描加一次 ++j
			}
		if(!p || j>i) //第i个元素不存在
			return error;
		e= p->date; //取第i个元素的值
		return ok;
	}

查找:按值查找,并给出值对应的地址

Lnode *LocateElem_L(LinkList L ,Elemtype e ) { //按值查找,获取地址

	Lnode *p;
	p=L->next;  //p指向首元结点
	int j=1; //扫描完1次,加1
	where(p->date != e && p!=NULL){ // 也可以 p->date != e && p
	
		p = p->next; //往后移动一个结点
		j++; //扫描第二次,加1
	}
	return p; // 返回地址
	if(p) return j; // 返回元素序号
	else return 0 ; 
}

插入:在第i个结点前插入一个值e

// 思路:
// 1,首先找到ai-1元素的储存位置p
// 2,新建结点s,把e赋值给s的数据域
// 3,插入新结点,把新s的指针域指向ai(s->next = p->next ),ai-1指向s ( p->next = s )

status ListInsert_L(LinkList &L ,int i,elemtype e ) { //在第i个元素之前插入e元素

	Lnode *p;
	p=L;  //p指向头结点,从头结点开始,是因为i可能等于1,那么如果直接从首元结点开始就没法实现了。
	int j=0;  //p指向的是头结点,不算一个元素,故j令为0
	where(j<i-1 && p){p = p->next;j++;} // 寻找第i-1个结点,p指向i-1个结点
	if(!p || j>i-1) return error;//i大于表长+1或者小于1,则非法
	s = new lnode;  //生成一个新结点
	s->date = e; //把元素e赋值到新结点的数据域
	s->next = p->next; // s指向第i个元素的地址域,把s插入到i前,把后面的结点串联起来了
	p->next = s; // 第i-1个结点的地址域指向s,把前面的结点串联起来了
	return ok ;
}

在这里插入图片描述

删除:删除第i个结点

// 思路:首先找到第i-1个结点的储存位置,保存要删除的ai的值
// 然后把ai的地址域 赋值给ai-1,那么ai-1就会指向ai+1了
// 释放结点ai的空间

status listInsert_L(LinkList L ,int i,elemtype e ) { //

	Lnode *p,*q;
	int j=0; 
	p=L;  //p指向头结点
	where(j<i-1 && p->next){p = p->next;j++;} // 寻找第i个结点,并令p指向其前趋i-1
	if(!(p->next) || j>i-1) return error;//i大于表长+1或者小于1,则非法
	q = p->next ; //临时保存被删结点的地址,以备释放
	p->next = p->next ->next ; // = q-> next 改变删除结点的前趋结点的地址域,第i-1个结点的地址域指向第i+1个结点,串联起来了,后继结点的后继结点p->next ->next 
	e = q->date ; // 保存删除结点的数据域
	free(q) // 释放删除结点的空间 
	return ok ;
}

建立:头插法

//从一个空表开始,重复读入数据
//生成一个新结点,将读入数据存放到新结点的数据域中
//从最后一个结点开始,依次将各结点,插入到链表的前端
// 建立链表L(A,B,C,D,E)
void CreateList_H(LinkList &L,int n){
	L = new Lnode; 
	L->next = NULL; // 创建一个头结点,此时L的指向空的头结点
	for(i=n,i>0,--i){
		p = new Lnode; //生成一个新结点
		cin>>p->data;//c语言语法,输入元素值scanf(&p->data); // 算法逻辑p->data = a[i];
		// 插入到表头
		p->next = L->next; // 把头结点的空挪到p的地址域
		L->next = p; // 让头结点指向p,两个结点连起来了
	}
}

// 时间复杂度,就是for循环语句,每个都会执行n次,算法复杂度O(n)

建立:尾插法

//从一个空表开始,将新结点逐个插入到链表尾部,尾指针r指向链表尾结点
//初始时,r和L都指向头结点,每读入一个元素,申请一个新结点,将新结点插入尾部后,r指向新结点
// 建立链表L(A,B,C,D,E)
void CreateList_R(LinkList &L,int n){
	L = new Lnode; 
	L->next = NULL; // 创建一个头结点,此时L的指向空的头结点
	
	r = new Lnode; //生成一个新结点 ,全程相当于一个中间变量
	r = L; // 把尾指针r指向头结点
	
	for(i=0,i<n,++i){
		p = new Lnode; //生成一个新结点
		cin>>p->data;//c语言语法,输入元素值scanf(&p->data); // 算法逻辑p->data = a[i];
		//L->next = p->next; // 让头指针指向新结点 ,不能这样,这样又变成头插了。
		p->next = NULL;
		// 接入到尾巴
		r->next = p; // r指向把新结点
		r=p; //r移动到新结点的位置
	}
}

// 时间复杂度,就是for循环语句,每个都会执行n次,算法复杂度O(n)
循环链表

定义:是一种头尾相接的链表(既最后一个结点的指针域指向头结点,整个链表形成一个环) 优点:从表中任一结点出发都能找到表中其他结点
注意:循环链表没有null指针,故涉及遍历操作时,终止条件就不像是再像非循环链表那样判断p 或者p->next 是否为空,而是判断它们是否是头指针

p != NULL ===>>> p!=L
p->next != NULL ===>>> p->next != L

在这里插入图片描述

在这里插入图片描述

//带尾指针的循环链表合并
// 算法思路:p存表头结点,先把tb连接到ta表尾,释放tb的表头结点,修改指针(把tb的表尾指向ta的表头)
LinkList Connect(LinkList ta,LinkList tb){
	p = new Lnode;
	// 假设ta,tb都是非空的单循环链表,---这个没讲语法细节
	p = ta->next ; //先p指向把ta的头结点
	ta->next = tb->next->next ; //然后再修改ta尾结点,指向tb的首元结点
	delete tb->next ; //删掉tb的头结点,因为tb是尾指针,所以,头指针的tb->next
	tb->next = p  // tb指向p,也就是ta的头结点
	return tb
}

// 时间复杂度,每个都执行1次,算法复杂度O(1)
双链表

定义:在单链表的每个结点里再增加一个指向前驱结点指针域prior,这样链表就形成了有两个不同 方向的链,故成为双向链表

在这里插入图片描述

双向链表的结构定义

typedef struct DulNode{
	Elemtype date;
	struct DulNode *prior;*next;
}DulNode,*DulLinkList;

在这里插入图片描述
双向链表具有对称性

p->next->prior = p = p->prior->next

双向链表的有些操作跟单链表完全一致,但在插入和删除时,需要同时修改两个方向的指针,操作复杂度均为o(n)。
区分在于,是只需要用到一个指针还是两个指针
在这里插入图片描述

双向链表的插入操作
在这里插入图片描述

// 在b结点前之前,插入一个s结点

void ListInsert_DuL(DuLinkList &L,int i ,Elemtype e){
// 在带头结点的双向循环列表L的第i个位置元素b之前插入元素e
	Lnode *p,*s; //定义变量
	if(!(p=GetElemP_Dul(L,i)) return error;//…… 查找语句,p定位到an;
	s = new DuLnode; //新建双向链结点
	s->data = e;  //把e赋值给s的数据域
	// 老师的版本,全程没有用到a和b,只是利用之间变量p
	// 改的是哪个位置,哪个位置就在等式左边,右边则是希望左边是什么值的所实现的代码!!!赋值运算的新一层次的理解
	// 链接上a 
	s->prior = p->prior ; //把p也就是b的前趋赋值给s的前趋,那么前趋指针链链上了a
	p->prior->next = s ; // 把p的前趋指针指向元素a的后继指针赋值给s,那么a的后继就指向s,那么后继指针链接了s;???暂时不明白为什么不能直接a->next?而是要p->prior 找到a的位置,然后用a->next域 ,即p->prior->next
	// 链接上b
	s->next = p ; //s的后继指向p,即指向a,那么后驱指针链上了b
	p->prior = s ;//b的前趋指向s,那么b的前趋指针链上了s
	return ok;


//我的版本,错误百出,我也不知道怎么理解了……先搁置,等后续学完再来反思。
// 把a的后继指针指向s,s的后继指向p,则把后继指针连上了;
// 把p的前趋指针指向s(把s前趋与a连接上),b的前趋指向s(把b前趋与s连接上),则把前趋指针串上了
	a->next = s ; //a后驱指针指向s
	s-next = p->next ; //【s-next = p->next】s后驱指向p的后驱这个肯定错了,p的后驱是b+1了,应该是s-next = p
	s->prior = p->prior ; //s前趋指向p
	s->prior = b->prior ; //【s->prior = b->prior】s前趋指向b的前趋,这个倒是没错

}

// 时间复杂度

双向链表的删除操作
在这里插入图片描述

// 删除b结点

void ListDelete_DuL(DuLink &L,int i ,Elemtype &e){
	// 把带头结点的双向循环列表的第i个位置元素删除
	Lnode *p; //定义变量
	if(!(p=GetElemP_Dul(L,i)) return error;//…… 查找语句,p定位到ai;
	e = p->data ;  //把p也就是i元素的值赋值给e
	// 改的是哪个位置,哪个位置就在等式左边,右边则是希望左边是什么值的所实现的代码!!!赋值运算的新一层次的理解
	p->prior->next = p->next ; // 把p的前趋指针指向元素a的后继指针赋值给p(也就是i)的后继指针,那么a的后继就指i+1;
	p->next->prior = p->prior ; // 把i+1的前趋指针指向p的前趋也就是a
	free(p);
	return ok;
}
// 时间复杂度,如果知道i的位置,不需要去找,那么每个语句执行一次,时间复杂度为o(1);如果需要去找i,那么执行循环语句,时间复杂度为o(n),整个算法的时间复杂度增加为o(n)

以下表均带头结点

表逻辑类型指针类型找首元结点找尾结点找*p的前驱结点找*p的后继结点
单链表L头指针L->next o(1)L->next 依次遍历 o(n)通过 p>next 无法直接找前驱p->next o(1)
循环链表L头指针L->next o(1)L->next 依次遍历 o(n)p>next 依次遍历 o(n)p->next o(1)
循环链表R尾指针R->next-next o(1)R o(1)p->next 依次遍历 o(n)p->next o(1)
双向链L头指针L->next o(1)L->prior o(1)p->prior o(1)p->next o(1)

顺序表和链表的比较

比较项目存储结构顺序表链表
空间储存空间预先分配,会导致空间溢出或者闲置动态分配,不会有空间溢出和闲置
空间存储密度不用为结点间的逻辑关系增加额外开销,=1需要借助指针来表示结点间的逻辑关系,增加额外开销,<1
时间存取元素随机存取,按位置访问元素,o(1)顺序存取,依次访问元素直至找到,o(n)
时间插入删除平均移动表约一半的元素,o(n)不移动表中元素,确定插入or删除位置后,o(1)
适用情况表长变化不大,且能事先确定变化范围,很少进行插入和删除操作,经常通过位置访问元素频繁变化表长,经常增删

栈和队列

是两种重要,常见的线性表
是限定插入和删除只能在端点进行的线性表

栈和队列是线性表的子集,是一种插入和删除受限定的一个线性表类型

操作普通表队列
插入insert(L,i,x)1<i<n+1insert(S,n+1,x)insert(Q,n+1,x)
删除delete(L,i)1<i<ndelete(S,n)delete(Q,1)
总结任意位置后进先出先进先出
栈和队列的定义及特点

  • stack:是一种特殊的线性表,限定仅在一端(通常是表尾)进行插入和删除的线性表
  • LIFO:又称 后进先出(Last in first out)的线性表 ,简称LIFO结构
  • 栈顶&栈底:表尾an用栈顶来表示(top),表头ai用栈底来表示(base)
  • 入栈:插入元素到栈顶的操作称为入栈,PUSH(x);=进栈,压栈
  • 出栈:从栈顶删除最后一个元素的操作,POP(y)
  • 顺序栈:存储结构跟线性表一样,顺序和链式都可,比较常见的为顺序栈
  • 实现方式:重点是出栈和入栈操作

队列

  • queue:是一种特殊的线性表,仅在一端插入(表尾),另一端(表头)删除的线性表
  • FIFO:又称 先进先出(frist in first out)的线性表 ,简称FIFO结构
  • 队首&队尾:表尾an用队尾来表示(top),表头a1用队首来表示(base)
  • 入队:插入元素到队尾的操作称为入队
  • 出队:从队头删除第一个元素的操作称为出队
  • 循环顺序队列:存储结构跟线性表一样,顺序和链式都可,比较常见的为循环顺序队列
  • 实现方式:重点是出队和入队操作
栈的表出与实现
//栈的抽象数据类型定义
ADT stack {
数据对象:
	D={ai | ai∈ElemSet,i=1,2,3,,n,n>=0}
数据关系:
	R1={ <ai-1,ai> | ai-1, ai ∈D,i=2,3,,n}
	约定an为栈顶,a1为栈底
基本操作:初始化,进栈,出栈,取栈顶元素等
}ADT stack
栈的顺序表出
//顺序栈的表示
#define MAXSIZE 100
typedef struct{
	SElemType *top; // 栈顶指针
	SElemType *base; //栈底指针
	int stacksize;  //最大容量
	// 定义了头,定义了尾,定义了长度,就是一个数组了,就不用再额外定义一个数组了
}SqStack;

//附设top指针,指向栈顶,但实际中,为了方便操作,top是指向栈顶后的那个元素,以便进栈和出栈操作
//附设base指针,指向栈底
//stacksize 表示栈的最大容量,最多元素个数
//栈空 top == base
//栈满 top - base == stacksize 

使用数组作为顺序栈存储方式的特点

  • 简单,方便,但容易溢出(数组大小是固定的)
    • 上溢:栈已满,还要压入元素 —这个看做一种错误,使问题无法再继续进行
    • 下溢:栈已空,还要弹出元素 —这个看做一个约束条件,表示问题处理结束
//初始化
	Status InitStack(SqStack &S){
		S.base = new SElemtype[MAXSIZE];  //申请一个空间存数组
		if(!S.base) exit(overflow); //分配空间失败
		S.top = S.base;
		S.stacksize = MAXSIZE;
		return ok;
	}
//判栈空 
	Status StackEmpty(SqStack S){
		if(top == base)
			return 1 ;
		else 
			return 0;
	}
//清空栈
	Status ClearStack(SqStack S){
		if(S.top) 
		S.top = S.base;
		return ok;
	}
//销毁
	Status ClearStack(SqStack S){
		if(S.base){
			detele S.base;
		S.stacksize = 0
		S.top = S.base = NULL;
		}
		return ok;
	}
//求栈的长度
	Status StackLength(SqStack S){
			return top - base;
	}

// 入栈:先把元素值赋值给栈顶,然后挪栈顶位置往后挪一位
	Status 	Push(SqStack &S,SElemtype e  ){
		if(S.top-S.base == stacksize) //判断是否满栈
			return error;
		*S.top++ = e ;  // 等价于 *S.top =e; *S.top++;
		return ok;
	}

// 出栈:先挪栈顶位置往前挪一位,然后把需要删除的那个元素的值赋值给e
	Status 	Pop(SqStack &S,SElemtype e  ){
		if(S.top==S.base ) //判断是否空栈
			return error;
		e = *--S.top  ;  // 等价于  --S.top;e= *S.top ;
		return ok;
	}
栈的链式表出

在这里插入图片描述

//链栈的表示
//链栈是运算受限的单链表,只能在链表头部进行操作

typedef struct StackNode{
	SElemType date; // 数据域
	struct StackNode *next //指针域,头指针就是栈顶指针,next域指向栈底方向,也就是前趋元素
}StackNode,*LinkStack;

LinkStack S; //定义一个链栈表S

  • 链表的头指针S就是栈顶top
  • 不需要头结点
  • 基本不存在满栈的情况
  • 空栈则是头指针指向NULL
  • 插入和删除都只在栈顶执行

//初始化
	viod InitStack(LinkStack &S){
		S NULL;
		return ok;
	}

//判栈空 
	Status StackEmpty(LinkStack S){
		if(S == NULL)
			return 1 ;
		else 
			return 0;
	}

//清空栈
	Status ClearStack(LinkStack S){
		if(S) 
		S == NULL;
		return ok;
	}

//获取栈顶元素
	Status 	GetTop(LinkStack S){
		if(S != NULL)
			return S->date;
	}

// 入栈:先把元素值赋值给栈顶,然后挪栈顶位置往后挪一位
	Status 	Push(LinkStack &S,SElemtype e  ){
		if(S == NULL)
			return error ;
		p = new StackNode;
		p->date = e;  //把e插入数据域
		p->next = S;  //把地址域指向S
		S=p; //头指针指向新栈顶p
		return ok;
	}

// 出栈:先挪栈顶位置往前挪一位,然后把需要删除的那个元素的值赋值给e
	Status 	Pop(LinkStack &S,SElemtype e  ){
		p = new StackNode;
		e=S->date ;  //把S的数据域存到e里
		p=S; //把p指向S
		S=S->next;  //把S往下挪一位,n-1成为栈顶
		drop(p); //删掉原来的栈顶
		return ok;
	}
	
王道的栈的课后算法题答案
//假设I和O分别表示出栈和入栈操作。栈的初始和终态均为空。
//入栈和出栈可表示仅由I和O组成的序列,可以操作的序列成为合法序列,不可以操作的称为不合法序列。
//请用算法判定一个已知序列是否合法,若合法则返回true,若不合法则返回false。
//(假定被判断的序列已经存入一维数组中)

//算法思想
//依次遍历数组,并计算当前遍历时的入栈和出栈次数,如果中途发现出栈次数大于入栈,则非法。依次遍历至最后,发现入栈不等于出栈,也非法。

bool Judge(char A[]){
	int i=0;
	int j=k=0; //j和k分别为字母I和O的个数
	//先依次遍历
	while(A[i]!='\0'){
		//在每次遍历循环里,计入栈和出栈的次数
		switch(A[i]){
			case 'I' :j++;break;
			case 'O' :k++;
			//在每次计次后,判断出栈次数的合理性
			if(k>j)
				{print("序列非法\n");return false;}
		}
		i++; //往后依次遍历
	}
	//遍历完后判断终态是否为空
	if(j!=k)
		{print("序列非法\n");return false;}
	else 
		{print("序列合法\n");return ture;}

}




队列的表示与实现
//队列的抽象数据类型定义
ADT quene {
数据对象:
	Q={ai | ai∈ElemSet,i=1,2,3,,n,n>=0}
数据关系:
	R={ <ai-1,ai> | ai-1, ai ∈D,i=2,3,,n} //待定
	约定an为队尾,a1为队头
基本操作:初始化,进队,出队,取队头元素等
}ADT quene

队列的顺序表出
//顺序队列的表示
#define MAXQSIZE 100//最大表长
typedef struct{
	QElemType *base; // Q的元素存在base数组中,引用base的首地址
	int front;  //队头,若队列不为空,指向队头元素
	int rear;  //队尾,若队列不为空,指向队尾元素的下一个位置
	//Qlengh MAXQSIZE; //队长  待定 ,这里也没有定义表长
}SqQuene;

//MAXQSIZE 表示队的最大容量,最多元素个数
//队空 front == rear
//队满 (front+1)/mod == rear  //王卓老师以队后留一个空的方式来定义队满,其他书上可能还有其他方法(另设一个标志,区别队满对空;另设一个变量,记录元素个数),根据实际情况再看
//初始化
	Status InitQuene(SqQuene &Q){
		Q.base = new QElemtype[MAXSIZE];
		if(!Q.base) exit(overflow); //分配空间失败
		Q.front = Q.rear = 0;
		//Q.  = MAXSIZE;  待定  没有定义表长
		return ok;
	}
//判队空 
	Status QueneEmpty(SqQuene Q){
		if(Q.front == Q.rear)
			return 1 ;
		else 
			return 0;
	}
//清空队
	Status ClearQuene(SqQuene &Q){
		if(Q.front == Q.rear) 
		Q.front == Q.rear;
		return ok;
	}
//求队的长度
	int QueneLength(SqQuene Q){
			return ( (Q.rear-Q.front+MAXQSIZE)%MAXQSIZE );
	}

// 入队:先把元素值赋值给e,然后队尾位置往后挪一位
	Status 	EnQuene(SqQuene &Q,QElemtype e  ){
		if((Q.rear+1)%MAXQSIZE==Q.front ) //判断是否满栈
			return error;
		Q.base[Q.rear] = e ;//在队尾插入元素e
		Q.rear = (Q.rear+1)%MAXQSIZE;// 队尾指针后移一位
		return ok;
	}

// 出队:先队头位置往后挪一位,然后把需要删除的那个元素的值赋值给e
	Status 	DeQuene(SqQuene &Q,QElemtype e  ){
		if(Q.front==Q.rear ) //判断是否空栈
			return error;
		e = Q.base[Q.front];//头先出,把队头指针元素赋值给e
		Q.front = (Q.front+1)%MAXQSIZE;// 头指针后移一位
		return ok;
	}
// 取队头元素
	QElemType GetHead(SqQuene &Q){
		if(Q.front==Q.rear ) //判断是否空栈
			return Q.base[Q.front];  //返回队头指针元素
		return ok;
	}

队列的链式表出
typedef struct QNode{
	QElemType date; // 数据域
	struct QNode *next //指针域,头指针就是front指针
}QNode,*QuenePtr;

typedef struct{
	QuenePtr front// 队头指针,指向头结点
	QuenePtr rear// 队尾指针,指向最后一个元素
}LinkQuene;

LinkQuene Q; //定义一个链栈表Q
Q.front;//访问头指针
Q.rear;//访问尾指针   这一趴暂时不理解,为什么是.不是->

在这里插入图片描述

  • 需要头结点,front指向头结点
  • 空队则是front和rear都指向空节点
  • 进队(删除)在队头front进行
  • 出队(插入)在队尾rear进行

//初始化
	viod InitQuene(LinkQuene &Q){
		Q.front = Q.rear = (QuenePtr)malloc(sizeof(Qnode));
		Q.front->next = NULL;
		return ok;
	}

//判栈空 
	Status QueneEmpty(LinkQuene Q){
		if(Q.front == NULL )
			return 1 ;
		else 
			return 0;
	}

//清空栈
	Status ClearQuene(LinkQuene Q){
		if(Q.front) 
		Q.front == NULL;
		return ok;
	}
//销毁队列
	//从头指针开始,一个个出,直到为空
	Status DestroyQuene(LinkQuene &Q){
		p = (QuenePtr)malloc(sizeof(Qnode))
		while(Q.front) { //依次循环,到头指针指向空
			p = Q.front->next; //用p来移动,p指向下一个指针
			free(Q.front); //头指针出队
			Q.front = p; //头挪到下一个指针
		} //由于Q.rear没用,可以把p换称Q.rear
		return ok;
	}

//获取队头元素
	Status 	GetHead(LinkQuene Q,QElemtype &e){
		if(Q.front = Q.rear} //队空
			return error;
		e = Q.front->next->date;
		rerutn ok;
	}

// 入队:
	Status 	EnQuene(LinkQuene &Q,QElemtype e){
		p = (QuenePtr)malloc(sizeof(Qnode)) //新建一个结点
		if(!p) exit(overflow);
		p->date = e;  //把e插入新结点的数据域
		p->next = NULL;  //尾结点的地址域需要设为空
		Q.rear->next = p; //让原尾指针指向p,让p进队
		Q.rear = p;  //把尾指针挪到p的位置
		return ok;
	}

// 出队:
	Status 	DeQuene(EinkQuene &Q,QElemtype e){
		if(Q.front == Q.rear} //队空
			return error;
		p = (QuenePtr)malloc(sizeof(Qnode)) //新建一个结点
		p = Q.front->next; //p指向首元结点
		e = p->date ;  //把首元结点的数据域的元素存到e中
		Q.front->next = p->next;  //把头指针指向首元结点下一个结点
		if(p == Q.rear}  
			Q.rear = Q.front;
		drop(p); //删掉原来的首元结点
		return ok;
	}
	

串,数组和广义表

串是内容受限的特殊表,内容只能是字符
数组:里面的元素是一个线性表
广义表:里面的元素是一个广义表

定义:0个或者任意多个字符串组成的有限序列;S为串名,ai串值,n为串长,n=0则为空串
子串:任意个连续字符组成的子序列(含空串)称为该串的子串
真子串:不包含自身的所有子串
主串:包含子串的串
字符位置:字符在序列中的序号,为该字符在串中的位置
子串位置:子串的第一个字符出现在主串中的位置
空格串:由一个或者多个空格组成的串,与空串不同
串相等:长度相等 + 对应位置上的字符都相等。所有的空串都是相等的

//串的抽象数据类型定义
ADT String {
数据对象:
	D={ai | ai∈CharacterSet,i=1,2,3,,n,n>=0}
数据关系:
	R1={ <ai-1,ai> | ai-1, ai ∈D,i=2,3,,n} //待定
	约定an为队尾,a1为队头
基本操作:串赋值,串比较,求串长,串连接,求子串,定位子串的位置等
}ADT String

串的表出
//顺序串的表示
#define MAXLEN 255//最大表长
typedef struct{
	char cha[MAXLEN+1]; // 存储串的一组数组
	int length;  //串当前的长度
}SString;

//MAXLEN 表示队的最大容量,最多元素个数
//length 表示当前的长度,最大为MAXLEN+1(1为NULL结尾所占长度)

//链串的表示
//一般都用块链结构,这样存储密度会大很多,不会那么浪费
#define CHUNKSIZE 80 // 块的大小可自定义
type struct CHUNK{
	char cha[CHUNKSIZE]; // 存储串的一个块数组
	struct CHUNK *next;  // 链式结构 
}CHUNK;
type struct {
	CHUNK *head,*tail;//块的头指针,尾指针
	int curlen;//串当前的长度
}LString;

串的算法实现

模式匹配算法

算法目的:确定主串中所含子串(模式串)第一次出现的位置(定位)
算法种类

  • BF算法(古典的,经济的,朴素的,穷举的 — 重点掌握
  • KMP算法(特点:速度快)
//BF算法
//算法思路:从S的每一个字符开始依次与T字符进行匹配
int Index_BF(SString S,SString T,int pos){
	int i = pos,j=1;
	while(i<=S.length && j<=T.length){ //依次匹配,直至子串匹配完或者主串匹配完
		if(s.ch[i] == t.ch[j]){
			i++;
			i++;
			}
		else {
			i = i-j+2; //回溯,移动了i-j+1次,然后需要挪到原位的下一位重新扫描 i-j+1+1
			j=1; //j直接回到初始位
			}
	}
	if(j >= T.length) 
		return i - T.length; //返回匹配成功 的第一个字符的下标
	else 
		return 0; //匹配不成功
}

//BF算法
//算法思路:从S的每一个字符开始依次与T字符进行匹配
int Index_BF(SString S,SString T,int pos){
	int i = pos,j=1;
	while(i<=S.length && j<=T.length){ //依次匹配,直至子串匹配完或者主串匹配完
		if(s.ch[i] == t.ch[j])
			{i++;j++;}
		else {
			i = i-j+2; //回溯,移动了i-j+1次,然后需要挪到原位的下一位重新扫描 i-j+1+1
			j=1; //j直接回到初始位
			}
	}
	if(j > T.length) 
		return i - T.length; //返回匹配成功 的第一个字符的下标
	else 
		return 0; //匹配不成功
}

//KMP算法
//算法思路:从S的每一个字符开始依次与T字符进行匹配
int Index_KMP(SString S,SString T,int pos){
	int i = pos,j=1;
	while(i<=S.length && j<=T.length){ //依次匹配,直至子串匹配完或者主串匹配完
		if(j == 0 || s.ch[i] == t.ch[j])
			{i++;j++;}
		else 
			j=next[j]; //i不变,j后退
	}
	if(j > T.length) 
		return i - T.length; //返回匹配成功 的第一个字符的下标
	else 
		return 0; //匹配不成功
}
	
数组
基本概念
声明类型: 数据类型 变量名称[行数][列数];
	int num[5][8];

定义一个m行n列的数据
typedef elemtype array2[m][n];
等价于
typedef elemtype array1[n];
typedef elemtype array1 array2[m];
  • 结论:线性表示数组结构的一个特例(一维的数组);数组结构是线性表结构 的一个扩展(多维线性表)
  • 特点:结构固定。数组定义好后维数和维界,不再改变。
顺序存储方式
//数组的抽象数据类型定义

ADT Array {
数据对象:
	j = 1,2,3,,n,i = 1,2,3,,n
	D={ai | ai∈CharacterSet,i=1,2,3,,n,n>=0}
数据关系:构造,销毁,取值,赋值
	
基本操作:定义
}ADT Array
特殊矩阵的压缩存储
  1. 对称矩阵 若对一个"阶矩阵/中的任意一个元素都有如 产 知 则称其为对称矩阵 其中的元素可以划分为3个部分,即上三角区、主对角线和下三角区,

对于n阶对称矩阵,上三角区的所有元素和下三角 区的对应元素相同,若仍采用二维数组存放,则会浪费 几乎一半的空间,为此将n 阶对称矩阵A
存放在一维数 组B[n(n+l)/2]中,即元素%”存放在施中。比如只 存放下三角部分(含主对角)的元素。

  1. 三角矩阵 下三角矩阵[见图3.22(a)] 中,上三角区的所有元素均为同一常量。其存储思想与对称矩阵 类似,不同之处在于存储完下三角区和主对角线上的元素之后,紧接着存储对角线上方的常量一 次,故可以将n阶下三角矩阵』压缩存储在B[n
    (n+l)/2 +1]中。

  2. 三角对称矩阵 对角矩阵也称带状矩阵。 对于〃阶矩阵Z中的任意一个元素a,当>1时,有ay=0(lWi,jWn),则称为三对角矩阵 在三对角矩阵中,所有非零元素都集中在以主对角线为中心的3条对角线的区域,其他区域的元素都为零。

三对角矩阵S也可以采用压缩存储,将3条对角线上的元 素按行优先方式存放在一维数组B中,且a”存放于B[0]中, 其存储形式如图3.25所示。

稀疏矩阵

矩阵中非零元素的个数f,相对矩阵元素的个数S来说非常少,即的矩阵称为稀疏矩阵。
例如,一个矩阵的阶为100x100,该矩阵中只有少于100个非零元素。

将 非零元素及其相应的行和列构成一个三元组(行标,列标,值)然后按照某种规律存储这些三元组。稀疏矩阵压缩存储后便失去了随机存取特性。

稀疏矩阵的三元组既可以采用数组存储,也可以采用十字链表法存储。

广义表

非线性逻辑结构

树与二叉树

树的概念

定义

  • 树是n个结点的有限集。当n = 0时,称为空树。
  • 任意一棵非空树中应满足:
    • 有且仅有一个特定的称为根的结点。
    • 当n>1时,其余结点可分为m个互不相交的有限集,其中每个集合本身又是一棵树,并且称为根的子树。
  • 树是一种递归的数据结构。
  • 树作为一种逻辑结构,同时也是一种分层结构,有两个特点:根没有前驱,除了根外的所有结点有且只有一个前驱但有0个或者多个后继

术语

  • 结点的度:一个结点的孩子数
  • 树的度:树中结点的最大的度数
  • 分支节点:度大于0的结点,有孩子的结点
  • 叶结点:度等于0的结点,没孩子的结点
  • 结点的层次:从树根开始定义,根节点为第1层,它的子节点为第2层,以此类推
  • 结点的深度:从根节点开始,自顶向下,逐层累计
  • 结点的高度:从叶节点开始,自底向上,逐层累计
  • 树的高度or深度:树中结点的最大层数
  • 有序树:从左到右是有次序的,不能互换,则成为有序树,否则为无序树
  • 路径和路径长度:两个结点之间的路径是由这两个结点之间所经过的结点序列构成,而路径长度是路径上所经过的边的个数
  • 森林:森林是m棵互不相交的树的集合。把树的根节点删掉,就成了森林。反之,给森林加一个根节点,就成了树
    /
/ An highlighted block
var foo = 'bar';`
11

二叉树的概念

定义
二叉树:是n个结点的有限集,它或者是空集(n=0),或者是由一个根节点,两颗互不相交的分别成为左子树和右子树的二叉树组成。

性质
性质1:在二叉树的第i层上至多有2^(i-1)个结点
在这里插入图片描述
性质2:深度为k的二叉树至多有2^k-1个结点
推理:深度为k的二叉树至少有k个结点
在这里插入图片描述
性质3:对于任意一个二叉树,如果其叶子树为n0,度为2的结点为n2,则n0=n2+1
在这里插入图片描述

满二叉树。一棵高度为如 且含有2"-1个结点的二叉树称为满二叉树,即树中的每层都
含有最多的结点,如图5.3(a)所示。满二叉树的叶结点都集中在二叉树的最下一层,并且 除叶结点之外的每个结点度数均为2。 可以对满二叉树按层序编号:约定编号从根结点(根结点编号为1)起,自上而下,自左 向右。这样,每个结点对应一个编号,对于编号为,的结点,若有双亲,则其双亲为质2」, 若有左孩子,则左孩子为2让若有右孩子,则右孩子为27+1。
完全二叉树。高度为力、有〃个结点的二叉树,当且仅当其每个结点都与高度为介的满二 叉树中编号为1〜
〃的结点一一对应时,称为完全二叉树,1 若fWL〃/2」,则结点z•为分支结点,否则为叶结点。 2
叶结点只可能在层次最大的两层上出现。对于最大层次中的叶结点,都依次排列在该 层最左边的位置上。 3
若有度为1的结点,则只可能有一个,且该结点只有左孩子而无右孩子(重要特征)。 4
按层序编号后,一旦出现某结点(编号为i)为叶结点或只有左孩子,则编号大于i 的结点均为叶结点。 5
若"为奇数,则每个分支结点都有左孩子和右孩子;若〃为偶数,则编号最大的分支
结点(编号为”/2)只有左孩子,没有右孩子,其余分支结点左、右孩子都有。

存储结构

  • 顺序存储
    • 指用一组地址连续的存储单元依次自上而下、自左至右存储完全二叉树上的结点元素,即将完全二叉树上编号为的结点元素存储在一维数组下标为的分量中。
    • 完全二叉树和满二叉树采用顺序存储比较合适。树中结点的序号可以唯 一地反映结点之间的逻辑关系,这样既能最大可能地节省存储空间,又能利用数组元素的下标值 确定结点在二叉树中的位置,以及结点之间的关系。
  • 链式存储
    • 用链表结点来存储
    • 二叉树中的每个结点。在二叉树中,结点结构通常包括若干数r— Hd… I’ data 据域和若干指针域,二叉链表至少包含’3个域:数据域data、-左指针域Ichild和右指针域rchild,
      • 完全二叉树和满二叉树采用顺序存储比较合适。树中结点的序号可以唯 一地反映结点之间的逻辑关系,这样既能最大可能地节省存储空间,又能利用数组元素的下标值 确定结点在二叉树中的位置,以及结点之间的关系。
二叉树的遍历

4种遍历方法:先序,中序,后序,层次

//递归遍历:三种方式差别就在于什么时候访问输出根节点
//先序
Status InOrderTraverse(BiTress T){
	if(T = NULL) 
		return ok ;
	else{
		visit(T); //访问根节点
		InOrderTraverse(T->lchild); //递归遍历左子树
		InOrderTraverse(T->rchild);//递归遍历右子树
	}
}
//中序
Status InOrderTraverse(BiTress T){
	if(T = NULL) 
		return ok ;
	else{
		InOrderTraverse(T->lchild); //递归遍历左子树
		visit(T); //访问根节点
		InOrderTraverse(T->rchild);//递归遍历右子树
	}
}
//后序
Status InOrderTraverse(BiTress T){
	if(T = NULL) 
		return ok ;
	else{
		InOrderTraverse(T->lchild); //递归遍历左子树
		InOrderTraverse(T->rchild);//递归遍历右子树
		visit(T); //访问根节点
	}
}


//非递归遍历:利用栈
//中序
Status InOrderTraverse(BiTress T){
	BiTress P;InitStack(S); 
	p=T; //p指向根节点
	while(p || !StackEmpty(S)) {
		if(p){//根节点非空,则把根先入栈,去找左孩子
			PUSH(S,p);p=p->lchild;}
		else{//左孩子已空,则根出栈,输出,然后去找右孩子
			POP(S,q);printf("%c",q->date);p=q->rchild;}
	}
}

//先序
Status InOrderTraverse(BiTress T){
	BiTress P;
	InitStack(S); 
	p=T; //p指向根节点
	while(p || !StackEmpty(S)) {
		if(p){
			printf("%c",p->date);p=p->lchild;}
		else{
			p=q->rchild;}
	}
}

//层次遍历
//原理:从上到下,从左到右,访问每一个节点
//算法思路:将根节点入队;队不空则进入循环,从队列中出列一个结点*p,访问它;若它有左孩子,讲左孩子入队,若它有右孩子,右孩子入队(出一个进2个(若都无则不进,继续出))
void LevelOrder(BTNODE *b){
	BTNODE *p;
	SqQuene *qu;
	InitQuene(qu);   //初始化队列
	enQuene(qu,b);   //根节点进入队列
	while(QueneEmpty(qu)){  //队列非空,则一直循环
		deQuene(qu,p);   //出队一个根节点
		printf("%c",p->date);   //输出出队结点
		if(p->lchild) enQuene(qu,p->lchild);   //若孩子不为空,则进队
		if(p->rchild) enQuene(qu,p->rchild);   //若孩子不为空,则进队
		}
}

二叉树的应用,跳过

线索二叉树

线索二叉树:利用二叉链表中的空指针域,若某个结点的左孩子为空,则把空的左孩子指针指向前驱结点,若右孩子为空,则把空的右孩子指针指向后继节点,这种改变指向的指针叫做线索。加上了线索的二叉树称为线索二叉树
线索化:把二叉树按照某种遍历方式将其变成线索二叉树的过程,称为线索化
标志域:ltag&rtag,用来标志指针域指向的是孩子还是线索,=0为孩子,=1为线索
线索二叉树的结点结构:lchild,ltag,data,rtag,rchild
增设头结点:ltag=0,lchild指向根节点,data为空,rtag=1,rchild指向遍历序列最后一个结点。遍历序列中第一个lchild域和最后一个的rchild域都指向头结点,闭环。

typedef struct BiThrNode{
int data;
int ltag,rtag;
struct BiThrNode *lchild,rchild; 
}BiThrNode,*BiThrTree;
树和森林

树的存储结构

双亲表示法
顺序存储结构
定义结构数组,存放树的结点,每个结点含两个域:

  • 数据域(存放自身数据信息)
  • 双亲域(指示本节点的双亲在数组中的位置)

特点:找双亲容易,找孩子难

//数的结点的抽象类型定义
typedef struct PTNode{
Elemtype date; 
int parent; //双亲的位置域
}PTNode;

//数的结构
#define MAX_TREE_SIZE 100
typedef struct {
PTNode notes[MAX_TREE_SIZE]; 
int r,n; //根结点的位置和结点个数
}PTree;

孩子链表
顺序存储 + 链式存储结构

特点:找孩子容易,找双亲难
如果给数组中加一个双亲的位置(像双亲表示法一样),则找孩子找双亲都简单些,就是空间占得多,这个叫带双亲的孩子链表

//孩子结点的抽象类型定义,单链表形式
typedef struct CTNode{
int child; 
struct CTNode *next; 
}*ChildPtr;

//双亲结点结构,顺序表形式
typedef struct {
Elemtype date; 
ChildPtr firstchild; //孩子链表头指针
}CTbox;

//数的结构
#define MAX_TREE_SIZE 100
typedef struct {
CTbox notes[MAX_TREE_SIZE]; 
int r,n; //根结点的位置和结点个数
}PTree;

孩子兄弟表示法
也叫二叉链表表示法,二叉树表示法
二叉链表存储结构
链表中的每个结点的两个指针,一个指向其第一个孩子结点,一个指向下一个兄弟结点

特点:找孩子和兄弟容易,找双亲还是挺难
如果给结点结构中加指向一个双亲的指针,则找孩子找双亲都简单些,就是空间占得多

//抽象类型定义,二叉树表示形式
typedef struct CSNode{
Elemtype date; 
struct CSNode *firstchild,*nextsibling; 
}CSNode,*CSTree;

树与二叉树的转换

通过二叉链表存储法,把树转化为二叉树,进行运算后,再通过二叉链表转换为树

**一一对应:**给定一棵树,可以找到唯一的一棵二叉树与之对应
在这里插入图片描述

树转化为二叉树的步骤
加线:在兄弟之间加一条连线
抹线:对每一个结点,除了其左孩子外,去除其与其它孩子的连线
旋转:以树的根节点为轴心,将树顺时针旋转45度
树变二叉树口诀:兄弟相连留长子
在这里插入图片描述

二叉树转化为树的步骤
加线:若p结点是双亲的左孩子,则将p的右孩子,以及右孩子的右孩子等一系列右孩子,都与p的双亲连线起来
抹线:抹掉原来二叉树中,双亲与孩子的连线
调整:将结点按层次排列,形成树结构
二叉树变树口诀:左孩右右连双亲,去掉原来右孩线
在这里插入图片描述

森林与二叉树的转换

森林转化为二叉树的步骤
将每棵树转化为二叉树
将每棵树的根节点用线相连
以第一课树根节点为二叉树的根节点,再以根节点为轴心,顺时针旋转
树变二叉树口诀:树变二叉根相连
在这里插入图片描述

二叉树转化为森林的步骤
抹线:抹掉原来二叉树中,根节点的右孩子及右孩子的右孩子的连线,全部抹掉,拆成独立的树
还原:将孤立的二叉树还原
二叉树变树口诀:去掉全部右孩线,孤立二叉再还原
在这里插入图片描述

树与森林的遍历

这里是引用
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

哈夫曼树

概念

最优二叉树
结点的路径长度
完全二叉树是路径最短的二叉树之一

结点的带权路径长度
树的带权路径长度

哈夫曼算法

哈夫曼树只有度为0和2,没有度为1的结点
哈夫曼树一定有2n-1个结点:n个根节点,根节点的n-1次合并有n-1个结点在这里插入图片描述

哈夫曼算法实现

顺序存储结构
在这里插入图片描述

举例
在这里插入图片描述
先初始化
在这里插入图片描述
再创造哈夫曼树
在这里插入图片描述
在这里插入图片描述

//哈夫曼算法
//结点的抽象类型定义
typedef struct{
int weight;
int parent,lchild,rchild;
}HTNode,*HuffmanTree

哈夫曼编码

哈夫曼编码的操作
在这里插入图片描述
在这里插入图片描述
为什么最优
在这里插入图片描述

哈夫曼编码的算法实现

这里是引用

//哈夫曼编码的算法实现
void 

图的概念
(1)定义:
	由顶点的有穷非空集合和顶点之间边的集合组成
	通常表示为:G(V,E)
	    G表示一个图
	    V是图G中的顶点的集合
	    E是图G中边的集合

(2)分类
	根据边的方向分
		有向图:边有向
		无向图:边无向
	根据边的数量分
		完全图:任意两点都一条边相连
			完全无向图:有n(n-1)/2条边
			完全有向图:有n(n-1)条边
		稀疏图:有很少的边或者弧
		稠密图:有很多边或者弧
	根据边的权值分
		图:边不带权值的图
		网:边带权值的图
(3)顶点的度
	无向图:与该顶点相关的边的数量则称为度
	有向图:有向图顶点分为 入度(箭头朝自己) 和 出度(箭头朝外)

(4)路径
	定义:
		路径:接续的边构成的顶点序列
		路径长度:路径上边或者弧的数目或者权值之和
	衍生:
		A:回路():第一个顶点和最后一个顶点相同的路径
		B:简单路径:除了起点和终点外,其他路过的顶点都不相同的路径
		A+B:简单回路():起点和终点相同的简单路径
		
(4)连通
	定义:任意两个顶点之间都存在可以到达的路径则称为连通图
		无向的连通图:连通图
		有向的连通图:强连通图
	衍生:
	极大连通子图:
		再加一个顶点,就不再连通的连通子图
		并不包含所有顶点
	连通分量:无向图的极大连通子图
	极小连通子图:
		再删一条边,就不再连通的连通子图
		(n个顶点要留n-1条边)
	生成树:
		包含所有顶点的极小连通子图
	有向树:
	    有向图中一顶点入度为0
	    其余顶点入度为1的叫 有向树
	生成森林:待确认
		对于非连通图,由各个连通分量生成的树的集合
	    一个有向图由若干棵有向树构成生成 森林
	
		

在这里插入图片描述在这里插入图片描述在这里插入图片描述

在这里插入图片描述在这里插入图片描述

权:边或者弧具有的相关系数,称为权。表名一个顶点到另一个顶点耗费的时间或者距离等
子图:抽图A的一部分顶点一部分边的集合,成为图A的子图
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

图的存储结构
邻接矩阵

数组表示法(邻接矩阵)
建立一个顶点表(记录各个顶点信息)和一个邻接矩阵(记录各个顶点的关系)
在这里插入图片描述
邻接矩阵特征
无向图(有边记为1):为对称矩阵,且主对角线为0;第i个顶点的度:第i行所有元素之和
有向图(发出记为1):不一定为对称阵,但主对角线仍为0;第i个顶点的度:第i行所有元素之和(出度)+第i列所有元素之和(入度)
网:不一定为对称阵,但主对角线仍为0
在这里插入图片描述

存储结构

1)邻接矩阵

	a:组成:两个数组:一个一位数组(存顶点)+一个矩阵(存边的关系)
	
	b:存储表示:
	# define Maxlnt 32767 //表示极大值,即∞
	# define MVNum 100 //表示最大顶点数
	typedef char vertaxtype //设顶点的数据类型为字符型
	typedef int arctype //设边的权值为整型
	typedef struct{
		vertaxtype vexs[MVNum];
		arctype arcs[MVNum][MVNum];
		int vexnum,arcnum; //图的当前点数和边数
	}AMGraph
	
	c:采用邻接矩阵创建无向网
		算法思想:
 		- 输入顶点数和边数
 		- 依次输入点的信息存入顶点表
 		- 初始化邻接矩阵,使得每个权值初始化为最大值
 		- 构建邻接矩阵
 		算法:
 			Status CreateUDN(AMGraph &G){
				cin>>G.vexnum>>G.arcnum;
				// 构建顶点数组
				for(i=0;i<G.vexnum;++i)
					cin>>G.vexs[i];
				// 初始化邻接矩阵
				for(i=0;i<G.vexnum;++i){
					for(j=0,j<G.vexnum;++j)
						G.arcs[i][j]=Maxlnt;
				}
				// 构建邻接矩阵
				for(k=0;k<G.arcnum;++k){
					cin>>v1>>v2>>w; //输入一条边所依附的顶点及边的权值
					i=LocateVex(G,v1);
					j=LocateVex(G,v2);//确定v1和v2在G中的位置
					G.arcs[i][j]= w;
					G.arcs[j][i]= G.arcs[i][j]
				}
				return ok;
			}
			
			补充下在图中找顶点的算法:
			int LocateVex(AMGraph G,vertaxtype u){
				int i;
				for(i=0;i<G.vexnum;++i)
					if(u==G.vexs[i])
						return i;
				return -1;
			}2)邻接表
	a:组成:一个顶点链表+一个邻接链表(存边的关系)
	
	b:存储表示:
	//顶点的结点结构
	typedef struct Vnode{
		vertaxtype data;
		arcnode *firstarc; //指向第一条边的指针
	}Vnode,Adjlist[MVNum]; //Adjlist表示邻接表的类型
	//边的结点结构
	# define MVNum 100 //表示最大顶点数
	typedef struct Arcnode{
		int adjvex; //该边所指向的顶点的位置
		struct Arcnode *nextarc;//指向下一条边的指针
		Otherinfo info; //边的相关信息
	}Arcnode;
	//图的结构
	typedef struct {
		Adjlist vertices ;
		int vexnum,arcnum;
	}ALGraph;
	
	c:采用邻接表创建无向网
		算法思想:
 		- 输入顶点数和边数
 		- 建立顶点表
 		- 依次输入点的信息存入顶点表,并使表的头结点的指针域初始化为null
 		- 构建邻接表
 			- 依次输入每条边依附的两个顶点
 			- 确定两个顶点的序号i和j,建立边结点
 			- 将此边结点分别插入vi和vj对应的两个边链表的头部 	
 		算法:
 		Status CreateUDG(ALGraph &G){
			cin>>G.vexnum>>G.arcnum;
			//构建顶点链表
			for(i=0;i<G.vexnum;++i){
				cin>>G.vertices[i].date; //输入顶点信息
				G.vertices[i].firstarc =NULL; //把顶点结点的地址域置空
				}
			//构建边链表
			for(k=0;k<G.arcnum;++k){
				cin>>v1>>v2;
				i=LocateVex(G,v1);
				j=LocateVex(G,v2);
				//创建一个新结点p1
				p1=new ArcNode;
				p1->adjvex =j ;//找到需要插入的顶点j
				p1->nextarc =G.vertices[i].firstarc;//把这个结点插入
				G.vertices[i].firstarc =p1;
				
				p2=new ArcNode;
				p2->adjvex =i ;
				p2->nextarc =G.vertices[j].firstarc;
				G.vertices[j].firstarc =p2;
			}
			return ok ;
		}
		
	
邻接表

在这里插入图片描述

存储结构
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
采用邻接表表示法创建无向网
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

无向图:邻接表不唯一(因为没有顺序);若无向图有n个顶点e条边,则需要n个头结点2e个表结点(因为无向,所以i到j和j到i都要记一次,一共记两次)。适合存稀疏图;度好算,订点i的度=第i个单链表的表结点的个数
有向图:邻接表不唯一(因为没有顺序);若有向图有n个顶点e条边,则需要n个头结点e个表结点。出度好算,订点i的度=第i个单链表的表结点的个数,但入度不好算,需要遍历, 看哪几个单链表的表结点存了i。同样,可以设计逆邻接表,这样就会入度容易算,出度难算,看图的出度入度情况

网和图的差别:非边部分为0还是为无穷大(设定一个极大值),边部分是1还是权。其他构建步骤都完全一样。> 有向和无向差别:是否对称。所以赋值ij时不需要copy到ji,只要按每次输入的ij赋值即可>

采用邻接矩阵

优点:直观简单好理解;能知道任意一对顶点是否存在边;方便找任意顶点的所有邻接点,度好算> 缺点:删除和修改不方便;空间复杂度是o(n)与边无关,如果边少,则很容易浪费空间;如果要统计边,也是要遍历才行o(n),很浪费时间

采用邻接表

优点:直观简单好理解;方便找任意顶点的所有邻接点;节约空间,只需要o(n+2e);无向表的度好算,但有向图的度不好算;删除和修改方便
缺点:不能知道任意一对顶点是否存在边,需要遍历才知道
邻接矩阵和邻接表的联系与区别

在这里插入图片描述

在这里插入图片描述
总是为了解决现有方式的缺点,创造一种新的解决方案,每个科目都是如此,进步的空间,进步的方向,牛皮克拉斯
在这里插入图片描述

十字链表

这里是引用
在这里插入图片描述

邻接多重表

这里是引用

图的遍历

定义
在这里插入图片描述
在这里插入图片描述

深度优先搜索

一条路走到黑,走到无路可走,然后返回到有路可走继续走,循环,直到遍历完每一个顶点
起点不唯一,路线不唯一,所以遍历结果不唯一,有很多种遍历结果
用深度优先搜索算法,遍历一个邻接矩阵表示的图
因为存储结构定了,只要起点也定了,那遍历结果只会有一种,就不会有多种了
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

广度优先搜索

从某个顶点开始,一次性点亮所有邻接点,然后再点亮邻接点的邻接点,依次点亮所有,最多点亮各顶点的度的最大值次
用广度优先搜索算法遍历用邻接表表示的图
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

图的应用

考察重点,但只考选择填空和论述,不考算法
结合图的实例来考查算法的具体操作过程,读者必须学会手工模拟给定图的各个算法的执行过程。此外, 还需掌握对给定模型建立相应的图去解决问题的方法。

最小生成树

最小生成树
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

prim普里姆算法

算法思想
在这里插入图片描述

kruskal克鲁斯卡尔算法

算法思想这里是引用

比较
在这里插入图片描述

最短路径

定义
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Dijkstra迪杰斯特拉算法

Dijkstra迪杰斯特拉算法

算法思想
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

floyed弗洛伊德算法

所有顶点间的最短路径floyed弗洛伊德算法

算法思想
在这里插入图片描述
在这里插入图片描述

有向无环图应用

这里是引用
在这里插入图片描述

拓扑排序

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

关键路径

AOE网
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

操作

查找

查找概念

这里是引用

线性表的查找

顺序查找
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
能不能改进下? 牛皮clus的小技巧
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

折半查找
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

分块查找
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

顺序查找折半查找分块查找
ASL最大,o(n),(1+n)/2,与表结构和存储结构无关最小,o(log2 n),(log2 n+1) - 1中间,可到o(n),也可o(log2 n)
表结构有序表,无序表有序表分块+有序表
储存结构顺序表,线性链表顺序表顺序表,线性链表
树表的查找
二叉排序树的查找

二叉排序树
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
插入操作在这里插入图片描述
生成操作
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
删除操作
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

平衡二叉树的查找

平衡二叉树
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
把中间位置的拿到根的位置
在这里插入图片描述

哈希表的查找
概念

在这里插入图片描述
这里是引用

散列函数的构造方法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

处理冲突的方法

在这里插入图片描述

散列表的查找

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

散列表的查找效率分析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

排序

概念

这里是引用
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

插入排序

这里是引用
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

交换排序
选择排序
归并排序
基数排序
外部排序
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值