数据结构与算法

VS2022编译器中鼓励大家使用scanf_s函数来预防原scanf函数的数组越界问题,所以不能使用scanf和strcpy,两种解决方案:
第一种
在代码最前面添加
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:4966)
这样方便程序的移殖
在这里插入图片描述
第二种使用编译器的函数scanf_s和strcpy_s
只需要在scanf_s中添加数组长度即可
例如
char str[128]; scanf_s("%s", str,128);
前面预备知识中代码我写的是第二种方案,后面就一直写第一种。

流程,语句功能,试数

pS->pTop = (PNODE)malloc(sizeof(NODE));
动态内存分配malloc返回首字节地址并赋值给TOP

为什么变量使用前需要初始化,因为当软件运行完毕后,操作系统将回收该内存空间(注意:操作系统并不清空该内存空间中遗留下来的数据),以便再次分配给其他软件使用。所以记得声明变量后记得定义初始化。

为什么if条件中要写if(1==n)而不是if (n==1),因为这样写是防止你写成赋值语句n=1,搞完还半天检查不出来错误,写成1==能有效避免这种错误。

前言

  • 个人觉得郝斌老师的数据机构教程讲的很不错,既讲出了原理,也讲出来实践,其他老师的课是先将一大堆伪算法,后面可能会将实际运用,这会导致我们听的迷迷糊糊,但郝斌老师会讲原理流程的同时进行实际运用
  • 本文章主要参考视频为:【郝斌】-数据结构入门为主;参考的书籍有:程杰的《大话数据结构》、严蔚敏老师的《数据结构(C语言)(第二版)》

数据结构概述

1.什么叫数据结构

定义:我们如何把现实中大量而复杂的问题以特定的数据类型和特定的存储结构保存到主存储器(内存)中,以及在此基础上为实现某个功能(比如查找某个元素,删除某个元素,对所有元素进行排序)而执行的相应操作,这个相应的操作也叫算法。
数据结构=个体+个体的关系
算法=对存储数据的操作

2.衡量算法的标准

算法: 解题的方法和步骤
衡量算法的标准
1.时间复杂度:大概程序要执行的次数,而非执行的时间。

详细可以看这篇文章
如何分析算法的时间复杂度

2.空间复杂度:算法执行过程中大概所占用的最大内存
3.难易程度(可读性:你的算法一定易于让别人读懂)
4.健壮性:对于非法输入,你的算法一定可以做出判断做出争取的处理,比如打印出:“您的输入非法”等。

严奶奶写的是正确性(你的算法一定是正确的、可读性、健壮性、高效性(你的算法要尽可能的跑的快,占用的空间小))。

3.数据结构的特点

数据结构是软件中最核心的课程
程序=数据的存储+数据的操作+可以被计算机执行的语言

内存中什么叫栈?什么叫堆?
并不是内存中专门分配一块区域叫栈,一块区域叫堆,这样理解是不对的,其实是分配内存的算法不一样,如果是以出栈压栈的方式为栈内存,以堆排序的方式为堆内存。

预备知识

指针

地址:内存单元的编号;从0开始的非负整数;范围:0~FFFFFFFF【0-4G-1】
指针:指针就是地址,地址就是指针;指针变量是存放内存单元地址的变量;指针的本质是一个操作受限的非负整数。

分类:
1.基本类型的指针

基本概念

	int i = 10;
	int* p=&i;  //等价于int* p; p = & i;

详解这两部操作:
1)p存放了i的地址,所以我们说p指向了i
2)p和i是完全不同的两个变量,修改其中的任意一个变量的值,不影响另一个变量的值
3)p指向i,*p就是i变量本身。
图解:
第一步:
请添加图片描述
因为怕有错误说法,所以暂且用比喻,这一步就相当于你在前台开了一个房间,但房间里面存放的东西(值)和房间号(地址)你不知道。

操作系统向内存开辟一个内存单元,准备存放数据类型为int类型 i 的值

第二步:
在这里插入图片描述
i已经初始化,这个时候相当你开了门,知道里面放的东西(值),且通过&i这个操作此时你已经知道i的房间号(地址)是什么。然而现在你又开了个薛定谔的房间,因为这个房间(p)只能存放地址,而你又没对它进行操作,所以它无法访问。

内存单元地址编号为“xxxxxx”()存放10的值,但此时指针变量在定义时还未初始化,其值是随机的,指针变量的值是别的变量的地址,意味着指针指向了一个地址是不确定的变量,也就是野指针。

空指针

  • 没有存储任何的内存地址的指针就称为空指针(NULL指针)
  • 空指针就是被赋值为0的指针,在没有初始化之前,它的值为0;

野指针

  • 野指针不是NULL指针,是指向“垃圾”内存(不可用内存)的指针。
  • 在计算机中,内存的分配由操作系统来管理,要使用内存就需要先向操作系统申请。
  • 野指针的内存空间是由系统随机分配的,属于非法访问内存。

第三步:
在这里插入图片描述
现在你将 i 的房间号(地址)放入了p中,自然你通过p就可以知道了i的房间号,也就知道i房间里面的东西(值)。

编译器给指针分配的空间大小是和CPU的寻址长度相关的,比如32位的CPU,它的寻址长度为32位,那么这个空间也就占四个字节,其实不管你定义什么样的指针类型,这个空间只是用来存地址,只占四个字节,而真正该空间所存的地址是哪一段内存的首地址才和所定义的指针类型相关。

最终结果:
在这里插入图片描述
同学们验证的时候会发现地址是不同的,因为操作系统都会随机开辟一片空间,所以地址是不同的。

注意:

  • %p中的p是pointer(指针)的缩写,是十六进制的形式,但是会全部打完,即有多少位打印多少位。
  • %x:无符号十六进制整数(字母小写,不像上面指针地址那样补零)
  • %X:无符号十六进制整数(字母大写,不像上面指针那样补零)
    32位编译器的指针变量为4个字节,64位编译器的指针变量为8个字节。【一个字节=8bit】
    所以,在32位编译器下,使用%p打印指针变量,则会显示32位的地址(16进制的);在64位编译器下,使用%p打印指针变量,则会显示64位的地址(16进制的),左边空缺的会补0。%x、%X和%p的相同点都是16进制,不同点是%p按编译器位数长短(32位/64位)输出地址,不够的补零。所以笔者这里弄错了

另外郝斌老师在p6说所有指针变量只占4个字节,是因为当时(09年)x64电脑还未普及,默认都是x32电脑。

总结:

  1. 如何一个指针变量(假定为p)存放了某个普通变量(假定为i)的地址,那我们就可以说:“p指向了i”,但p与i是两个不同的变量,修改p的值不影响i的值,修改i的值不影响p的值。
  2. p等价于i或者说p可以与i在任何地方互换。
  3. 如果一个指针变量指向了某个普通变量,则*指针变量 就完全等价于 该普通变量
    注意:
    指针变量也是变量,只不过它存放的不能是内存单元的内容,只能存放内存单元的地址。
    普通变量前不能加*;常量和表达式前不能加&

如何通过被调函数修改主调函数中普通变量的值?
实参为相关变量的地址
形参为以该变量的类型为类型的指针变量
在被调函数中通过 *形参变量名 的方式就可以修改主函数相关变量的值

2.指针和数组的关系
指针 和 一维数组

数组名

  • 一维数组名是个指针常量,
  • 它存放的是一维数组第一个元素的地址
  • 它的值不能被改变

它的值也就是地址,地址是操作系统负责随机分配,所以不能被改变

  • 一维数组名指向的是数组的第一个元素
    下标和指针的关系:a[i]<<==>>*(a+i)

假设指针变量的名字为p,则p+i的值是p+i*(p所指向的变量所占的字节数)。
指针常量的运算:

  • 指针变量的运算,指针变量不能相加,不能相乘,不能相除;如果两指针变量属于同一数组,则可以相减。
  • 指针变量可以加减一整数,前提是最终结果不能超过指针变量
  • p+i 的值是 p+i*(p 所指向的变量所占的字节数)
  • p-i 的值是 p-i*(p 所指向的变量所占的字节数)
  • p++<==>p+1
  • p–<==>P-1

如何通过被调函数修改主调函数中一维数组的内容
两个参数:存放数组首元素的指针变量存放数组元素长度的整形变量

#include<stdio.h>
void Show_Array(int* p, int len)
{
	int i = 0;
	for (i = 0; i < len; ++i)
		printf("%d\n", p[i]);
}
int main(void)
{
	int a[5] = { 2,4,5,6,7 };
	Show_Array(a,5);//a等价于&a[0],&a[0]本身就是int* 类型
	return 0;
}

结构体

为什么会出现结构体:为了表示一些复杂的数据,而普通的基本类型变量无法满足要求。
什么叫结构体:结构体是用户根据实际需要自己定义的符合数据类型。
如何使用结构体
两种方式:

struct Student st = { 1000,"zhangsan",20 };
struct Student* pst;
 //第一种方式
st.age = 99; 
//第二种方式
pst->sid = 99;//pst->sid 等价于(*pst).sid  而   (*pst).sid 等价于 st.sid, 所以 pst->sid 等价于 st.sid

注意事项

  • 结构体变量不能加减乘除,但可以相互赋值
  • 结构体变量和结构体指针变量作为函数传参的问题
    示例:
#include<stdio.h>
#include<string.h>

void f(struct Student* pst);
void g1(struct Student st);
void g2(struct Student* pst)

struct Student
{
	int sid;
	char name[200];
	int age;
};

int main(void)
{
	struct Student st ;
	f(&st);
	//g1(st);
	g2(&st);
	return 0;
}
void g1(struct Student st)
{
	printf(" % d % s % d\n", st.sid, st.name, st.age);
}//这种方式耗内存,耗时间,不推荐,因为这种方式是将整个数组传过去了

void g2(struct Student *pst)
{
	printf(" % d % s % d\n", pst->sid, pst->name, pst->age);
}

void f(struct Student* pst)
{
	(*pst).sid = 99;	
	pst->age = 22;
	strcpy_s(pst->name,sizeof(pst->name)+1, "zhangsan");
	//这个是我在VS2022的写法,用strcpy会出问题。
}

动态内存的分配和释放

//#define _CRT_SECURE_NO_WARNINGS
//#pragma warning(disable:4966)
#include<stdio.h>
#include<malloc.h>

int main(void)
{
	//静态分配
	int a[5] = { 4,23,432,34,3 };
	//动态分配
	int len;       //因为scanf_s需要
	printf("请输入你需要分配的数组的长度:len = ");
	scanf_s("%d", &len,sizeof(len));
	int* pArr = (int*)malloc(sizeof(int) * len);
	//*pArr = 4;    //类似于a[0] = 4;
	//pArr[1] = 10; //类似于a[1] = 10;
	//*(pArr + 2) = 5;

	//我们可以把pArr当作一个普通数组来使用
	for (int i=0; i < len; ++i)
		scanf_s("%d", &pArr[i],sizeof(i));
	for (int i = 0; i < len; ++i)
		printf("%d\n", *(pArr + i));


	free(pArr);   //将pArr所代表的动态分配的20个字节的内存释放
	return 0;
}

跨函数使用内存讲解及其示例

#include<stdio.h>
#include<malloc.h>

struct Student
{
	int sid;
	int age;
};

struct Student* CreateStudent(void);
void ShowStudent(struct Student *);

int main(void)
{
	struct Student* ps;
	ps = CreateStudent();
	ShowStudent(ps);
	return 0;
}
struct Student* CreateStudent(void)
{
	struct Student* p = (struct Student*)malloc(sizeof(struct Student));
	p->sid = 3;
	p->age = 2;
	return p;
}
void ShowStudent(struct Student* pst)
{
	printf("%d  %d\n", pst->sid, pst->age);
	return 0;
}

模块一:线性结构【把所有的结点用一根直线穿起来】

在使用VS2022时发现不能使用bool类型,有两种解决方案,一种是将bool类型转换成 int类型,然后返回值设成 1 or 0;一种是将把默认值改为编译为C++代码
在这里插入图片描述

连续存储【数组/顺序表】

顺序表是在计算机内存中以数组的形式保存的线性表,是指用一组地址连续的存储单元依次存储数据元素的线性结构。
线性表采用顺序存储的方式存储就称之为顺序表。顺序表是将表中的结点依次存放在计算机内存中一组地址连续的存储单元中。
从顺序表的定义上可以看出,顺序表就是数组。二者只是不同领域中的称呼。
顺序表是数据结构中的专有名词,而数组是在C语言或者其它编程语言中的一种数据类型。
可以说,数组是顺序表在实际编程中的具体实现方式。
exit()通常零值表示正常结束,非零值表示应错误返回。

1.什么叫数组:元素类型相同,大小相同

第一部分:初始化数组/线性表

#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:4966)
#include<stdio.h>
#include<malloc.h>   //包含了 malloc 函数
#include<stdlib.h>   // 包含了 exit 函数

//第一部分  
void init_arr(struct Arr* pArr, int length);   //初始化
bool is_empty(struct Arr* pArr);               //判断数组是否为空
void show_arr(struct Arr* pArr);               //输出数组
bool is_full(struct Arr* pArr);                //判断数组是否满了

//第二部分  线性表的一系列操作   所谓的增删改查
bool append_arr();     
bool insert_arr();
bool delete_arr();
int get();
void sort_arr();
void inversion_arr();


struct Arr
{
	int* pBase;     //存储的是数组第一个元素的地址
	int len;        //数组所能容纳的最大元素的个数
	int cnt;        //当前数组有效元素的个数
//  int increment;  //自动增长因子
};



int main()
{
	struct Arr arr;
	init_arr(&arr,6);
	show_arr(&arr);

	return 0;
}

void init_arr(struct Arr *pArr,int length)
{
	pArr->pBase = (int*)malloc(sizeof(int) * length);
	if (pArr->pBase == NULL)
	{
		printf("动态内存分配失败!\n");
		exit(-1);   //exit()通常零值表示正常结束,非零值表示应错误返回。
	}
	else
	{
		pArr->len = length;
		pArr->cnt = 0;
	}
	return;

}
bool is_empty(struct Arr* pArr) 
{
	if (pArr->cnt == 0)
		return true;
	else 
		return false;
}
bool is_full(struct Arr* pArr)
{
	if (pArr->cnt == pArr->len)
		return true;
	else
		return false;
}
void show_arr(struct Arr* pArr)
{
	if (is_empty(pArr))
	{
		printf("数组为空!\n");
	}
	else
	{
		for (int i = 0; i < pArr->cnt; ++i)
			printf("%d ", pArr->pBase[i]);
		printf("\n");
	}
}

第二部分:线性表的一系列操作 所谓的增删改查

#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:4966)
#include<stdio.h>
#include<malloc.h>   //包含了 malloc 函数
#include<stdlib.h>   // 包含了 exit 函数

//第一部分  
void init_arr(struct Arr* pArr, int length);   //初始化
bool is_empty(struct Arr* pArr);               //判断数组是否为空
void show_arr(struct Arr* pArr);               //输出数组
bool is_full(struct Arr* pArr);                //判断数组是否满了

//第二部分  线性表的一系列操作   所谓的增删改查
bool append_arr(struct Arr* pArr, int val);           //增加   val:增加元素的值
bool insert_arr(struct Arr* pArr, int pos,int val);   //插入   pos:插入元素的地址,且从1开始,相当于下标 
bool delete_arr(struct Arr* pArr, int pos,int* pVal); //删除   pos:删除元素的地址,且从1开始   pVal:保存被删除元素的值
int get();
void sort_arr(struct Arr* pArr);                       //选择排序?
void inversion_arr(struct Arr* pArr);                  //倒置


struct Arr
{
	int* pBase;     //存储的是数组第一个元素的地址
	int len;        //数组所能容纳的最大元素的个数
	int cnt;        //当前数组有效元素的个数
//  int increment;  //自动增长因子
};


int main()
{
	struct Arr arr;
	int val;        //到时候用于保存被删除元素的值
	init_arr(&arr,6);

 //   append_arr(&arr, 1);
	//append_arr(&arr, 2);
	//append_arr(&arr, 3);
	//append_arr(&arr, 4);
	//append_arr(&arr, 5);
	//append_arr(&arr, 6);
	//show_arr(&arr);

	delete_arr(&arr, 5, &val);
    insert_arr(&arr, 7, 2);
	show_arr(&arr);

	return 0;
}

void init_arr(struct Arr *pArr,int length)
{
	pArr->pBase = (int*)malloc(sizeof(int) * length);
	if (pArr->pBase == NULL)
	{
		printf("动态内存分配失败!\n");
		exit(-1);   //exit()通常零值表示正常结束,非零值表示应错误返回。
	}
	else
	{
		pArr->len = length;
		pArr->cnt = 0;
	}
	return;

}
bool is_empty(struct Arr* pArr) 
{
	if (pArr->cnt == 0)
		return true;
	else 
		return false;
}
bool is_full(struct Arr* pArr)
{
	if (pArr->cnt == pArr->len)
		return true;
	else
		return false;
}
void show_arr(struct Arr* pArr)
{
	if (is_empty(pArr))
	{
		printf("数组为空!\n");
	}
	else
	{
		for (int i = 0; i < pArr->cnt; ++i)
			printf("%d ", pArr->pBase[i]);
		printf("\n");
	}
}

bool append_arr(struct Arr* pArr, int val)
{
	//数组满了则返回false
	if (is_full(pArr))
	{
		printf("空间已满,第 %d 个元素增加成功失败!\n", pArr->cnt + 1);
		return false;
	}
	//没满就可以增加
	pArr->pBase[pArr->cnt] = val;
	printf("第 %d 个元素增加成功\n" ,pArr->cnt+1);
	pArr->cnt++;
	return true;
}
bool insert_arr(struct Arr* pArr, int pos, int val)
{
	if (is_full(pArr)|| pos<1 || pos>pArr->cnt + 1)  //如果数组满了,或者插入位置为负数是不可能,且不能插在根节点的位置,也不能插在没有值的元素位置之后
		return false;
	for (int i = pArr->cnt - 1; i >= pos - 1; --i)
	{
		pArr->pBase[i + 1] = pArr->pBase[i];
	}
	pArr->pBase[pos - 1] = val;
	pArr->cnt++;
	return true;
}
bool delete_arr(struct Arr* pArr, int pos, int* pVal)
{
	if (is_empty(pArr) || pos<1 || pos>pArr->cnt)  
	{
		printf("删除失败!\n");
		return false;
	}
	*pVal = pArr->pBase[pos - 1];
	for (int i = pos; i < pArr->cnt; ++i)
	{
		pArr->pBase[i - 1] = pArr->pBase[i];
	}
	pArr->cnt--;
	printf("删除成功!且删除的元素值为:%d\n",*pVal);
	return true;
}
void inversion_arr(struct Arr* pArr)
{
	int i = 0;
	int j = pArr->cnt - 1;
	int t;

	while (i < j)
	{
		t = pArr->pBase[i] = pArr->pBase[j];
		pArr->pBase[i] = pArr->pBase[j];
		pArr->pBase[j] = t;
		++i;
		--j;
	}
	return;
}
void sort_arr(struct Arr* pArr)
{
	int i = 0;
	int j = pArr->cnt - 1;
	int t;
	while (i < j)
	{
		for (i = 0; i < pArr->cnt; ++i)
		{
			for (j = i + 1; i < pArr->cnt; ++j)
			{
				if(pArr->pBase[i]>pArr->pBase[j])
				{
					t = pArr->pBase[i] = pArr->pBase[j];
					pArr->pBase[i] = pArr->pBase[j];
					pArr->pBase[j] = t;
				}
			}
		}
	}
	return;
}

离散存储【链表】

线性结构 树结构 图结构 本质上都是链表的各种变形

预备知识:typedef的用法

为已有都数据类型取个名字

typedef struct Student
{
	int sid;
	char name[100];
	char sex;
}ST;
int main()
{
	//两种写法
	struct Student st;
	ST st1;
	return 0;
}

高级用法:

typedef struct Student
{
	int sid;
	char name[100];
	char sex;
}* PST;  //PST 等价于 struct Studnet*
int main()
{
	struct Student st;
	PST ps = &st;
	ps->sid = 99;
	printf("%d", ps->sid);
	return 0;
}
typedef struct Student
{
	int sid;
	char name[100];
	char sex;
}* PST,STU;//  STU等价于struct Student ; PST 等价于 struct Studnet*

int main()
{
	STU st;
	PST ps = &st;
	ps->sid = 99;
	printf("%d", ps->sid);
	return 0;
}

链表的定义

定义

  • n个节点离散分配,彼此通过指针相连
  • 每个节点只有一个前驱节点,每个节点只有一个后继节点
  • 首节点没有前驱节点,尾节点没有后续节点

专业术语:

  • 首节点:第一个有效节点
  • 尾节点:最后一个有效节点
  • 头节点:第一个有效节点之前的那个节点,并不存放有效数据,目的是为了方便对链表的操作【头节点并没有存储有效数据,也没有存放链表中有效节点的个数。首节点开始存放有效数据。在链表前边加一个没有实际意义的头节点,可以方便对链表的操作。头节点于之后节点的数据类型相同】
  • 头指针:指向头节点的指针变量
  • 尾指针:指向尾节点的指针变量

如果希望通过一个函数来对链表进行处理,我们至少需要接受链表的哪些参数?
只需要一个参数:头指针。因为我们通过头指针可以推算出链表的其他所有参数

每一个链表结点的数据类型该如何表示?
每个节点至少有两部分:有效数据和指向下一个节点的指针,尾节点指针指空(NULL)。【绿色解释:一个结构体变量的某个成员指向了一个与自己本身数据类型一样的另一个变量】
在这里插入图片描述
链表中每个节点的存储结构 【预备知识】

typedef struct Node
{
	int data;          //数据域  存储数据本身
	struct Node* pNext;//指针域  pNext指向一个和它本身存储指向下一个节点的指针
};
  • PNODE p =(PNODE)malloc(sizeof(NODE));
    解释:将动态分配的新节点的地址赋给p
  • frer p; //删除p指向结点所占的内存,不是删除p本身所占内存;
    f()函数里面定义的指针变量p所在的内存单元将被释放,里面是地址,但指针p所指向的那些内存单元(即malloc分配的将不会被释放)
  • p->pNext ; // p所指向结构体变量中的pNext成员本身

算法遍历,查找,清空,销毁,求长度,排序,删除节点,插入节点

分类
单链表:
双链表:每一个节点有两个指针域
循环链表:能通过任何一个节点找到其他所有的节点

非循环单链表:

头文件:

#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:4966)
#include<stdio.h>
#include<malloc.h>
#include<stdlib.h>

定义链表:

typedef struct Node
{
int data;//数据域
struct Node * pNext;//指针域
}NODE,*PNODE;//NODE 等价于 struct Node,PNODE 等价于 struct Node *

函数声明:

PNODE create_list(void);//创建一个链表
void traverse_list(PNODE pHead);//遍历链表
bool is_empty(PNODE pHead);//链表是否为空
int length_list(PNODE); //和遍历链表算法一样,只是在遍历链表时用一个变量++就行
bool insert_list(PNODE,int,int);
bool delete_list(PNODE,int,int*);
void sort_list(PNODE);
链表创建(尾插法)和链表遍历算法的演示
int main(void)
{
	PNODE pHead = NULL;//等价于 struct Node* pHead = NULL;

	pHead = create_list(); //功能:创建一个非循环单链表,并将该链表的头节结点的地址赋给pHead;
	traverse_list(pHead);
	return 0;
}
//链表创建(尾插法)
PNODE create_list(void)
{
	int len;  //用来存放有效结点的个数
	int i;
	int val ; //用来临时存放用户输入的结点的值

	//分配了一个不存放有效数据的头结点
	PNODE pHead = (PNODE)malloc(sizeof(NODE));
	if (pHead == NULL)
	{
		printf("头结点分配失败,程序终止!\n");
		exit(-1);
	}
	PNODE pTail = pHead;
	pTail->pNext = NULL;
	
	printf("请输入你需要生成的链表结点的个数:len =");
	scanf("%d", &len);

	for (i = 0; i < len; i++)
	{
		printf("请输入第%d个结点的值:", i + 1);
		scanf("%d", &val);

		PNODE pNew = (PNODE)malloc(sizeof(NODE));
		if (pNew == NULL)
		{
			printf("链表结点分配失败,程序终止!\n");
			exit(-1);
		}
		pNew->data = val;
		pTail->pNext = pNew;
		pNew->pNext = NULL;
		pTail = pNew;
	}

	return pHead;
}
//链表遍历
void traverse_list(PNODE pHead)
{
	PNODE p = pHead->pNext;

	while (p != NULL)
	{
		printf("%d ", p->data);
		p = p->pNext;
	}
	printf("\n");
}
判断链表是否为空 和 求链表长度 算法的演示
int main(void)
{
	PNODE pHead = NULL;//等价于 struct Node *pHead=NULL;
	pHead = create_list();//创建一个非循环单链表,并将该链表的头节点的地址付给 pHead
	traverse_list(pHead);
	int len = length_list(pHead);
	printf("链表长度是%d\n", len);
	if (is_empty(pHead))
		printf("链表为空!\n");
	else
		printf("链表不空!\n");
	return 0;
}
bool is_empty(PNODE pHead)
{
	if (pHead->pNext == NULL)//头节点指针域为 NULL 则链表为空
		return true;
	else
		return false;
}
int length_list(PNODE pHead)//和遍历链表算法一样,只是在遍历链表时用一个变量++就行
{
	PNODE p = pHead->pNext; //头节点的指针域,指向第一个节点的指针(地址)
	int len = 0;
	while (NULL != p) //最后一个节点指针域为 NULL
	{
		len++;//节点数自增
		p = p->pNext; //下一个节点的指针域。不连续,不能用 p++
	}
	return len;//节点数
}
通过链表(选择 )排序算法的演示

链表排序伪算法:(选择排序算法)
从第一个开始分别和右边一个一个的两相进行比较,(黄色分别和后面比较,绿色分别和后面比较)两数互换,小的放左边,大的放右边。

算法:
狭义的算法是与数据的存储方式密切相关
广义的算法是与数据的存储方式无关
泛型:
利用某种技术达到的效果就是:不同的存储方式,执行的操作是一样


int main(void)
{
	PNODE pHead = NULL;//等价于 struct Node *pHead=NULL;
	pHead = create_list();//creat_list()功能:创建一个非循环单链表,并将该链表的头节点的地址付给 pHead
	traverse_list(pHead);
	int len = length_list(pHead);
	printf("链表长度是%d\n", len);
	if (is_empty(pHead))
		printf("链表为空!\n");
	else
		printf("链表不空!\n");
	sort_list(pHead);
	traverse_list(pHead);
	return 0;
}
void sort_list(PNODE pHead)//选择排序
{
//数组选择排序伪算法
/*  
	for (i = 0; i < len - 1; ++i)//两两比较只需要比较 len-1 次
	{
		for (j = i + 1; j < len; ++j)//从第 i+1 个开始进行比较
		{
			if (a[i] > a[j])//两数互换,大右小左,两杯水互换,引入第三个杯子 temp
			{
				temp = a[i];
				a[i] = a[j];
				a[j] = temp;
			}
		}
	}
*/
	int i, j, t;
	int len = length_list(pHead);
	PNODE p, q;
	for (i = 0, p = pHead->pNext; i < len - 1; ++i, p = p->pNext)//两两比较只需要比较 len-1 次
	{
		for (j = i + 1, q = p->pNext; j < len; ++j, q = q->pNext)//从第 i+1 个开始进行比较
		{
			if (p->data > q->data)//类似于数组中的:a[i]>a[j]
			{
				t = p->data;//类似于数组中的: t=a[i];
				p->data = q->data;//类似于数组中的: a[i]=a[j];
				q->data = t;//类似于数组中的: a[j]=t;
			}
		}
	}
}
链表插入和删除算法的演示

非循环单链表插入节点伪算法讲解:
在这里插入图片描述
删除非循环单链表节点伪算法讲解:
在这里插入图片描述

int main(void)
{
	PNODE pHead = NULL;//等价于 struct Node *pHead=NULL;
	int val;
	pHead = create_list();//creat_list()功能:创建一个非循环单链表,并将该链表的头节点的地址付给 pHead
	traverse_list(pHead);
	int len = length_list(pHead);
	printf("链表长度是%d\n", len);
	if (is_empty(pHead))
		printf("链表为空!\n");
	else
		printf("链表不空!\n");
	sort_list(pHead);
	traverse_list(pHead);
	insert_list(pHead, 4, 33);
	traverse_list(pHead);
	if (delete_list(pHead, 4, &val))
	{
		printf("删除成功,您删除的元素是:%d\n", val);
	}
	else
	{
		printf("删除失败!您删除的元素不存在!\n");
	}
	traverse_list(pHead);
	return 0;
}
//在 pHead 所指向链表的第 pos 个节点的前面插入一个新的节点,新节点的值是 val,并且pos 的值是从 1 开始
//看 20_非循环单链表插入节点伪算法讲解部分
bool insert_list(PNODE pHead, int pos, int val)
{
	int i = 0;
	PNODE p = pHead;//指向头节点的指针(地址)
	while (NULL != p && i < pos - 1)//在遍历链表时用一个变量++然后和要插入 pos 位置进行比较
	{
		p = p->pNext; //下一个节点的指针域。不连续,不能用 p++
		++i; //节点数自增
	}
	if (i > pos - 1 || NULL == p)//插入位置大于原链表的长度或插入到 NULL 空链表
		return false;
	PNODE pNew = (PNODE)malloc(sizeof(NODE)); //创造新的节点
	if (NULL == pNew)
	{
		printf("动态分配内存失败!\n");
		exit(-1);
	}
	pNew->data = val; //节点数据域中的数据插入
	//看 20_非循环单链表插入节点伪算法讲解部分
	 //问题:已知 p 节点,把新节点 q 插入到 p 节点后面?两种插入算法
	PNODE q = p->pNext;
	p->pNext = pNew;
	pNew->pNext = q;
	return true;
}
//看 21_非循环单链表删除节点伪算法的讲解部分
bool delete_list(PNODE pHead, int pos, int* pVal)
{
	int i = 0;
	PNODE p = pHead;
	while (NULL != p->pNext && i < pos - 1)
	{
		p = p->pNext;
		++i;
	}
	if (i > pos - 1 || NULL == p->pNext)
		return false;
	PNODE q = p->pNext;
	*pVal = q->data;
	//删除 p 节点后面的节点
	p->pNext = p->pNext->pNext;
	free(q);
	q = NULL;

	return true;
}

综合运用:

#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:4966)
#include<stdio.h>
#include<malloc.h>   //包含了 malloc 函数
#include<stdlib.h>   // 包含了 exit 函数
#include<stdbool.h>

typedef struct Node
{
	int data;//数据域
	struct Node* pNext;//指针域   
}NODE,*PNODE;//NODE等价于struct Node     PNODE等价于struct Node*

PNODE create_list(void);//创建一个链表
void traverse_list(PNODE pHead);//遍历链表
bool is_empty(PNODE pHead);//链表是否为空
int length_list(PNODE); //和遍历链表算法一样,只是在遍历链表时用一个变量++就行
bool insert_list(PNODE, int, int);
bool delete_list(PNODE, int, int*);
void sort_list(PNODE);

int main(void)
{
	//第一步:创建一个链表,并实现遍历功能
	PNODE pHead = NULL;
	pHead = create_list();
	traverse_list(pHead);
	//第二步:实现 判断链表是否为空 和求链表长度 功能
	if (is_empty(pHead) == 1)
		printf("链表为空\n");
	else printf("链表不为空且链表长度为%d\n", length_list(pHead));
	//第三步:实现 (选择)排序
	sort_list(pHead);
	printf("排序后链表:");
	traverse_list(pHead);
	//第四步:实现 插入 删除
	insert_list(pHead, 4, 100);
	traverse_list(pHead);
	int val;
	if (delete_list(pHead, 4, &val))
	{
		printf("删除成功,您删除的元素是:%d\n", val);
	}
	else
	{
		printf("删除失败!您删除的元素不存在!\n");
	}
	traverse_list(pHead);
}

PNODE create_list(void)
{
	int len;
	int val;
	PNODE pHead = (PNODE)malloc(sizeof(NODE));
	if (pHead == NULL)
	{
		printf("头结点创建失败\n");
		exit(-1);
	}
	PNODE pTail = pHead;
	pTail->pNext = NULL;

	printf("请输入所创建链表的节点个数:len=");
	scanf("%d", &len);
	
	for (int i = 0; i < len; i++)
	{
		printf("请输入第%d个节点的值:", i + 1);
		scanf("%d", &val);

		PNODE pNew = (PNODE)malloc(sizeof(NODE));
		if (pNew == NULL)
		{
			printf("分配失败\n");
			exit(-1);
		}
		pNew->data = val;
		pTail->pNext = pNew;
		pNew->pNext = NULL;
		pTail = pNew;
	}
	return pHead;
}
void traverse_list(PNODE pHead)
{
	PNODE p = pHead->pNext;
	while (p != NULL)
	{
		printf("%d ", p->data);
		p = p->pNext;
	}
	printf("\n");
}
bool is_empty(PNODE pHead)
{
	if (pHead->pNext == NULL)
		return true;
	return false;
}
int length_list(PNODE pHead)
{
	int len=0;
	PNODE p = pHead->pNext;
	while (p != NULL)
	{
		len++;
		p = p->pNext;
	}
	return len;
}
void sort_list(PNODE pHead)
{
	int i, j, t;
	int len = length_list(pHead);
	PNODE p, q;
	for (i = 0, p = pHead->pNext; i < len;i++ , p = p->pNext)
	{
		for (j = i + 1, q = p->pNext; j < len; j++, q = q->pNext)
		{
			if (p->data > q->data)
			{
				t = p->data;
				p->data = q->data;
				q->data = t;
			}
		}
	}
}
bool insert_list(PNODE pHead, int pos, int val)
{
	int i = 0;
	PNODE p = pHead;

	while (p != NULL && i < pos - 1)
	{
		p = p->pNext;
		++i;
	}
	
	if (i > pos - 1 || NULL == p)
		return false;
	PNODE pNew = (PNODE)malloc(sizeof(NODE)); 
	if (NULL == pNew)
	{
		printf("动态分配内存失败!\n");
		exit(-1);
	}
	pNew->data = val; 
	PNODE q = p->pNext;
	p->pNext = pNew;
	pNew->pNext = q;
	return true;
}
bool delete_list(PNODE pHead, int pos, int*pVal)
{
	int i = 0;
	PNODE p = pHead;
	while (NULL != p->pNext && i < pos - 1)
	{
		p = p->pNext;
		++i;
	}
	if (i > pos - 1 || NULL == p->pNext)
		return false;
	PNODE q = p->pNext;
	*pVal = q->data;
	//删除 p 节点后面的节点
	p->pNext = p->pNext->pNext;
	free(q);
	q = NULL;
	return true;
}
复习

数据结构
狭义:
数据结构是专门研究数据存储的问题
数据的存储包含两方面:个体的存储+个体关系的存储
广义:
数据结构既包括数据的存储也包括数据的操作
对存储数据的操作就是算法

算法
狭义:算法是和数据的存储方式密切相关
广义:算法和数据的存储方式无关
这就是泛型思想

数据的存储方式有几种
1、线性存储::连续存储【数组】和离散存储【链表】

  • 连续存储【数组】
    优点:存取速度很快
    缺点:事先需要知道数组的长度;插入删除元素很慢;空间通常有限制;需要大块连续的内存块
  • 离散存储【链表】
    优点:空间没有限制;插入删除元素很快
    缺点:存取速度很慢;线性结构的应用–栈;线性结构的应用–队列

2、非线性存储:树 和 图

线性结构的两种常见应用之一 栈

栈的定义

栈的定义:栈( stack )是限定仅在表尾进行插入和删除操作的线性表,又称后进先出(Last In First Out)的线性表,简称LIFO结构
我们把允许插入和删除的一端称为栈顶(top) ,另一端称为栈底 (bottom)。不含任何数据元素的栈称为空栈

分类:静态栈 和 动态栈
算法:出栈 和 压栈
(链式栈)
新建一个栈

#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:4966)
#include<stdio.h>
#include<malloc.h>   //包含了 malloc 函数
#include<stdlib.h>   // 包含了 exit 函数
#include<stdbool.h>

typedef struct Node
{
	int data;//数据域
	struct Node* pNext;//指针域   
}NODE,*PNODE;//NODE等价于struct Node     PNODE等价于struct Node*

typedef struct Stack
{
	PNODE pTop;
	PNODE pBottom;
}STACK,*PSTACK;

void init(PSTACK);
void push(PSTACK, int);
bool pop(PSTACK,int*);
void traverse(PSTACK); 
bool is_empty(PSTACK );
void clear(PSTACK);//清空

int main(void)
{
	STACK S;
	int val;
	init(&S);
	push(&S, 1);
	//push(&S, 2);
	//push(&S, 3);
	traverse(&S);
	clear(&S);
	traverse(&S);
	if (pop(&S, &val))
		printf("出栈成功,且出栈的元素为%d\n", val);
	else printf("出栈失败\n");
	//traverse(&S);
	//clear(&S);
	//traverse(&S);
}
void init(PSTACK pS)
{
	pS->pTop = (PNODE)malloc(sizeof(NODE));
	if (pS->pTop == NULL)
	{
		printf("动态内存分配失败!\n");
		exit(-1);
	}
	else
	{
		pS->pBottom = pS->pTop;
		pS->pTop->pNext = NULL;
	}
}//目的是造出一个空栈
void push(PSTACK pS, int val)
{
	PNODE pNew = (PNODE)malloc(sizeof(NODE));

	pNew->data = val;
	pNew->pNext = pS->pTop;
	pS->pTop = pNew;

	return;
}
void traverse(PSTACK pS)
{
	PNODE p = pS->pTop;
	while (p != pS->pBottom)
	{
		printf("%d ", p->data);
		p = p->pNext;
	}
	printf("\n");
	return;
}
bool is_empty(PSTACK pS)
{
	if (pS->pBottom == pS->pTop)
		return true;
	else return false;
}
bool pop(PSTACK pS, int* pVal)
{
	if (is_empty(pS))
		return false;
	else
	{
		PNODE r = pS->pTop;
		*pVal = r->data;
		pS->pTop = r->pNext;
		free(r);
		r = NULL;
		return true;
	}

}
void clear(PSTACK pS)
{
	if (is_empty(pS))
		return;
	else
	{
		PNODE p = pS->pTop;
		PNODE q = NULL;
		while (p != pS->pBottom)
		{
			q = p->pNext;
			free(p);
			p = q;
		}
		pS->pTop = pS->pBottom;
	}
	return;
}

应用:函数调用;中断;表达式求值;内存分配;缓冲处理;迷宫

线性结构的两种常见应用之二 队列

定义:一种可以实现“先进先出”的存储结构。
分类:

  • 链式队列–用链表实现
  • 静态队列–用数组实现
    静态队列通常都必须是循环队列
循环队列的讲解:

循环队列讲解

  1. 静态队列为什么必须是循环队列
  2. 循环队列需要几个参数来确定
    需要2个参数(front rear )来确定
  3. 循环队列各个参数的含义
    而这2个参数不同场合有不同的含义,建议初学者先记住,然后慢慢体会

1).队列初始化:frontrear的值都是0
2).队列非空:front代表的是队列的第一个元素,rear代表的是队列的最后一个有效元素的下一个元素
3).队列空:frontrear的值相等,但不一定是0

  1. 循环队列入队伪算法讲解
    两步完成:
    1.将值存入 r 所代表的位置
    2.错误的写法:r=r+1;
    正确写法是:r=(r+1)%数组的长度
    在这里插入图片描述
  2. 循环队列出队伪算法讲解
    front=(front+1)%数组的长度
  3. 如何判断循环队列是否为空
    如果 frontrear的值相等,则该队列一定为空。
  4. 如何判断循环队列是否已满 p4队列_判断循环队列是否已满
    在这里插入图片描述
    已知:上图中 front 的值和 rear 的值没有规律,即可以大(3>1),小(0<4),等(f=r)。刚开始 f=r 一定为空,那么之后经过一个循环后 r 又等于 f 了,但此时 r=f 为满,即不能通过 r=f 这个条件来判断其既是空又是满。所以必须通过 f 和 r 的其他关系来判断其为满。
    那么问题:若 f、r 相等,不知道队列到底是空还是满?
    两种方式:1.多增加一个标识参数;2.少用一个元素【通常使用第二种方式】
    如果r的下一个位置是f,则队列已满,n-1个元素可以被使用
if((r+1)%数组长度 == f)
已满
else
不满

链式队列伪算法:

循环队列程序演示

队列算法:
入队
出队

#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:4966)
#include<stdio.h>
#include<malloc.h>   //包含了 malloc 函数

typedef struct Queue
{
	int* pBase;
	int front; //队头下标
	int rear; //队尾下标
}QUEUE;
/*假设以下函数中形参都是普通变量,那么在调用此函数并执行完毕后,形参的内存会被释
放,而且传递进来的实参和形参是两块不一样的内存,无法达到更改实参变量值的目的,
所以以下的函数内的形参都是以指针的方式进行定义,传递的实参为是要被修改的变量的
指针(地址),可以通过指针修改变量的值,而且在被调函数执行完毕后,被调函数的内存
被释放,但是通过指针操作而被修改的变量的值不会被改变,从而达到修改变量值的目的。
从而达在被调函数如 void init(QUEUE *);中修改主调函数 main()中的 QUEUE Q;变量中的值
*/
void init(QUEUE*);//队列初始化
bool en_queue(QUEUE*, int);//入队
void traverse_queue(QUEUE*);//遍历队列
bool full_queue(QUEUE*);//队满
bool out_queue(QUEUE*, int*);//出队
bool empty_queue(QUEUE*);//队空
int main(void)
{
	QUEUE Q;
	int val;
	init(&Q);
	en_queue(&Q, 1);
	en_queue(&Q, 2);
	en_queue(&Q, 3);
	en_queue(&Q, 4);
	en_queue(&Q, 5);
	en_queue(&Q, 6);
	en_queue(&Q, 7);
	traverse_queue(&Q);
	if (out_queue(&Q, &val))
	{
		printf("出队成功,队列出队的元素是%d\n", val);
	}
	else
	{
		printf("出队失败!\n");
	}
	traverse_queue(&Q);
	return 0;
}
void init(QUEUE* pQ)
{
	pQ->pBase = (int*)malloc(sizeof(int) * 6);//长度为 6 个元素的数组
	pQ->front = 0; //队头下标为 0
	pQ->rear = 0; //队尾小标为 0
}
bool full_queue(QUEUE* pQ)
{
	if ((pQ->rear + 1) % 6 == pQ->front)//队列满时 rear/front 紧挨着,由于是循环所以对数组长度
		//取余,且 rear 是最后一个有效元素的下一个元素,rear 指
		//向的元素是空起来不使用的,空一元素判断队满。
		return true;
	else
		return false;
}
bool en_queue(QUEUE* pQ, int val)
{
	if (full_queue(pQ))//是否队满
	{
		return false;
	}
	else
	{
		pQ->pBase[pQ->rear] = val;//从队尾开始,数据入队
		pQ->rear = (pQ->rear + 1) % 6;//队尾 rear 加 1
		return true;
	}
}
void traverse_queue(QUEUE* pQ)
{
	int i = pQ->front;//获取队头,从队头开始出队
	while (i != pQ->rear)//是否出队到队尾
	{
		printf("%d ", pQ->pBase[i]);
		i = (i + 1) % 6;//通过空一元素不使用判断是否到队尾,而 front+1 后对数组长度取余
	}
	printf("\n");
}
bool empty_queue(QUEUE* pQ)
{
	if (pQ->front == pQ->rear) //rear==front 则队列为空
		return true;
	else
		return false;
}
bool out_queue(QUEUE* pQ, int* pVal)//出队
{
	if (empty_queue(pQ))//队列是否空
	{
		return false;
	}
	else
	{
		*pVal = pQ->pBase[pQ->front];//从 front 开始出队
		pQ->front = (pQ->front + 1) % 6;//获取新的队头
		return true;
	}
}

队列的具体应用
所有和时间有关的操作都有队列的影子

专题:递归

函数的调用

  • 当在一个函数的运行期间调用另一个函数时,在运行被调函数之前,系统需要完成三件事:
    1.将所有的实际参数,返回地址等信息传递给被调函数保存
    2.为被调函数的局部变量(也包括形参)分配存储空间
    3.将控制转移到被调函数的入口
  • 从被调函数返回主调函数之前,系统也要完成三件事:
    1.保存被调用函数的返回结果
    2.释放被调函数所占的存储空间(静态)
    3.依照被调函数保存的返回地址将控制转移到调用函数
  • 当有多个函数互相调用时,按照“后调用先返回”的原则,上述函数之间信息传递和控制转移必须借助“栈”来实现,即系统将整个程序之间运行时所需的数据空间安排在一个栈中,每当调用一个函数时,就在栈顶分配一个存储区,进行压栈操作,每当一个函数退出时,就释放它的存储区,进行出栈操作
  • A函数调用A函数 和 A函数调用B函数在计算机看来没有任何区别的,只不过是我们日常的思维方式理解比较怪异。

定义:一个函数自己直接或间接调用自己

递归必须满足三个条件:

  1. 递归必须得有一个明确的中止条件
  2. 该函数所处理的数据规模必须在递减

K在这里插入图片描述虽然n的值在递增,但它处理的数据规模在递减。

  1. 这个转化必须是可解的

循环和递归

  • 递归:
    易于理解
    速度慢
    (浪费)存储空间大
  • 循环:
    不易理解
    速度快
    (浪费)存储空间小
    举例:
1.求阶乘

思想就是 n 的阶乘和前一个(n-1)有关,是相乘的关系 n*(n-1),如 2!=2*(2-1)。n 开始以此类推(n-1)和前一个(n-1)-1 有关,如 3!= 3*(3-1)*[(3-1)-1]。n 减到 n=1 则终止。

#include<stdio.h>

long f(long n)
{
	if (1 == n)
	{
		return 1;
	}
	else return f(n - 1) * n;
}

int main(void)
{
	printf("%d", f(5));
	return 0;
}
2.1+2+3+4…100的和

思想同上求阶乘。n 阶数求和,为 n 和前一个(n-1)有关为求和关系,n+(n-1)。如 2 求和为 2+(2-1),n 减到 n=1 则终止。

#include<stdio.h>

long sum(int n)
{
	if (1 == n)
	{
		return 1;
	}
	else return sum(n - 1) + n;
}

int main(void)
{
	printf("%d", sum(100));
	return 0;
}
3.汉诺塔

p57_汉诺塔
题目:三根柱子分别为起始柱 A、辅助柱 B 及目标柱 C。每次只能移动一个盘子,不重复移动,并且在移动过程中三根杆上都始终保持大盘在下,小盘在上,操作过程中盘子可以置于 A、B、C 任一杆上。
在同一时间只能有一个盘子在某根柱子上移动。每根柱子上最下面的一个盘子称为第 n 号,n 之上的盘子共 n-1 个,因为每个 n 都和之前的 n-1 有关,以此类推直到 n=1。递归的思想。A 起始柱(也是中介柱),B 中介柱(也是起始柱),C 目标柱。

第一步:A 柱作为起始柱,最底号为 n,n 号上面共有 n-1 个盘子。将 n-1 个盘子借助 C 柱,让其 n-1 个盘子按照上小下大的顺序移到中介柱 B 上后,让 A 柱上仅剩的第 n 号盘直接移动目标柱 C 上。
第二步:这时 A 柱上为空没有盘子,B 柱上共 n-1 个盘子。此时 A 和 B 柱功能互换,A 是起始柱,B 是中介柱。B 柱最底号为 n-1,n-1 号上面共有(n-1)-1 个盘子。将(n-1)-1 个盘子借助C 柱,让其按照上小下大的顺序移到 A 柱后,B 柱上就只剩最底号 n-1 号盘。让 B 柱上仅剩的第 n-1 号盘直接移动目标柱 C 上。
第三步:这是 B 柱为空没有盘子,A 柱上共有(n-1)-1 盘子,A 柱作为起始柱,B 柱作为中介柱,又回到第一步了,经过前两步的几个递归循环,实现移动盘子的目的。

2 个圆盘的时候 2 的 2 次方减 1 共 3 次
3 个圆盘的时候 2 的 3 次方减 1 共 7 次
4 个圆盘的时候 2 的 4 次方减 1 共 15 次

#include<stdio.h>
//汉诺塔
void hannuota(int n, char A, char B, char C)
{
	/*
	如果是一个盘子
	直接将 A 柱子上的盘子从 A 移到 C
	否则
	先将 A 柱子上的 n-1 个盘子借助 C 移到 B
	直接将 A 柱子上的盘子从 A 移到 C
	最后将 B 柱子上的 n-1 个盘子借助 A 移到 C
	*/
	if (1 == n)
	{
		printf("将编号为%d 的盘子直接从%c 柱子移到%c 柱子\n", n, A, C);
	}
	else
	{
		hannuota(n - 1, A, C, B);
		printf("将编号为%d 的盘子直接从%c 柱子移到%c 柱子\n", n, A, C);
		hannuota(n - 1, B, A, C);
	}
}
int main(void)
{
	char ch1 = 'A';
	char ch2 = 'B';
	char ch3 = 'C';
	int n;
	printf("请输入要移动盘子的个数 :");
	scanf("%d", &n);
	hannuota(n, 'A', 'B', 'C');
	return 0;
}
4.走迷宫

递归的应用:树和森林就是以递归的方式定义的;树和图的很多算法都是以递归来实现的;很多数学公式就是以递归的方式定义的(例如斐波拉契序列);

线性结构总复习

p59_线性结构总复习

模块二:非线性结构

树结构是一类重要的非线性数据结构。直观来看,树是以分支关系定义的层次结构。树结构在客观世界中广泛存在,如人类社会的族谱和各种社会组织机构都可用树来形象表示。树在计算机领域中也得到广泛应用,尤以二叉树最为常用。如在操作系统中,用树来表示文件目录的组织结构,在编译系统中,用树来表示源程序的语法结构,在数据库系统中,树结构也是信息的重要组织形式之一。

树的定义

书面定义:(Tree)是n (n20)个结点的有限集,它或为空树(n=0);或为非空树,对于非空树T:
(1)有且仅有一个称之为根的结点;
(2)除根结点以外的其余结点可分为m(m>0)个互不相交的有限集T1,T2, …,Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)

通俗的定义:1.树是由节点和边组成;2.每个节点只有一个父节点但可以有多个子节点;3.但有一个例外,该节点没有父节点,此节点称为根节点。

专业术语:

树
(1)结点:树中的一个独立单元。包含一个数据元素及若干指向其子树的分支,如图5.1(b)中的A、B、C、D等。(下面术语中均以图5.1 (b)为例来说明)
(2)结点的度:结点拥有的子树数称为结点的度。例如,A 的度为 3,C 的度为 1,F 的度为 0。
(3)树的度:树的度是树内各结点度的最大值。图 5.1(b)所示的树的度为 3。
(4)叶子:度为 0 的结点称为叶子或终端结点。结点 K、L、F、G、M、I、J 都是树的叶子。
(5) 非终端结点:度不为 0 的结点称为非终端结点或分支结点。除根结点之外,非终端结点也称为内部结点。
(6)双亲和孩子:结点的子树的根称为该结点的孩子,相应地,该结点称为孩子的双亲。例如,B 的双亲为 A,B 的孩子有 E 和F。
(7)兄弟:同一个双亲的孩子之间互称兄弟。例如,H、Ⅰ 和J互为兄弟。
(8)祖先:从根到该结点所经分支上的所有结点。例如,M 的祖先为 A.D 和 H。
(9)子孙:以某结点为根的子树中的任一结点都称为该结点的子孙。如B 的子孙为E、K、L和F。
(10)层次:结点的层次从根开始定义起,根为第一层,根的孩子为第二层。树中任一结点的层次等于其双亲结点的层次加 1。
(11)堂兄弟:双亲在同一层的结点互为堂兄弟。例如,结点 G 与E、F、H、1、J互为堂兄弟。
(12)树的深度:树中结点的最大层次称为树的深度或高度。图5.1(b)所示的树的深度为 4。
(13)有序树和无序树:如果将树中结点的各子树看成从左至右是有次序的(即不能互换),则称该树为有序树,否则称为无序树。在有序树中最左边的子树的根称为第一个孩子,最右边的称为最后一个孩子。
(14)森林:是 m(m≥0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。由此,也可以用森林和树相互递归的定义来描述树。

就逻辑结构而言,任何一棵树都是一个二元组 Tree =(root,F),其中 root 是数据元索,称作树的根结点; F是m(m20)棵树的森林,F=(Ti,T2,…,T-),其中T,-(r,F,)称作根root的第i棵子树;当m0时,在树根和其子树森林之间存在下列关系:
RF ={<root, ri> i= 1, 2,…, m, m>0}
这个定义将有助于得到森林和树与二叉树之间转换的递归定义。

二叉树树的定义:

二叉树(Binary Tree)是n(n≥0)个结点所构成的集合,它或为空树(n=0);或为非空树,对于非空树 T:
(1)有且仅有一个称之为根的结点;
(2)除根结点以外的其余结点分为两个互不相交的子集T1和 T2,分别称为T的左子树和右子树,且 T1 和T2本身又都是二叉树。
二叉树与树一样具有递归性质,二叉树与树的区别主要有以下两点:
(1)二叉树每个结点至多只有两棵子树(即二叉树中不存在度大于2的结点);
(2)二叉树的子树有左右之分,其次序不能任意颠倒。二叉树的递归定义表明二叉树或为空,或是由一个根结点加上两棵分别称为左子树和右子树的、互不相交的二叉树组成。由于这两棵子树也是二叉树,则由二叉树的定义,它们也可以是空树。由此,二叉树可以有 5 种基本形态,如图 5.3 所示。

在这里插入图片描述

二叉树的分类:

现在介绍两种特殊形态的二叉树, 它们是满二叉树完全二叉树
二叉树的分类

满二叉树:在不增加树的前提下,无法再多添加一个节点的二叉树就是满二叉树 【深度为 K且含有2k-1个结点的二叉树。】

满二叉树的特点是:每一层上的结点数都是最大结点数,即每一层i的结点数都具有最大值2i-1。

完全二叉树:如果只是删除了满二叉树最底层最右边的连续若干个节点,这样形成的二叉树就是完全二叉树。【完全二叉树:深度为k的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点–对应时,称之为完全二叉树。图5.6(b)所示为一棵深度为4的完全二叉树。】
(满是完全的特例)

完全二叉树的特点是:
(1)叶子结点只可能在层次最大的两层上出现;
(2)对任一结点,若其右分支下的子孙的最大层次为 L,则其左分支下的子孙的最大层次必为 L 或 L+1。图5.6中©和(d)不是完全二叉树。

树的存储:

一般树和森林都是转换成二叉树来存储,因为二叉树的存储较为成熟。

二叉树的存储

为什么一般二叉树要以数组连续存储的话要先转化成完全二叉树?

连续/顺序存储【完全二叉树】

为什么用完全二叉树的原因
在这里插入图片描述

在严奶奶书中也提到过,但郝斌讲的更透彻一点。在这里插入图片描述

完全二叉树在很多场合下出现, 下面的性质 4 和性质 5 是完全二叉树的两个重要特性。11:30开始讲解在这里插入图片描述
优点:查找某个节点的父节点和子节点(也包括判断有无子节点)速度很快
缺点:耗用内存空间过大

链式存储
一般树的存储
1.双亲表示法

在这里插入图片描述

2.孩子表示法和双亲孩子表示法

在这里插入图片描述

3.二叉树表示法

一个普通的树转化成二叉树

具体转换方法:设法保证任意一个节点的左指针域指向它的第一个孩子右指针域指向它的下一个亲兄弟只要能满足此条件,就可以把一个普通的胡转换成二叉树。【一个普通树转换成的二叉树移动没有右子树】
举例
在这里插入图片描述

森林的存储

先将森林转化为二叉树,再存储二叉树
在这里插入图片描述

二叉树操作

遍历
先序遍历

在这里插入图片描述

中序遍历

在这里插入图片描述

后序遍历

在这里插入图片描述

综合举例:
在这里插入图片描述

已知两种遍历序列求原始二叉树

通过 先序和中序 或者 中序和后序我们可以还原出原始的二叉树,但是通过先序和后序是无法还原出原始的二叉树的。
换种说法,只有通过先序和中序,或通过中序和后序,我们才能唯一的确定一个二叉树。

  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值