郝斌——数据结构笔记(数组、链表、栈、队列)(递归)


学玩数据结构要达到的目的:
(1)对数据结构要有一个基本的了解。
(2)要熟练 掌握链表的操作
(3)伪算法要能看懂(伪算法:只是逻辑,没有代码实现)

一、预备知识

使用教材:严蔚敏,吴伟民编写的《数据结构》
但是书中的算法都是伪算法(不是程序),都是解题的思路
具体的程序由高一凡主编的书里面有。
黄国瑜写的数据结构也可以。

模块一: 线性结构

  • 连续存储(数组
    优点:存取速度很快,可以直接找到某一个元素
    缺点:
    (1)插入删除元素比较慢(后面的元素都得移动)
    (2)需要大片的内存
    (3)事先知道数组的长度(防止内存污染)

  • 离散存储(链表
    优点:
    (1)插入删除元素比较方便(只需要切换指针域即可)
    (2)并不需要大片的内存
    缺点:存取速度比较慢

  • 线性结构的两种常用应用之一 (

  • 线性结构的两种常用应用之一 (队列

  • 专题:递归
    (1)1+2+3+4+5+… 100 的和
    (2)求阶乘
    (3)汉诺塔
    (4)走迷宫

模块二:非线性结构

模块三:查找和排序

  • 折半查找
  • 排序:冒泡、插入、选择、快速、归并

1、预备知识

(1)数据结构概述

定义:
1、我们如何把现实当中,大量而复杂的问题以,特定的数据类型特定的存储结构,保存到内存当中。

2、在此基础上,为实现某个功能(比如查找某个元素、删除某个元素、对所有元素进行排序),而执行的相应操作,这个相应的操作叫做算法

举例:如何保存一个班级学生的信息?

(1)少数较少的时候,使用数组就可以。(需要内存是连续的)
(2)但是保存1000 个学生信息,就没有这么大的连续空间,可以使用链表实现。(使用链表,可以将其他零散的内存利用起来)

(3)如保存人事单,如果使用链表则不知道他们之间的关系谁是领导,这种结构需要使用树结构。(树的结构:就可以知道谁是领导,谁是下属)

(4)再如交通图,几个站点之间修路,则需要使用图结构实现。

总结:
数据结构:解决数据如何存储的问题。(个体和个体的关系)
算法:如何对已经存储的数据,进行操作

2、衡量算法的标准

1、时间复杂度:大概程序要执行的次数、而非执行的时间。(因为硬件不同)

2、空间复杂度:算法执行过程中,大概所占用的最大内存

3、难易程度:不能只有自己一个看懂

4、健壮性:不能给一个非法立即数就挂掉了。

3、数据结构的地位

数据结构是软件中最核心的课程

程序 = 数据的存储 + 数据的操作 + 可以被计算执行的语言。

4、指针

在这里插入图片描述

(1)内存是 cpu 能够直接访问的,唯一的存储器

(2)通过地址线,来确定,对哪块内存来进行存储。(32位——0 – 4G-1)

(3)通过控制线,来确定,对内存是 读取还是 写入。

(4)通过数据线,来确定,传输什么数据。

总结:
(1)地址:内存单元的编号, 0000 0000 – FFFF FFFF, 从0 开始的非负整数。

(2)指针:指针就是地址,地址就是指针。是一个操作受限的非负整数。

(3)指针变量:是存放,内存单元地址(编号),的单元。

(1)基本类型的指针

int *p;

p : 是一个变量的名字
int * :表示 p 只能存放整形变量的地址

必须理解,只能存放,这个概念。

int * p;
char a = 'A';
p = &a; //error:&a 不是整形变量的地址
p = 10//error:10 是一个整数,不是一个地址

(2)数组和指针

int a[5] = {1,2,3,4,5};

在这里插入图片描述

数组名的理解:
(1)一维数组名:它是一个,指针常量
(2)一维数组名:它里面存放的是,第一个元素的地址
(3)一维数组名:指向,数组当中的第一个元素。

下标的理解:

a[3] <=====> *(a+3)

3[a] <=====> *(a+3)

5、结构体的使用

(1)为什么会出现结构体?
为了表示一些复杂的数据类型,而单个基本类型无法满足需要。

(2) 什么是结构体?
用户根据实际需要,自己定义的复合数据类型。

分号不能省略,分号表示结构体定义结束

   struct Student
 {
int aga;
char address;
char sex;
}; //分号不能省略,分号表示结构体定义结束
  • 定义了一个,新的数据类型。(类似于 int )
  • 只是单纯的定义了一个类型,而不定义一个变量,也就是说并不分配内存
  • 这个数据类型的名字struct Student

(3)如何使用结构体

  两种方式:
	struct Student st = {1000, "zhangsan", 20};
	struct Student * pst = &st;
                
1.
	st.sid
2.
	pst->sid
 pst所指向的结构体变量中的sid这个成员

注意事项

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

struct Mystruct
{
	int id;
	char name[200];
	int age;
};
void fcun(struct Mysturct s1) // 输入型参数,不会修改本身的值
{
printf("%d,%s,%d \n", s1.id,s1.name,s1.age);
} 

void func1(struct Mysturct *ps1) // 输出型参数,会修改实参的值
{
	*ps1.s1 = 10086;
	strcpy(ps1->name,"zhangsan");
	ps1->age = 20;	
}

int main(void)
{
	struct Mysturct s1;
	func1(&s1);
	func(s1);
}

(1)当结构体变量,变为函数参数时候:

void fcun(struct Mysturct s1) // 输入型参数,不会修改本身的值
{
printf("%d,%s,%d \n", s1.id,s1.name,s1.age);
} 

这样传参:会发生结构体的赋值,整个结构体单元都会被拷贝。
这种方法:耗用内存、耗费时间,不推荐。(发送了 200多个字节)

解决办法:结构体指针传参

void fcun(struct Mysturct *s1) // 输入型参数,不会修改本身的值
{
printf("%d,%s,%d \n", s1->id,s1->name,s1->age);
} 

这样传参:只传送了 4 个字节。

6、动态内存的分配

怎么区分动态内存和静态内存?

只要使用了 malloc 函数就是动态的。
没使用,就是静态的。

(1)举例:动态构造一个int型数组

char a[5] = {1,2,3,4,5};

这个数组的空间大小,在运行当中是不可以改变的,只能重新编写。

int len;
printf("请输入你需要的数组的长度 len = ");
scanf("%d",len);
int *pArr = (int *) malloc(sizeof(int) * len);

*pArr = 4; // 类似于 a[0] = 4
pArr[1] = 5; // pArr[1] <=====> *(pArr + 1)

free(pArr); // 释放这个内存空间

这个数组的空间大小,在运行当中,取绝于用户的输入,所以是动态的。

(1)malloc 函数的形参:是一个 int 类型的整数,表示申请多少个字节大小的内存空间。
sizeof 返回值是一个整数,int 占用4个字节)

(2)malloc函数的功能是请求系统len个字节的内存空间,如果请求分配成功,则返回第一个字节的地址,如果分配不成功,则返回NULL.

第一个字节地址:它并没有实际的含义,因为我们根据这个地址,可以把 8 个字节当一个变量,也可以把4个字节当一个变量

(3)(int *) 强制转换:就是根据这个地址,把4个字节当作一个变量。

(4)malloc 申请的空间使用

*pArr = 4; // 类似于 a[0] = 4
pArr[1] = 5; // pArr[1] <=====> *(pArr + 1)

可以当作一个普通的数组来使用。

(2)跨函数使用内存

通过调用 fun ,使main 函数当中的指针变量 p 指向一个合法的变量单元。

void fun(int *q)
{
	int s;
	q = &s;
}

int main(void)
{
	int *p;
	fun(p);
}

不可以:因为这是一个 值传递,调用完函数,实参p 的值根本没有改变

void fun(int **q)
{
	int s;
	*q = &s;
}

int main(void)
{
	int *p;
	fun(&p);
}

不可以:
(1)虽然变成了地址传递,最终 p 的值也发生了改变。
(2)但是 s 变量的内存,在fun 执行完毕之后,就销毁了。

void fun(int *q)
{
	q = (int *) malloc(4);
}

int main(void)
{
	int *p;
	fun(p);
}

不可以:因为这是一个 值传递,调用完函数,实参p 的值根本没有改变

void fun(int **q)
{
	*q = (int *) malloc(4);
}

int main(void)
{
	int *p;
	fun(&p);
}

可以访问:因为malloc 函数申请的内存,只有 free 函数才能释放。

使用案例:

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

struct Student
{
	int id;
	int age;
};

struct Student * CreatStudent(); // 返回值使 struct Student *  类型
{
	return (struct Student *) malloc(sizeof(struct Student));
}
void ShowStudent(struct Student *ps1)
{
	printf("%d, %d\n", ps1->id, ps1->age);
}

int main()
{
struct Student *ps1; //申请了 4 个字节的内存空间
ps1 = CreatStudent(); // 又申请了 8 个字节(1个结构体变量)的空间
ShowStudent(ps1); // 输入型参数
return 0;
}

7、typedef 的用法

我们自己构建的数据类型,struct Mystruct 的名字太长用起来不方便

typedef int zhangsan 
int i = 0; <======> zhangsan i = 0;
typedef struct Student
{
	int sid;
	char name[20]'
	int age;
}ST;
// typedef  给 struct Student 新起了一个名字叫做 ST

struct Student st;   <=======>	ST st;
struct Student *pst; <=======>	ST *pst; 
typedef struct Student
{
	int sid;
	char name[20]'
	int age;
}* PST;
// typedef  给 struct Student * 新起了一个名字叫做 PST
struct Student *pst; <=======>	PST pst; 
typedef struct Student
{
	int sid;
	char name[20]'
	int age;
}* PSTU,STU;
// typedef  给 struct Student * 新起了一个名字叫做 PSTU
// 给 struct Student 新起了一个名字叫做 STU
struct Student *pst; <=======>	PSTU pst; 
struct Student st <=======> STU st;

模块一:线性结构

数据结构 = 个体的存储 + 个体的关系存储
算法 = 对存储数据的操作(增加、删除、遍历等等)

别人问:什么是线性结构?
把所有的结点用一根直线穿起来

在这里插入图片描述

1、连续存储数组

int a[10];
int *pArr = (int *)malloc(len);

自己定义一个数据类型,并且为它添加方法。

#include <stdio.h>
#include <malloc.h>
//定义一个数据类型,里面又三个变量
struct Arr
{
	int *pBase; // 存放数组第一个元素的地址
	int len; // 数组所能容纳的最大个数
	int cnt;// 当前数组的元素个数
}
// 定义一些算法
void init_arr(); // 初始化整个数组
bool append_arr(); //在数组最后,追加一个元素
bool insert_arr(); // 在数组当中插入一个元素
bool delete_arr(); // 删除一个数组元素
bool get_arr();
bool is_empty(); // 判断是否为空
boll is_full();	 // 判断是否为满

void sort_arr(); // 排序整个数组
void show_arr(); // 输出数组
void inversion_arr(); //导致整个数组


int main(void)
{
	struct Arr arr;
	init_arr(&arr,6);
	shouw_arr(&arr);
	return 0;
}

void init_arr(struct Arry *pArr , int length)
{
pArr -> pBase = (int *) malloc(sizeof(struct Arry) * length);
if(NULL == pArr->pBase)
	{
		printf("malloc error");
		exit(-1); // 终止整个程序
	}
else
	{
		pArr -> len = length;
		pArr -> cnt = 0;
	}
	return;
}


(1)init_arr

我们希望,一调用 init_arr 函数, arr 当中的 pBase 指向一块连续内存。len 表示这个内存的大小,cnt 有效元素的个数。
在这里插入图片描述

分析:
1、需不需要返回值
2、需不需要参数:肯定需要,没有的话,不知道操作谁
	(1)输入型参数?
	(2)输出型参数?

struct Arr arr;
init_arr(arr);
void init_arr(strcut Arr array) // 传值方式,根本不会改变实参的内容 
{
	array.len = 99; 
}



struct Arr arr; 
init_arr(&arr);  // 传入地址
void init_arr(strcut Arr *parr) // 传址方式,可以改变实参内容
{
	(*parr).len = 99; 
}

void init_arr(struct Arry *pArr , int length)
{
pArr -> pBase = (int *) malloc(sizeof(struct Arry) * length);
if(NULL == pArr->pBase)
	{
		printf("malloc error");
		exit(-1); // 终止整个程序
	}
else
	{
		pArr -> len = length;
		pArr -> cnt = 0;
	}
	return;
}

(2)show_arr

输出这个数组
传入指针:只传递 4 个字节的内容

void show_arr(struct Arry *pArr) // 输出数组,
{
	if(数组为空)
		提示用户数组为空
	else
		输出数组有效内容
}

bool is_empty(struct Arry *pArr)
{
	if(0 == pArr -> cnt)
		return ture;
	else
		return flase;
}

要注意:pArr 是一个结构体变量的地址
void show_arr(struct Arry *pArr) // 输出数组,
{
	if(is_empty(pArr))
		printf("数组为空!\n");
	else
		for(int i = 0; i < pArr->cnt , i++)
		{
			printf("%d\n",pArr->pBase[i]);
		}
}

(3)append_arr

bool append_arr(struct Arry *pArr, int val)
{
	if(数组满了)
		提示用户数组满了
	else
		追加
}

bool is_full(struct Arry *pArr)
{
	if(pArr->cnt == pArr->len)
		return ture;
	else
		return flase;
}

bool append_arr(struct Arry *pArr, int val)
{
	if(is_full(pArr))
		{
			printf("数组已满\n");
			return flase;
		}
	else
		pArr->pBase[pArr->cnt] = val;
		(pArr->cnt)++;
		return ture;
}

(4)insert_arr

思考函数要实现的功能:
向指定位置插入一个数字。
所以需要传入的参数:
1、结构体(数组、数组大小、当前元素个数)
2、准备插入的位置
3、要插入的值

在这里插入图片描述

// pos从1开始,假设pos = 3, val = 55; 则 a[2] = 55;
bool insert_arr(struct Arry *pArr, int pos,int val)
{
	int i = 0;

	if(is_full(pArr))  // 如果满了
		return flase;
	if(pos<1 || pos > pArr->cnt-1) //如果元素不够:一共有 3 个元素,就不可以在第 5 个位置插入
		return flase;

// 先进行后移
	for(i = cnt-1; i>=pos-1; i--)  // 这个循环,一步一步去试数字然后往出写
	{
		pArr->pBase[i+1] = pArr->pBase[i]
	}
// 再进行插入
	pArr->pBase[pos-1] = val;
	(pArr->cnt)++;
}

(5)delete_arr

思考函数要实现的功能:
向指定位置删除一个函数、并且返回成功、还是返回失败?并且想返回被删除的值?
所以需要传入的参数:
1、结构体(数组、数组大小、当前元素个数)
2、准备删除的位置
3、输出型参数 int *pVal (从而实现多个返回值)

主函数调用:
int val; // 定义一个变量来接收删除的数字
struct Arr arr;

delete(&arr,1,&val);

bool delete(struct Arry *pArr, int pos, int *pVal)
{
	int i = 0;
	if( is_empty(pArr) )
		return flase;
	if(pos<1 || pos > pArr->cnt) // 要删除,必须有这个值
		return flase;
	
	// 先拿出
	*pVal = pArr->pBase[pos-1];
	// 再移动
	for(i = pos; i<cnt ; i++)
	{
		pArr->pBase[i-1] = pArr->pBase[i];
		(pArr->cnt)--;
	}
	
	return ture;
}

(6)inversion_arr

函数功能:倒置
最后一个换到第一个

void inversion_arr(struct Arry *pArr)
{
	int i = 0; //第一个元素
	int j = (pArr->cnt)- 1// 最后一个元素
	int tmp = 0;
while(i<j)
	{
		tmp = pArr->pBase[i];
		pArr->pBase[i] = pArr->pBase[j];
		pArr->pBase[j] = tmp;
	} 
	return 0;
}

(7)sort_arr

void sort_arr(struct Arry *pArr)
{
	int i, j = 0;
	int tmp = 0;
	
	for(i=0; j < pArr->cnt-1; ++i) // cnt-1 说明最后一个不用比较
	{
		for(j = i+1 ; j <pArr->cnt; ++j)
			{
				if(pArr->pBase[i] > pArr->pBase[j]) // 把小的换到前面来
					{
						tmp = pArr->pBase[i];
						pArr->pBase[i] = pArr->pBase[j];
						pArr->pBase[j] = tmp;
					} 
			}

	}

}

2、离散存储链表

链表的重要性:是我们学习数据结构的基础,
如下面的树(一个结点指向下面多个结点
在这里插入图片描述

图(任何一个结点可以保存其他结点的地址
在这里插入图片描述

(1)专业术语

定义

(1)n个节点离散分配
(2)彼此通过指针相连接,上一结点保存了下一个结点的地址
(3)每个节点只有一个前驱节点,一个后续节点。首节点没有前驱节点,尾结点没有后续节点。
(树下面可以有很多节点)
在这里插入图片描述
在这里插入图片描述

专业术语:

首节点:第一个有效结点
尾结点:最后一个有效结点

头结点:
(1)第一个有效结点,之前的结点
(2)头结点,不存放有效数据
(3)加头结点的目的是可以方便我们对链表的操作
(4)头结点与其他结点,数据类型完全一样

头指针:第一个节点的地址
指向头结点的指针变量(可能不对,是指向第一个有效结点的指针)

尾指针:最后一个节点的地址
指向尾结点的指针变量。

如果我们希望,通过函数来对链表进行处理,我们至少需要接受链表的几个参数

数组:(3个参数)首地址、长度、当前元素个数。

链表:(1个参数)首地址(头指针)
注:
(1)头节点的数据结构,和其他的节点一样。
(2)所以我们知道 头指针,可以一个一个推算出下面节点的信息。

(2)节点的数据类型,该如何表示

每一个节点,都是一个数据类型
每一个节点当中,应该包含什么?应该有几个成员?

  • 有效数据
  • 指针(地址):指向下一个节点。

问题:
(1)通过指针,我们要找到后面一个节点,而不是仅仅是首地址。

int *p = (int *)0x10002000;
*p = 2; // 这样的解引用,是将整数 2 ,存放到了 0x10002000 地址开头的四个字节当中。

char *p = (char *)0x10002000;
*p = 2; // 这样的解引用,是将整数 2 ,存放到了 0x10002000 地址开头的 1 个字节当中。

我们每个节点的数据类型一样
所以我们指针,相当于指向了和自己数据类型一样的变量。

struct Node
{
	int data; // 数据域(本身可以特别复杂)

	struct Node *pNext; // 指针域
}

typedef struct Node
{
	int data; // 数据域(本身可以特别复杂)

	struct Node *pNext; // 指针域
}NODE, *PNODE;

(3)链表的分类

  • 单链表
    在这里插入图片描述

  • 双链表(每一个节点有两个指针域

在这里插入图片描述

  • 循环链表 能通过一个节点,找到任何一个节点
    在这里插入图片描述

  • 非循环链表

(4)链表的伪算法

遍历(找到节点,之后就可以,增加、删除、改变、等等)
查找
清空
销毁
求长度
排序
删除结点
插入节点

插入非循环节点的伪算法

把 q 指向的节点,插入 p 指向节点的后面
在这里插入图片描述

注意:指针和结构体的知识:
(1)指针 p :本身并没有指针域, p 指向的结构体才有指针域

p -> pNext ; // p 指向结构体,这样调用该结构体当中的成员

(2)实现的效果:p中的指针域,指向q。 q当中指针域指向下一个节点

p -> pNext = q; // p指向的结构体当中的指针域,指向q
q -> pNext =    // 发现已经丢失了一下个节点的地址
r = p -> pNext; // 先将下一个地址,存起来
p -> pNext = q;
q -> pNext = r; 
q -> pNext = p -> pNext; // 先将 q 的指针域,指向下一个节点
p -> pNext = q;

删除肺循环节点的伪算法

在这里插入图片描述

问题:导致内存泄漏(找不到第2个节点的地址,所以无法释放)

p -> pNext = p -> pNext -> pNext ;
free(p -> pNext) ; //错误:p -> pNext 已经发生改变

解决:

r = p -> pNext; // 先将第二个节点地址保存
p -> pNext = p -> pNext -> pNext ;
free(r); // 然后再释放

(5)基本算法实现

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

typedef struct Node
{
	int data; // 数据域

	struct Node *pNode; // 指针域
}NODE, *PNODE;

int main(void)
{
	//跨函数使用内存(动态内存)
	PNODE pHead = NULL;
	pHead = create_list(); // 创建一个非循环单链表,并将该链表的头节点地址赋值给 pHead 



	return 0;
}

(1)create_list

1、返回值:(是一个地址,头节点地址) PNODE
2、参数:不需要

流程:

1、首先生成一个头节点
2、由用户决定生成的节点个数
3、创建这些节点

不循环生成节点:非常麻烦,想要申请100个,就得写100次。

//获取用户信息
printf("请输入第 1 个节点的值: val= ");
scanf("%d",&val);
//生成第一个节点的地址为 pNew 
PNODE pNew1 = (PNODE)malloc(sizeof(NODE));
//将用户信息存放
pNew1 -> data = val;
//将这个节点的首地址,挂到上一个节点指针域
pHead -> pNext = pNew1;
// 每次创建,将尾结点的指针域指向 NUll
pNew1 -> pNext = NULL;

// 第二个节点
//获取用户信息
printf("请输入第 2 个节点的值: val= ");
scanf("%d",&val);
//生成第二个节点的地址为 pNew2 
PNODE pNew2 = (PNODE)malloc(sizeof(NODE));
//将用户信息存放
pNew2 -> data = val;
//将这个节点的首地址,挂到上一个节点指针域
pNew1 -> pNext = pNew2 ;
// 每次创建,将尾结点的指针域指向 NUll
pNew2 -> pNext = NULL;

循环生成节点:

for (i = 0; i < len; i++)
{
		//获取用户信息
printf("请输入第 1 个节点的值: val= ");
scanf("%d",&val);
//生成第一个节点的地址为 pNew 
PNODE pNew = (PNODE)malloc(sizeof(NODE));
//将用户信息存放
pNew -> data = val;
//将这个节点的首地址,挂到上一个节点指针域
pHead -> pNext = pNew;
// 每次创建,将尾结点的指针域指向 NUll
pNew -> pNext = NULL;
}

出现的问题:只有第一次可以创建成功,后面都失败
在这里插入图片描述
解决办法:自己构造一个,多余的指针,让这个指针始终指向尾结点

for (i = 0; i < len; i++)
{
		//获取用户信息
printf("请输入第 1 个节点的值: val= ");
scanf("%d",&val);
//生成第一个节点的地址为 pNew 
PNODE pNew = (PNODE)malloc(sizeof(NODE));
//将用户信息存放
pNew -> data = val;
//将这个节点的首地址,挂到上一个节点指针域
pTail -> pNext = pNew;
// 每次创建,将尾结点的指针域指向 NUll
pNew -> pNext = NULL;
// 将pTail 后移
pTail = pNew;
}

整体的创建实现

PNODE create_list()
{
	int len = 0;
	int i = 0;
	int val = 0;
	
	//生成一个头节点
	PNODE pHead = (PNODE) malloc(sizeof(NODE));
	if(NULL = pHead)
	{
	printf("分配失败\n");
	exit(-1);
	}
	PNODE pTail = pHead ;
	pHead -> pNext = NULL; // 尾结点的指针域永远为NULL
 	printf("请输入您需要生成的链表节点个数:len = ");
	scanf("%d",len);
	
	//数组的内存是连续的,所以可以直接 malloc
	//链表的内存是离散的,所以需要一个循环
	for (i = 0; i < len; i++)
	{
	//获取用户信息
	printf("请输入第 1 个节点的值: val= ");
	scanf("%d",&val);
	//生成第一个节点的地址为 pNew 
	PNODE pNew = (PNODE)malloc(sizeof(NODE));
	//将用户信息存放
	pNew -> data = val;
	//将这个节点的首地址,挂到上一个节点指针域
	pTail -> pNext = pNew;
	// 每次创建,将尾结点的指针域指向 NUll
	pNew -> pNext = NULL;
	// 将pTail 后移
	pTail = pNew;
	}

	return pHead; // 返回头指针
}
(2)traverse_list

1、返回值:不需要
2、参数:必须要,指定对哪一个链表进行遍历

void traverse_list(PNODE pHead)
{
	PNODE p = pHead -> pNext;
	
	while(NUll != p) //如果 p 不为空(下一个节点存在)
	{
	printf("%d\n", p-> data);
	// p++ ,移动到下一个节点
	p = p->pNext;
	}
	printf("\n");
}

(3)is_empty

参数: PNODE pHead 判断是哪一个链表
返回值:bool,判断是否成功

bool is_empty(PNODE pHead)
{
	if(NULL == pHead->pNext) //头节点的指针域为空
		return true;
	else
		return false;
}
(4)length_list

参数: PNODE pHead 判断是哪一个链表
返回值:int 返回长度

思路:遍历的时候计数即可

int length_list(PNODE pHead)
{
	PNODE p = pHead -> pNext;
	int cnt = 0;
	while(NUll != p) //如果 p 不为空(下一个节点存在)
	{
	cnt++; // 计数
	p = p->pNext;
	}
	return cnt;
}
(5)insert_list

参数:PNODE pHead 判断是哪一个链表
int pos:插入的位置
int val:插入的值 (输入型参数)
返回值:是否成功

bool insert_list(PNODE pHead,int pos,int val)
{
//  pos 的值必须合法,一共有 5 个节点,那么插入第 10 个就错误
int i = 0;
PNODE p = pHead;

//移动节点位置
// 当前节点不是尾结点 NULL!=p :说明如果移动到尾节点就停止移动

// 并且移动到第 pos-1 个节点之前
// 代数:pos=1时,向第一个节点之前插入,当前p指向头节点,不需要执行循环
// pos=2时,向第二个节点之前插入,所以需要执行一次。
while(NULL!=p && i<pos-1)
{
	i++;
	p = p->pNext;
}

// 如果pos的值不合法的时候
// 或者移动到尾结点之后,我们在此处进行返回
if(i>pos-1 || NULL==p)
	return false;
// 分配动态内存
PNODE pNew = (PNODE)malloc(sizeof(NODE));
if(NULL == pNew)
{
printf("insert 动态内存分配错误\n");
exit(-1);
}
// 保存用户数据
pNew -> data = val;
// 插入节点
	// 保存后面节点的地址
PNODE q = p->pNext;
	// 然后将前面的节点指向新申请的节点
p->pNext = pNew;
	// 将插入的节点指向后面的节点
pNew->pNext = q;
	return true;
}
(6)delete_list

参数:PNODE pHead 判断是哪一个链表
int pos:删除的位置
int *val:插入的值 (输出型参数)

bool delete_list(PNODE pHead,int pos,int val)
{
int i = 0;
PNODE p = pHead;
//移动节点位置
// 当前节点不是尾结点 NULL!=p :说明如果移动到尾节点就停止移动

// 并且移动到第 pos-1 个节点之前
// 代数:pos=1时,向第一个节点之前插入,当前p指向头节点,不需要执行循环
// pos=2时,向第二个节点之前插入,所以需要执行一次。
while(NULL!=p->Next && i<pos-1)
{
	i++;
	p = p->pNext;
}

// 如果pos的值不合法的时候
// 或者移动到尾结点之后,我们在此处进行返回
if(i>pos-1 || NULL==p->Next)
	return false;

//删除节点
	//保留当前节点地址
PNODE q = p->pNext;
	//输出删除节点的内容
*val = q->data;
	//更换指针域
p->pNext = p->pNext->pNext;
	//释放内存
free(q);
q = NULL;
	return true;
}
(7)sort_list

参数:PNODE pHead 判断是哪一个链表
返回值: 无

数组和链表的探讨

不同点:一个是连续的,另一个是离散的。
相同点:都是线性结构,算法严格来说是一样的(逻辑上)
比如:都可以使用冒泡来进行排序。(第一个依次和后面比较)

在这里插入图片描述

void sort_list(PNODE pHead)
{
	int i, j = 0;
	int tmp = 0;
	int len = length_list(pHead);
	PNODE p,q; // p相当于i, q相当于j
	
	for(i=0,p=pHead->pNext; i < len-1; ++i, p=p->pNext) 
	{
		for(j=i+1, q = p->pNext ; j <len ; ++j , q= q->pNext)
			{
				if(p->data > q->data ) // 把小的换到前面来
					{
						tmp = p->data;
						p->data = q->data;
						q->data = tmp;
					} 
			}
}

补充泛型的初步定义:

算法:(数组和链表的算法一样吗?)

狭义的算法:与数据的存储方式,密切相关
广义的算法:与数据的存储方式,不相关

泛型:
利用某种技术:达到不同的存储方式,执行的操作是一样的。

举例:运算符的重载

p++:我们可以将 ++ 运算符进行重载。(为 ++ 重写一个函数)
所以广义上来说就实现了泛型

3、线性结构的应用 —— 栈

(1)序言

静态内存:分配在
局部变量
栈序:先进后出

动态内存:分配在
malloc 函数
堆序:堆排序

栈的定义:事先 “先进后出” 的数据结构。(类似于一个杯子)

栈的分类:

静态栈:是用数组来实现
注意:静态栈必须提前确定栈的大小(有限的),并且都是连续的.

动态栈:是用链表来实现.
动态栈可以无限大小(内存够的情况下),并且是不连续的.

分析栈和链表的区别:
栈只能在栈顶进入,或者是栈顶删除。

栈的算法:

出栈:
入栈:

(2)栈的算法实现

在这里插入图片描述
pBottom:指向头节点(里面并不存放有效数据),栈底元素的下一个
pTop :指向尾结点

删除元素:pTop 向上移动
插入元素:pTop 向下移动

判断空栈:pTop == pBottom

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

// 定义每个节点的数据类型
typedef struct Node
{
	int data; // 数据域
	struct Node *pNext; // 指针域
}NODE, *PNODE;

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

int main(void)
{
	STACK S; // 等价于 struct Stack  S
// 初始化栈指针
	initStack(&S); // 一定要思考传入什么参数
// 压栈
	push(&S,1);
	push(&S,2);
// 遍历输出
	traverse(&S);
}

(1)init
// 数入之前:栈指针都是垃圾值
// 1、如果我们的 栈顶指针和栈底指针 都指向一个头节点(无用),才能说明我们构造了一个空栈
// 
void init(PSTACK PS)
{
	PS -> pTop = (PNODE)malloc(sizeof(NODE));
	if(NULL == PS -> pTop)
	{
		printf("init 动态分配出错\n");
		exit(-1);
	}
	else
	{
		PS -> pBottom = PS -> pTop;
		// 将头节点的指针域清空
		PS -> pTop ->pNext = NULL;
	}
}
(2)push

在这里插入图片描述

// 1、新申请一个节点
// 2、入栈的指针域,指向 上一个节点
// 3、栈顶指针,指向新malloc 的节点
// 4、
void push(PSTACK PS, int val)
{
	PNODE pNew = (PNODE) malloc(sizeof(NODE));
	pNew -> pdata = val;
	// 新申请的节点的指针域指向上一个节点,也就是 pTop 指向的节点
	pNew -> pNext = PS->pTop; 
	// 栈顶指针,指向新malloc 的节点
	PS->pTop = pNew;
}
(3)traverse

在这里插入图片描述

// 1、先输出 88 

// 思考:
// 1、我们不能改变栈顶指针,和栈底指针
// 2、所以我们定义一个临时的指针p ,指向栈顶元素
// 3、p 指针一个一个向下移动
// 4、p == pBottom 的时候,遍历完成
void traverse(PSTACK PS)
{
	PSTACK p = PS->pTop;
	
	while(p != PS->pBottom)
	{
		printf("%d ",p->data);
		p = p->pNext;
	}
return ;
}
(4)pop
// 1、保存栈顶节点的地址,然后释放
// 2、pTop 向下移动
// 3、将下一个节点的指针域,指向NULL


bool pop(PSTACK PS,int *val)
{
//栈为空
	if(PS -> pTop == PS -> pBottom)
	{
		return false;
	}
// 1、保存栈顶节点的地址,然后释放
	PNODE p =  PS ->pTop;
	*val = p -> data;
	free(p);
// 2、pTop 向下移动
	PS ->pTop = PS ->pTop -> pNext;
// 将下一个节点的指针域,指向NULL
	PS ->pTop -> pNext = NULL;
}
(5)clear
// 将栈中元素清零,然后节点还在
// 遍历一次,然后清除元素
void clear(PSTACK PS)
{
	if (empty(PS)) // 如果为空栈
{
	return ;
}
	PSTACK p = PS->pTop;
	
	while(p != PS->pBottom)
	{
		p->data = 0;
		p = p->pNext;
	}
return ;
}

// 将栈当中的节点释放
// 遍历一次元素,将其释放
// 但是栈顶指针,和栈底指针都必须留下,留下框架
void clear(PSTACK PS)
{
	if (empty(PS)) // 如果为空栈
	{
		return ;
	}
	else
	{
		PNODE p = PS->pTop;
		PNODE q = NULL;
		while(p !=  PS->pBottom) 
		{
			q = p->pNext;
			free(p);
			p=q;
		}
	}
	return ;
}

在这里插入图片描述

(3)栈的应用

1、函数的调用

int f ()
{
	int a,b = 0;
	g(&a;&b);
	
	printf("hello world\n");
}

分析:在 f() 函数当中调用g() 函数的时候,参与栈的使用。

将指令 printf("hello world\n"); 的地址压入栈
将参数所用的局部变量 int a,b = 0; 也压栈

2、中断

3、表达式求值
在这里插入图片描述
4、内存分配
5、缓冲处理
6、迷宫

4、线性结构的应用—— 队列

(1)序言

栈:我们讲的是动态队列,(本质还是链表)

什么是队列?
一种可以实现 “先进先出” 的存储结构。

链表的分类?

链式队列:本质是链表
静态队列:本质是数组

(2)静态队列的分析:

1、静态队列为什么必须是循环队列?
2、循环队列,需要几个参数来确定?
3、循环队列,各个参数的含义?
4、循环队列,入队伪算法讲解
5、循环队列,出队伪算法讲解
6、如何判断循环队列为空?
7、如何判断循环队列为满?

1、静态队列为什么必须是循环队列?
在这里插入图片描述
分析:

rear:用来添加元素的指针(向上移动)
front:用来删除元素的指针(向上移动)

发现一个问题:指针都是向上移动,内存总有一天会崩溃。(而且使用数组的时候,数组的大小是固定的)

解决办法:循环队列

2、循环队列,需要几个参数来确定?

  • 需要两个参数:front 指针和 rear 指针。

3、循环队列,各个参数的含义?

  • 2个参数不同的场合有着不同的含义
    在这里插入图片描述

(1)队列初始化:front 和 rear 的值都是

(2)队列空时:front 和 rear 的值相等,但不一定是

(3)队列为空:front代表第一个元素,rear代表最后一个元素的下一个元素。

4、循环队列,入队伪算法讲解

入队:在队尾加入
出队:在队头弹出

在这里插入图片描述

5、循环队列,出队伪算法讲解

在这里插入图片描述
6、如何判断循环队列为空?

如果,front 和 rear值相等,则该队列就一定为空。

7、如何判断循环队列已满?
在这里插入图片描述
1、可以发现,f 和 r 的值可以是任意值

2、可以看出当我们队列已经满了的时候,指针 p 和 指针 r 是相等的,所以和我们的,队列已空发生了冲突。

解决办法:

1、少使用一个元素
则:当 指针p指针r 互相临近,则队列已经满了。
因为:f 和 r 的值可以是任意值,所以当 r = 4 的时候,f 可以变为 f = 3,f = 5。(其中有一种情况是队列只有一个元素)
但是,入队的时候是,r + 1,所以说当 (r+1) % 数组的长度 == f 的时候,队列就是满的,另外一种情况排除。

2、添加一个元素
原理和少用一个元素是相同的

在这里插入图片描述

(3)循环队列程序演示

#include <stdio.h>

typedef struct queue
{
	int *pBase; // 数组的基地址
	int front; // 作为数组元素的下标
	int rear;  

}QUEUE,*QUEUE;


int main(void)
{
	QUEUE Q;
	
	init(&Q);
	
	return 0;
}
(1)init
//目的:
//1、创建一个数组
//2、下标进行初始化

void init(QUEUE *pQ)
{	
	// 6 个元素大小
	pQ -> pBase = (int*)malloc(sizeof(int)*6 );
	pQ -> front = 0;
	pQ -> rear = 0;
}
(2)en_queue

注意:判断是否已满就为少用一个元素,创造了条件


// 入队:如果队列不满
// 1、将值放在 r 当前的位置
// 2、将 r 移动到下一个位置

bool en_queue(QUEUE *pQ , int val)
{
	if( full_queue(pQ) )
	{
		return false;
	}
	else 
	{
		// 1、将值放在 r 当前的位置
		pQ->pBase[pQ->rear] = val;
		// 2、将 r 移动到下一个位置
		pQ->rear = (pQ->rear+1) % 6;
	}
}
(3)full_queue
bool full_queue(QUEUE *pQ)
{
	// 回顾伪算法
	if( (pQ->rear + 1)%6 == pQ->front )
		return true;
	else
		return false;
}
(4)traverse_queue

void traverse_queue(QUEUE *pQ)
{
	int i = pQ -> front;

// i != pQ->rear  时候,有东西要输出
	while(i != pQ->rear)
	{
		printf("%d ",pQ->pBase[i]);
		i = (i+1) % 6; // 开始循环
	}
}
(5)out_queue
// 出队:删除一个元素
// 1、先将值进行输出
// 2、指针 f 向上移动
bool out_queue(QUEUE *pQ , int *val)
{
	if( empty_queue() )
		return false;
	else
	{
		// 1、先将值进行输出
		*pVal = pQ->pBase[pQ->front];
		// 2、指针 f 向上移动
		pQ->front = (pQ->front + 1) %6;
	}	
}
(6)empty_queue
bool empty_queue( QUEUE *pQ )
{
	if(pQ->rear == pQ->front )
		return true;
	else
		return false;
}

(4)队列的具体的应用

1、所以和时间有关的操作,都与队列相关。

比如:任务,先进先执行

递归专题

1、前景知识

递归定义:不同函数之间的,相互调用

#include <stdio.h>

void f();
void g();
void k();

void f()
{
	printf("FFFF\n");
	g();
	printf("1111\n");
}
void g()
{
	printf("GGGG\n");
	k();
	printf("2222\n");
}
void k()
{
	printf("KKKK\n");
}

int main(void)
{
	f();
	return 0;
}

在这里插入图片描述

自己调用自己

1、死递归:不知道什么停止调用自己
2、递归:一定要知道,自己什么时候停止调用自己。

#include <stdio.h>
// 死递归
void f()
{
	printf("1111\n");
}

int main(void)
{
	f();
	return 0;  
}

// 不是死递归
void f(int n)
{
	if(n==1)
		printf("1111\n");
	else
		f(n-1);
}

int main(void)
{
	f();
	return 0;  
}

2、应用举例

(1)求阶乘

n! = n x (n-1)!

(1)使用循环来实现

#include <stdio.h>

int main(void)
{
	int val;
	int i=0;
	int mul = 1;
	printf("请输入一个数字:val = ");
	scanf("%d ",&val);
	
	for(i=1, i<=val; i++)
	{
		mul = i * mul;
	}

printf("%d 的阶乘是 %d\n", val, mul);
	return 0;
}

(2)使用递归来实现

#include <stdio.h>

// 1、出入一个数
// 2、返回这个数的阶乘
long f(long val)
{
	// f(1) 肯定可以实现
	if(1==n)	
		return 1;
	// f(n) 要借助 f(n-1) 来实现
	else
		return f(n-1) * n;
}

int main(void)
{
	printf("%d \n",f(5));
	return 0;
}

递归的思想
规模很大的问题的解决,是借助于规模很小问题解决,当最后规模最小的问题,不需要再借助其他解决办法的时候,再倒推回来。

求:100! -> 99! ->98! …-> … …-> 1!
所以,先求 1!,然后再反推。

1、n 规模问题的解决,可以借助 (n-1) 规模问题的解决而解决。
举例:求 100!,我们知道 99!,就可以轻易的得到 100!

(2)求 1+2+3+4+…+100

//

long f(long n)
{
//	规模最小的时候 n==1
	if(1==n)
		return 1;
// 剩下的规模,需要借助 n-1 的规模来解决	
	else
		return f(n-1) + n;
}

int main(void)
{
	printf("%d \n",f(5));
	return 0;
}

3、对递归的理解

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

(1)函数调用:

当在一个函数的运行期间调用另一个函数时,在运行被调函数之前,系统需要完成三件事

1、将所有的实际参数返回地址等信息传递给被调函数保存。
返回地址:下一条语句的地址

2、为被调函数的局部变量也包括行参)分配存储空间。

3、将控制转移到被调函数的入口。

从被调函数返回函数之前,系统也要完成三件事:

保存被调函数的返回结果。
保存 return 的值

释放被调函数所占的存储空间。

依照被调函数保存的返回地址将控制转移到调用函数。

举例1:

#include <stdio.h>

int f(int n)
{
	int i,j;
	n = n+2;
	return n;
}
// f 返回函数,需要做的事情
1、保存 n 的值
2、释放所有形参、局部变量,
3、根据保存地址,返回主调函数

int main(void)
{
	int val;
	
	val = f(5);
	printf("val = %d\n",val);
	
	return 0;
}

// 在 main 函数当中调用 f() 函数
1、将实参 5 ,下一个语句地址 printf 的地址,
2、为形参 n 分配空间
3、控制权限转移

A函数调用A函数,和A函数调用B函数,在计算机看来是没有任何区别的,只不过用我们日常的思维方式理解比较怪异而已!

(2)函数调用,栈的使用

当有多个函数相互调用时,按照”后调用先返回“的原则,上述函数之间信息传递和控制转移必须借助”“来实现。

即系统将整个程序运行时所需的数据空间安排在一个栈中,每当调用一个函数时,就在栈顶分配一个存储区,进行压栈操作,每当一个函数退出时,就释放它的存储区,就做出栈操作,当前运行的函数永远都在栈顶位置。

(3)递归调用,必须满足的3个条件

  • 递归必须得有一个明确的中止条件

  • 该函数所处理的数据规模必须在递减(递归的值,可以递增)

// 值在减小、规模也在减小
int f(int n)
{
	if(n<3)
		printf("结束\n")// 递归结束
	else
		n = f(n-1);  // 在此递归
return n;
}

// 值在增大、规模却在减小
int f(int n)
{
	if(n>7)
		printf("结束\n")// 递归结束
	else
		n = f(n+1);  // 在此递归
return n;
}
  • 这个转化必须是可解的

(4)循环的递归的关系

(1)理论上讲,所有的循环都可以转化为递归。但是用递归能解决的问题,不一定用循环可以解决。

(2)递归和循环的特点

递归:易于理解、速度比较慢、占用存储空间大
优点:易于理解
缺点:调用函数,有很大的开销。

循环:不易理解、速度比较快、占用存储空间比较小

(5)汉诺塔

如下图所示,从左到右有A、B、C三根柱子,其中A柱子上面有从小叠到大的n个圆盘,现要求将A柱子上的圆盘移到C柱子上去,期间只有一个原则:一次只能移到一个盘子且大盘子不能在小盘子上面。
求移动的步骤和移动的次数
在这里插入图片描述
解:

(1)n == 1
第1次 1号盘 A---->C sum = 1 次

(2) n == 2
第1次 1号盘 A---->B
第2次 2号盘 A---->C
第3次 1号盘 B---->C sum = 3 次

(3)n == 3
第1次 1号盘 A---->C
第2次 2号盘 A---->B
第3次 1号盘 C---->B
第4次 3号盘 A---->C
第5次 1号盘 B---->A
第6次 2号盘 B---->C
第7次 1号盘 A---->C sum = 7 次

在这里插入图片描述

n=1; 1次
n=2; 3次
n=3; 7次
总结:一共是 2 的 n次方,减一。

// 当用户输入盘子个数的时候,我们就假设 A 上有n个盘子,并且从小到大排列好了

void hannuota(int n, char A, char B, char C)
{
	如果是一个盘子,
			直接将 A 柱子上的盘子移动到 C柱子上面
	否则
			先将 A 柱子上的 n-1 个盘子借助 C 移动到B上面
			直接将 A 柱子上的第 n 个盘子移动到上C上面
			最后将 B 柱子上面的n-1个盘子借助 A 移动到 C 上
}

// n:代表要移动盘子的总数
// A:代表准备要移动的柱子,(不一定是 A ,有可能是 B)
// B:代表移动过程中借助的柱子(不一定是 B,有可能是 A)
// C:代表要接收的盘子的柱子,(也不一定是C)

void hannuota(int n, char A, char B, char C)
{
	if(1==n)
	{
		printf("将编号为 n 的柱子,直接从 %c 柱子,移动到 %c的柱子上面\n",n,A,C);
	}
	else
	{
	//将 A 上面的 n-1 个盘子,借助 C 移动到 B 上面.
		hannuota(n-1,A,C,B);
	//将编号为 n 的盘子,移动到 C 上面
		printf("将编号为 n 的柱子,直接从 %c 柱子,移动到 %c的柱子上面\n",n,A,C);
	//
		hannuota(n-1,B,A,C);	
	}
}

总结:其实从宏观上来说,只需要 3 步(只是其中两步比较复杂)

1、A上的 n-1 个移动到 B 上 (比较复杂)
这个原理和(将 n 个圆盘从 A 移动到 C 是一样的)

2、A上的第 n 个移动到 C 上

3、B 上的 n-1 个移动到 C 上(比较复杂)
这个原理和(将 n 个圆盘从 A 移动到 C 是一样的)

在这里插入图片描述

就像将大象放到冰箱里,也需要3步,第二步,将大象放到冰箱比较复杂,但是打开冰箱,和关闭冰箱比较容易。

4、递归的应用

1、树和森林,就是以递归的方式来定义的。
2、树和图,很多算法就是以递归来实现的。
3、很多数学公式,就是以递归的方式来定义的。

斐波那契数列:1、2、3、5、8、13、21、34
每个数都是前两项相加。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

想文艺一点的程序员

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值