数据结构(郝斌老师)

本文详细介绍了数据结构的基础知识,包括数据结构的定义、栈和队列的概念及操作,以及递归的原理。栈是一种后进先出(LIFO)的数据结构,常用于函数调用和表达式求值;队列则是先进先出(FIFO)的数据结构,应用于任务调度和数据传输。递归是函数自身调用自身的技术,常用于解决树形结构和复杂问题。此外,文章还探讨了静态存储和动态存储的区别,以及衡量算法效率的时间复杂度和空间复杂度。
摘要由CSDN通过智能技术生成

目录

一.数据结构概述

1.定义:

2.什么是数据结构

3.栈内存与堆内存的区别是什么?

4.衡量算法的标准

5.数据结构的地位

二.预备知识

1.指针

2.结构体

三.线性结构(把所有结点用一根直线穿起来)

1.连续存储(数组)

2.离散存储(链表)

3.线性结构的两种常见应用----栈

4.线性结构的两种常见应用----队列

5.专题----递归

四.非线性结构

1.树

2.图

五.查找和排序

1.查找

2.排序

3.数据结构和泛型


一.数据结构概述

1.定义:

如何把现实中大量而复杂的问题以特定的数据类型(个体)特定的存储结构(个体间的关系)保存到主存储器(内存)中,以及在此基础上为实现某个功能(查找、删除、排序)而执行的相应操作,这个相应的操作也叫算法

2.什么是数据结构

数据结构 = 个体 + 个体间的关系

算法 = 对存储数据的操作

广义数据结构:数据存储(数据怎么去存?)+算法设计(存完后怎么操作?)

狭义数据结构:数据存储

3.栈内存与堆内存的区别是什么?

分配内存的方式不一样,核心还是算法。

4.衡量算法的标准

时间复杂度(研究侧重):程序大概要执行多少次,而非执行时间,主要看程序最核心的循环执行了多少次

空间复杂度(研究侧重):算法执行过程中大概所占用的最大内存

难易程度(实用侧重):是否易懂

健壮性(实用侧重):是否安全、正确、可移植

问:为什么时间复杂度看重的是程序的执行次数而不是程序运行所用的时间?

答:同一个程序,在不同性能的计算机上,运行所用的时间可能存在明显差异。在性能优异的计算机上执行花费时间为1s,在性能低劣的计算机上运行花费时间可能就是10s,因此时间复杂度看重的是程序的执行次数而不是程序运行所用的时间。

5.数据结构的地位

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

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

二.预备知识

1.指针

指针的重要性:指针是C语言的灵魂

1.1.定义

指针就是地址,地址就是指针

指针变量是存放内存单元地址的变量

指针的本质就是一个操作受到限制的非负整数,只能支持赋值操作,不支持加减乘除等一系列算数操作。

内存的编号不得重复,但是内存编号所对应的内存空间单元里存储的值,可以重复。

1.2.地址

1.内存单元的编号

2.从0开始的非负整数

3.范围(64位操作系统,16G内存条):0~~~~2的34次方减一

2.结构体

三.线性结构(把所有结点用一根直线穿起来)

1.连续存储(数组)

1.1.什么叫数组

元素类型相同,大小相等

1.2.数组的优缺点

1.3.数组数据结构案例:

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

//定义了一个数据类型,名字叫做struct Arr,里面有三个成员
struct Arr {
	int * pBase;//存储的是数组元素的第一个值
	int len;//数组所能容纳的最大元素个数
	int count;//当前数组有效元素的个数
};

//1.数组初始化
void init_Arr(struct Arr * arr,int length);
//2.判断是否空数组
bool isEmpty(struct Arr * arr);
//3.判断数组是否已满
bool isFull(struct Arr * arr);
//4.数组元素追加
bool appeng_Arr(struct Arr * arr,int val);
//5.展示数组的值
void show_Arr(struct Arr * arr);
//6.在指定位置向数组中插入元素 
bool insert_Arr(struct Arr * arr,int pos,int val);
//7.根据索引删除数组中得元素
bool delete_Arr(struct Arr * arr,int pos,int * qq);
//8.根据索引取值 
int get(struct Arr * arr,int pos);
//9.数组倒置
void inversion_Arr(struct Arr * arr);
//10.数组排序
void sort_Arr(struct Arr * arr);

int main(void) {
	struct Arr arr;
	int q;
	//1.初始化数组
	init_Arr(&arr,9);
	//4.数组追加元素
	appeng_Arr(&arr,-1);
	appeng_Arr(&arr,8);
	appeng_Arr(&arr,79);
	appeng_Arr(&arr,-99);
	appeng_Arr(&arr,55);
	appeng_Arr(&arr,709);
	appeng_Arr(&arr,-199);
	appeng_Arr(&arr,5); 
	show_Arr(&arr);
	//7.根据索引删除数组中得元素
	delete_Arr(&arr,2,&q);
	printf("被删除值为:%d\n",q); 
	show_Arr(&arr);
	//9.数组倒置 
	inversion_Arr(&arr);
	show_Arr(&arr);
	//10.数组排序
    sort_Arr(&arr);
    show_Arr(&arr);
    
    int a = get(&arr,2);
    printf("数组第2值为:%d\n",a);
	return 0;
}

//1.数组初始化
void init_Arr(struct Arr * arr,int length) {
    //向操作系统申请分配内存
	arr->pBase=(int *)malloc(sizeof(int) * length);
	if(NULL == arr->pBase) {
		printf("动态分配内存失败!");
		exit(-1);
	} else {
		//当前数组的长度为传入的长度参数
		arr->len = length;
		//当前数组有效元素个数为0
		arr->count = 0;
	}
	return;

}

//2.判断是否空数组 
bool isEmpty(struct Arr * arr) {
	if(0 == arr->count) {
		return true;
	}
	return false;
}

//3.判断数组是否已满
bool isFull(struct Arr * arr) {
	if(arr->len <= arr->count) {
		return true;
	}
	return false;
}

//4.数组元素追加
bool appeng_Arr(struct Arr * arr,int val) {
	if(isFull(arr)) {
		printf("追加失败,数组下标越界!");
		return false;
	} else {
		//赋值
		arr->pBase[arr->count] = val;
		//有效个数+1
		(arr->count)++;
		return true;
	}
}

//5.展示数组的值
void show_Arr(struct Arr * arr) {
	if(isEmpty(arr)) {
		printf("数组为空");
		return;
	} else {
		for(int i=0; i < arr->count; i++) {
			printf("%d ",arr->pBase[i]);
		}
		printf("\n");
	}

}

//6.在指定位置向数组中插入元素
bool insert_Arr(struct Arr * arr,int pos,int val) {
	int i;
	if(isFull(arr)) {
		return false;
	}
	if(pos<1 || pos>arr->count+1) {
		return false;
	}
	for(i= arr->count-1; i>=pos-1; --i) {
		//赋值
		arr->pBase[i+1] = arr->pBase[i];
	}
	arr->pBase[pos-1] = val;
	(arr->count)++; 
	return true;

}

//7.根据索引删除数组中得元素
bool delete_Arr(struct Arr * arr,int pos,int *qq){
	int i;
	if(isEmpty(arr)) {
		return false;
	}
	if(pos<1 || pos>arr->count) {
		return false;
	}
	*qq = arr->pBase[pos-1];
	for(i=pos;i<arr->count;++i){
		arr->pBase[i-1] = arr->pBase[i];
		pos++;
	}
	(arr->count)--;
	return true;
}

//8.根据索引取值  
int get(struct Arr * arr,int pos){
	int i = arr->pBase[pos-1];
	return i; 
} 

//9.数组倒置
void inversion_Arr(struct Arr * arr){
	int i = 0;
	int j = arr->count-1;
	int t = 0;
	while(i<j){
	t = arr->pBase[i];
	arr->pBase[i] = arr->pBase[j];
	arr->pBase[j] = t;
	i++;
	j--;
	}
	return;
} 
 
//10.数组排序
void sort_Arr(struct Arr * arr){
	int i,j,t;
	for(i=0;i<arr->count;++i){
		for(j=i+1;j<arr->count;++j){
			if(arr->pBase[i]>arr->pBase[j]){
				t = arr->pBase[i];
	            arr->pBase[i] = arr->pBase[j];
	            arr->pBase[j] = t;
			}
		}
	}
	return; 
} 

2.离散存储(链表)

2.1.typedef的用法

案例一

代码:

#include<stdio.h>

//为int另外多取了一个 名字,在这个程序中zcj等价于int 
typedef int zcj;

//为struct Student*另外多取了一个名字为ST,在这个程序中ST等价于struct Student*
typedef struct Student{
	int id;
	char sex;
} * ST; 

int main(void){
	
	int a = 10;
	zcj z = 10;
	printf("a=%d,z=%d\n",a,z);
	
	struct Student student; 
	ST st = &student;
	st->id=100;
	printf("st中id = %d\n",st->id);
	return 0;
} 

运行结果:

a=10,z=10
st中id = 100

案例二

代码:

#include<stdio.h>
/*
为struct Student*取了一个名字为PST,在这个程序中PST等价于struct Student*
为struct Student取了一个名字为ST,在这个程序中ST等价于struct Student
*/
typedef struct Student{
	int id;
	double number;
} * PST,ST; 

int main(void){
	ST st;
	st.number = 123;
	printf("st中number = %lf\n",st.number);
	PST pst = &st;
	pst->id=100;
	printf("pst中id = %d\n",pst->id);
	return 0;
} 

运行结果:

st中number = 123.000000
pst中id = 100

2.2.定义

1.n个节点离散分配

2.彼此通过指针链接

3.每个节点只有一个前驱节点,每个节点只有一个后续节点

4.首个节点没有前驱节点,尾节点没有后驱节点

2.3.几个专业术语

1.首节点:第一个有效节点

2.尾节点:最后一个有效节点

3.头结点:第一个有效节点之前的那个节点,头结点不存放有效数据,头结点的存在的意义在于为了方便对链表的操作,并且头结点的数据类型和首节点的数据类型必须一致。

4.头指针:指向头结点的指针变量

5.尾指针:指向尾节点的指针变量

2.4.链表核心参数

问:如果希望通过一个函数来对链表进行处理,那么函数至少需要接收哪些关于链表的参数?

答:一个参数----即链表的头指针,原因在于可以通过头指针推算出链表的所有其他参数。

2.5.链表节点定义

代码

#include<stdio.h>

typedef struct Node{
	int data;//数据域,这一部分可以很复杂
	struct Node * pNext;//指针域 
}NODE,*PNODE;//NODE等价于struct Node,PNODE等价于struct Node *
int main(void){
	return 0 ;
}

2.6.链表的分类

2.6.1.单链表与双链表

1.单链表

2.双链表:每一个节点都有两个指针域

2.6.2.循环链表与非循环链表

1.循环链表:通过任何一个节点能够找到其他所有节点

2.非循环链表

2.7.链表相关算法

2.7.1.链表算法(代码)

a.创建链表

b.遍历链表

c.判断链表是否为空

d.链表排序

e.计算链表的长度

f.在指定位置插入一个结点

g.删除链表中的某个结点

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

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

//创建链表 
PNODE create_List(void);
//遍历链表 
void traverse_List(PNODE pHead);
//判断链表是否为空 
bool is_empty(PNODE pHead);
//链表排序 
void sort_list(PNODE pHead); 
//计算链表的长度 
int length_List(PNODE pHead); 
//在指定位置插入一个结点 
bool insert_List(PNODE,int,int);
//删除链表中的某个结点 
bool delete_List(PNODE,int,int *);

int main(void){
	PNODE pHead = NULL;
	//创建一个非循环单链表,并将该链表的头结点的地址值赋给pHead 
	pHead = create_List();
	traverse_List(pHead);
	sort_list(pHead);
    /*
    这个插入元素的算法还存在缺陷,如果链表的长度为2,那么在第4个结点插数据就会失败
	另外,如果将插入结点的值改为负数,如:
	   insert_List(pHead,-4,33);
	需要进行提示输入结点的位置值不合法 
	*/ 
	insert_List(pHead,4,33);
	
	//删除 
	int val;
	if(delete_List(pHead,1,&val)){
		printf("删除成功,您删除的元素是:%d\n",val);
	}else{
		printf("删除失败,您删除的元素不存在\n");
	}
	//遍历排序后的链表 
	traverse_List(pHead);
	if(is_empty(pHead)){
		printf("链表为空!\n");
	}else{
		printf("链表不为空!\n");
	}
	
	int a = length_List(pHead);
	printf("链表长度为%d",a);
	return 0;
}

PNODE create_List(void){
	int len;//用来存放有效结点个数 
	int i;
	int val;//用来临时存放用户输入的结点的值
	
	//分配了一个不存放有效数据的头结点 
	PNODE pHead = (PNODE)malloc(sizeof(NODE));
	if(NULL == pHead){
		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(NULL == pNew){
			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(NULL != p){
		printf("%d\t",p->data);
		p = p->pNext;
	}
	printf("\n");
	return;
}

bool is_empty(PNODE pHead){
	if(NULL==pHead->pNext){
		return true;
	}else{
		return false;
	}
}

int length_List(PNODE pHead){
	PNODE p = pHead->pNext;
	int len = 0;
	while(NULL != p){
		++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 -1 ;++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; 
			}
		}
	}
}

/*
在pHead所指向链表的第pos个结点前面插入一个新的节点,该结点的值为val,并且pos的值是从1开始的 
*/ 
bool insert_List(PNODE pHead,int pos,int val){ 
	int i = 0;
	PNODE p = pHead;
	while(NULL != p && 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("动态内存分配失败");
		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;
	}
	//pos-1的意义在于,如果是删除第4个结点,那么必须先找到前一个即第3个结点 
	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; 
}

2.7.2.算法

狭义的算法是与数据的存数方式密切相关

广义的算法是与数据的存储方式无关

泛型:利用某种技术达到的效果为----不同的存数方式,执行的操作是一样的。即在某个函数的外面看,这个操作相同,但是底层因为函数重载的原因,其实是存在区别的。

看懂程序的三步:流程----功能----试数

看不懂就背会

3.线性结构的两种常见应用----栈

动态分配是存在堆内存中分配(程序员手动建立 ),静态分配是在栈内存中分配(操作系统来分配)

栈和堆表示分配空间的一种方式,静态的、局部的变量是以压栈或者出栈的方式分配内存的,动态的内存是以一种堆排序的方式分配的内存,栈区和堆区分配内存的方式不一样

3.1.定义

一种可以实现“先进后出”的存储结构,类似于箱子,先放进箱子的东西后拿出来

3.2.分类

静态栈

动态栈

3.3.算法

出栈

入栈(压栈)

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

typedef struct NODE{
	int data;
	struct NODE * pNext;
} NODE,* PNODE;

/*
当pTop和pBottom同时指向一个无用的头结点,那么才算是将这个空栈构建成功 
*/
typedef struct Stack{
	PNODE pTop;
	PNODE pBottom;
} STACK,* PSTACK;

//这里可以不写形参 
//初始化 
void init(PSTACK);
//push一个结点 
void push(PSTACK,int);
//遍历栈中结点 
void traverse(PSTACK);
//判断栈是否为空 
bool empty(PSTACK);
//出栈 
bool pop(PSTACK,int*);
//清空栈的数据,保留栈结构 
void clear(PSTACK);

int main(void){
	STACK S;
	
	init(&S);//建造出一个空栈 
	push(&S,1);//压栈。不需要指定位置,也不允许指定位置 
	push(&S,2);
	push(&S,6);
	push(&S,24);
	push(&S,28);
	push(&S,92);
	
	int val;
	
	traverse(&S);//遍历输出 
	
	clear(&S);
	printf("?\n");
	traverse(&S);//遍历输出 
	printf("?\n");


	if(pop(&S,&val)) {
		printf("出栈成功%d\n",val);
	}else{
		printf("出栈失败");
	}	
	
	return 0; 
}

/*
当pTop和pBottom同时指向一个无用的头结点(这个头结点的数据域没有值,指针域为NULL),那么才算是将这个空栈构建成功 
*/
void init(PSTACK pS){
	 pS->pTop = (PNODE)malloc(sizeof(NODE));
	 if(NULL == pS->pTop){
	 	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\n",p->data);
		p = p->pNext;
	}
	printf("\n");
	return;
}

bool empty(PSTACK pS){
	if(pS->pTop == pS->pBottom){
		return true;
	}else{
		return false;
	}
}

//把pS所指向的栈出栈一次,并把出栈的元素存入pVal形参所指向的变量中
//如果出栈失败,返回false,否则返回true 
bool pop(PSTACK pS,int* pVal){
	//pS本身 存放的就是S的地址 
	if(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(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;
	}
}

3.4.应用

1.函数调用

在a函数中调用了b函数,b函数中又调用了c函数,出栈顺序为:c---->b---->a,典型的先进后出。

2.中断

3.表达式求值、

例如:计算2+5*6-8

用两个栈完成加减乘除的运算,一个栈区放数字2、5、6、8这些数字,另外一个栈区放+、-、*、/这几个符号。

4.内存分配

5.缓冲处理

6.走迷宫

4.线性结构的两种常见应用----队列

4.1.定义

一种可以实现“先进先出”的存储结构,类似于排队买票。

4.2.分类

链式队列

用链表实现的队列

静态队列

1.定义

静态队列是用数组实现的队列,静态队列通常都必须是循环队列

2.循环队列的讲解:

无论是删除还是添加,指针都是向上走的,即无论是入队还是出队,指针都是向上的

2.1.静态队列为什么必须是循环队列?

2.2.循环队列需要几个参数来确定?

需要两个参数确定,front和rear;

2.3.循环队列各个参数的含义

2个参数在不同的场合有不同的意义

建议初学者先记住,后面慢慢体会

a.队列初始化

front和rear的值都是0

b.队列非空

front代表的是队列中的第一个元素,rear代表的是队列中最后一个元素的下一个元素

c.队列空

front和rear的值相等,但不一定是0

2.4.循环队列入队伪算法讲解

将入队元素放在rear的位置,然后rear的位置往后移动一位

2.5.循环队列出队伪算法讲解

2.6.如何判断循环队列是否为空?

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

2.7.如何判断循环队列是否已满?

预备知识:

front的值有可能比rear的值大,front的值也有可能比rear的值小,甚至front的值会和rear的值相等

两种解决方式

A.多增加一个表标识参数

B.少用一个元素【推荐】----如果front和rear紧挨着,则表示队列已满

用C语言伪算法表示就是

if((rear+1)%(数组长度) == front){
    已满
}else{
    不满
}

3.队列的算法

主要就是入队和出队

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

typedef struct Queue{
	int * pBase;
	int front;
	int rear;
} QUEUE;

//队列算法只有两个----入队和出队 

//初始化队列 
void init(QUEUE*);
//入队 
bool en_queue(QUEUE *,int val);
//出队
bool out_queue(QUEUE *,int*);
//遍历队列 
void traverse_queue(QUEUE *);

bool full_queue(QUEUE *);

bool emput_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,444);
	en_queue(&Q,555);
	traverse_queue(&Q);
	
	if(out_queue(&Q,&val)){
		printf("出队成功:%d\n",val);
	}else{
		printf("出队失败!");
	}
	traverse_queue(&Q);
	return 0; 
}

void init(QUEUE* pQ){
	//数组长度为6 
	pQ->pBase = (int *)malloc(sizeof(int) * 6);
	pQ->front = 0;
	pQ->rear = 0;
}

bool full_queue(QUEUE *pQ){
	if((pQ->rear+1)%6 == pQ->front){
		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;
		return true;
	}
} 

bool emput_queue(QUEUE* pQ){
	if(pQ->front == pQ->rear){
		return true;
	}else{
		return false;
	}
}

bool out_queue(QUEUE* pQ,int* pVal){
	if(emput_queue(pQ)){
		return false;
	}else{
		*pVal = pQ->pBase[pQ->front];
		pQ->front = (pQ->front+1)%6; 
		return true;
	}
}

void traverse_queue(QUEUE* pQ){
	int i = pQ->front;
	while(i != pQ->rear){
		printf("%d",pQ->pBase[i]);
		i = (i+1)%6;
		printf("\n");
	}
	 
	return;
}
 

4.队列的应用

所有和时间有关的操作都有对列的影子。

5.专题----递归

5.1.定义:一个函数自己直接或者简介调用自己

5.2.举例:

1.求阶乘

循环实现

#include<stdio.h>

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

递归实现

#include<stdio.h>

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

int main(void){
	long c;
	printf("请输入一个数字:");
	scanf("%d",&c);
	long sum = f(c);
	printf("%d的阶乘是:%d",c,sum);
	return 0;
}

2.1+2+3+4+5+...+100的和

递归实现

#include<stdio.h>

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

int main(void){
	long c;
	printf("请输入一个数字:");
	scanf("%d",&c);
	long sum = f(c);
	printf("1+2+3+...+%d的和是:%d",c,sum);
	return 0;
}

3.汉诺塔

代码

#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,ch1,ch2,ch3);
	return 0;
} 

4.走迷宫

5.3.递归必须满足的三个条件

1.递归必须有一个明确的终止条件

2.该函数所处理的数据规模必须在递减

3.这个转化必须是可解的()

5.4.循环和递归的关系

所有用循环能解决的问题都能设计成递归,反之,是不一定行的

递归的特点

易于理解,速度慢,存储空间大,树的遍历用递归处理,否则很难处理

循环的特点

不易理解,速度快,几乎没有浪费空间

5.5.递归的应用

树和森林就是以递归的方式定义的

树和图的很多算法都是以递归来实现的

很多数学公式就是以递归的方式来实现的----斐波拉契序列

总结

栈和队列是一种特殊的线性结构

四.非线性结构

1.树

树的定义

树的分类

一般树:

任意一个节点的子节点的个数都不受限制,各个子树之间没有固定顺序,同一层子树之间可以互换位置

二叉树:

任意一个节点的子节点个数最多两个,且子节点的位置不可更改,同一层子树有严格的左子树与右子树位置上的区分且位置不可互换

二叉树的分类:

一般二叉树:略

满二叉树:在不增加树的层数的前提下,无法再多添加一个节点的二叉树就是满二叉树

完全二叉树:如果只是删除了满二叉树最底层最右边连续若干个节点(可以一个不删),这样形成的二叉树就是完全二叉树

森林:

n个互不相交的树的集合

树的存储

二叉树的存储

连续存储【完全二叉树】

优点:查找某个节点的父节点和子节点(也包括判断有没有子节点)速度很快

缺点:非常耗费内存空间,原因在于有很多为了将当前树补全为完全二叉树的空节点,如图,红色是树的有效节点,将这个树补全称为完全二叉树,需要很多蓝色的空节点来补全,浪费内存极大。

 链式存储

一般树的存储

双亲表示法:求父节点方便

孩子表示法:求子节点方便

 双亲孩子表示法:求父节点和子节点都很方便

二叉树表示法

把一个普通树转化成二叉树来存储

具体转换方法:

设法保证任意一个节点的左指针域指向它的第一个孩子,友指针域指向它的兄弟,只要能满足这个条件,就可以把一个普通树转换为二叉树

一个普通树转化的二叉树,一定没有右子树

 

森林的存储

先把森林转化为二叉树,再存储二叉树

二叉树操作

1.遍历

先序遍历(根左右)

先访问根节点,再先序访问左子树,再先序访问右子树

中序遍历(左根右)

中序遍历左子树,再访问根节点,再中序遍历右子树

后序遍历(左右根)

先中序遍历左子树,再中序遍历右子树,再访问根节点

练习

2.已知两种遍历求原始二叉树

通过先序和中序或者中序和后序,可以还原出原始二叉树,但是通过先序和后徐是无法还原出原始二叉树的

已知先序和中序,求后序

例1

例2

根据中序和后序求先序

例:

树的应用

1.树是数据库中数据组织一种重要形式

2.操作系统子父进程的关系本身就是一棵树

3.面向对象语言中类的继承关系本身就是一棵树

4.赫夫曼树

程序演示

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

struct BTNode{
	char data;
	struct BTNode *pLchild;//p是指针,L代表左,child代表孩子 
	struct BTNode *pRchild;
};

void PreTraverseBTree(struct BTNode *pT);
void InTraverseBTree(struct BTNode *pT);
void PostTraverseBTree(struct BTNode *pT);
struct BTNode * CreateBTree(void);

int main(void){
	//动态地造出了一个二叉树,如果是静止地造二叉树,那么函数终止后,造出地二叉树就释放掉了 
	struct BTNode * pT = CreateBTree();
	
	//1.先序遍历
	//PreTraverseBTree(pT);
	//2.中序遍历 
	InTraverseBTree(pT);
	//3.后序遍历 
	//PostTraverseBTree(pT);
	
	return 0;
}

/*
伪算法:
    先访问根节点
	再先序访问左子树
	再先序访问右子树 
*/ 
void PreTraverseBTree(struct BTNode *pT){ 
	if(NULL != pT){
		printf("%c\n",pT->data);
	if(NULL != pT->pLchild){
		PreTraverseBTree(pT->pLchild);
	}
	if(NULL != pT->pRchild){
		//pT->pLchild可以代表整个左子树
		PreTraverseBTree(pT->pRchild);
	}
  }	 
}

void InTraverseBTree(struct BTNode *pT){
	if(NULL != pT){
	if(NULL != pT->pLchild){
		InTraverseBTree(pT->pLchild);
	}
	printf("%c\n",pT->data);
	if(NULL != pT->pRchild){
		//pT->pLchild可以代表整个左子树
		InTraverseBTree(pT->pRchild);
	}
  }	 
}

void PostTraverseBTree(struct BTNode *pT){
	if(NULL != pT){
	if(NULL != pT->pLchild){
		PostTraverseBTree(pT->pLchild);
	}
	if(NULL != pT->pRchild){
		//pT->pLchild可以代表整个左子树
		PostTraverseBTree(pT->pRchild);
	}
	printf("%c\n",pT->data);
  }	 
}

//造一个写死地树,如果有能力,可以改成造一个可以动态添加节点地树,相对麻烦 
struct BTNode * CreateBTree(void){
	struct BTNode *pA = (struct BTNode *)malloc(sizeof(struct BTNode));
	struct BTNode *pB = (struct BTNode *)malloc(sizeof(struct BTNode));
	struct BTNode *pC = (struct BTNode *)malloc(sizeof(struct BTNode));
	struct BTNode *pD = (struct BTNode *)malloc(sizeof(struct BTNode));
	struct BTNode *pE = (struct BTNode *)malloc(sizeof(struct BTNode));
	
	pA->data = 'A';
	pB->data = 'B';
	pC->data = 'C';
	pD->data = 'D';
	pE->data = 'E';
	
	pA->pLchild = pB;
	pA->pRchild = pC;
	pB->pLchild = pB->pRchild = NULL;
	pC->pLchild = pD;
	pC->pRchild = NULL;
	pD->pLchild = NULL;
	pD->pRchild = pE;
	pE->pLchild = pE->pRchild = NULL;
	
	return pA; 
} 

2.图

五.查找和排序

排序和查找的关系----排序是查找的前提

1.查找

折半查找

2.排序

冒泡排序

插入排序

选择排序

快速排序

快速排序程序演示:

#include<stdio.h>

void QuickSort(int *a,int low,int high);
int FindPos(int *a,int low,int high); 

int main(void){
	int a[6]={5,6,2,-55,99,77};
	int i;
	QuickSort(a,0,5);
	
	for(i=0;i<6;i++){
		printf("%d\t",a[i]);
	}
	return 0;
} 

void QuickSort(int *a,int low,int high){
	int pos;
	if(low < high){
		pos = FindPos(a,low,high);
		QuickSort(a,low,pos-1);
		QuickSort(a,pos+1,high);
	}
}

int FindPos(int *a,int low,int high){
	int val = a[low];
	while(low < high){
		while(low<high && a[high]>=val){
			--high;
		}
		a[low] = a[high];
		while(low<high && a[low] <=val){
			++low;
		}
		//终止while循环之后low和high一定是相等的 
		a[high] = a[low];
	}
	a[low] = val;
	//high可以改为low,但是不能改为val、a[low]、a[high] 
	return high;
}

归并排序

3.数据结构和泛型

3.1.数据结构

数据结构研究的是数据的存储和数据的操作的一门学问,数据的存储分为两部分:个体的存储和个体关系的存储

从某个角度而言,数据的存储最核心的就是个体关系的存储,个体的存储可以忽略不计。

3.2.泛型

同一种逻辑结构,无论该逻辑结构物理存储是什么样子的,我们可以对它执行相同的操作,就叫泛型

  • 8
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值