3 线性结构
3.1 【数组】连续存储
1、什么叫做数组
元素类型相同,大小相等(数组传参,只要传进去首地址和长度就行)
2、数组的优缺点(相对于链表):
优点:
存取速度快
缺点:
事先必须知道数组的长度
插入删除元素很慢
空间通常是有限制的
需要大块连续的内存块
插入删除元素的效率很低
定义一个数组至少需要3个参数:首地址,长度,有效个数。
用指针自己定义一个数组:
# include <stdio.h>
# include <malloc.h> //包含了malloc函数
# include <stdlib.h> //包含了exit函数
//定义了一个数据类型,该数据类型的名字叫做struct Arr, 该数据类型含有三个成员,分别是pBase, len, cnt
struct Arr
{
int * pBase; //存储的是数组第一个元素的地址
int len; //数组所能容纳的最大元素的个数
int cnt; //当前数组有效元素的个数
};
void init_arr(struct Arr * pArr, int length); //分号不能省
bool append_arr(struct Arr * pArr, int val); //追加
bool insert_arr(struct Arr * pArr, int pos, int val); // pos的值从1开始,表示插入的位置是第几个元素,下标为0的是第一个元素
bool delete_arr(struct Arr * pArr, int pos, int * pVal); //pVal是返回的要删除的值
int get();
bool is_empty(struct Arr * pArr);
bool is_full(struct Arr * pArr);
void sort_arr(struct Arr * pArr);
void show_arr(struct Arr * pArr);
void inversion_arr(struct Arr * pArr);
int main(void)
{
struct Arr arr;
int val;
init_arr(&arr, 6);
show_arr(&arr);
append_arr(&arr, 1);
append_arr(&arr, 10);
append_arr(&arr, -3);
append_arr(&arr, 6);
append_arr(&arr, 88);
append_arr(&arr, 11);
if ( delete_arr(&arr, 4, &val) )
{
printf("删除成功!\n");
printf("您删除的元素是: %d\n", val);
}
else
{
printf("删除失败!\n");
}
/* append_arr(&arr, 2);
append_arr(&arr, 3);
append_arr(&arr, 4);
append_arr(&arr, 5);
insert_arr(&arr, -1, 99);
append_arr(&arr, 6);
append_arr(&arr, 7);
if ( append_arr(&arr, 8) )
{
printf("追加成功\n");
}
else
{
printf("追加失败!\n");
}
*/
show_arr(&arr);
inversion_arr(&arr);
printf("倒置之后的数组内容是:\n");
show_arr(&arr);
sort_arr(&arr);
show_arr(&arr);
//printf("%d\n", arr.len);
return 0;
}
void init_arr(struct Arr * pArr, int length)
{
pArr->pBase = (int *)malloc(sizeof(int) * length);//pArr指针变量所指向的结构体变量中的pBase成员
if (NULL == pArr->pBase)
{
printf("动态内存分配失败!\n");
exit(-1); //终止整个程序
}
else
{
pArr->len = length;
pArr->cnt = 0;
}
return;
}
bool is_empty(struct Arr * pArr)
{
if (0 == pArr->cnt)
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) ) //pArr就是Arr的地址,&Arr赋给pArr,结果还是Arr的地址
{
printf("数组为空!\n");
}
else
{
for (int i=0; i<pArr->cnt; ++i)
printf("%d ", pArr->pBase[i]); //int *,pArr->pBase表示pArr指向的主函数中的结构体变量中的pBase成员,
printf("\n");
}
}
bool append_arr(struct Arr * pArr, int val)
{
//满是返回false
if ( is_full(pArr) )
return false;
//不满时追加
pArr->pBase[pArr->cnt] = val;
(pArr->cnt)++;
return true;
}
bool insert_arr(struct Arr * pArr, int pos, int val)
{
int i;
if (is_full(pArr))
return false;
if (pos<1 || pos>pArr->cnt+1) //
return false;
for (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)
{
int i;
if ( is_empty(pArr) )
return false;
if (pos<1 || pos>pArr->cnt)
return false;
*pVal = pArr->pBase[pos-1];//把等待删除的元素赋给形参pval指向的主函数的val
for (i=pos; i<pArr->cnt; ++i)
{
pArr->pBase[i-1] = pArr->pBase[i];
}
pArr->cnt--;
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[i] = pArr->pBase[j];
pArr->pBase[j] = t;
++i;
--j;
}
return;
}
void sort_arr(struct Arr * pArr)
{
int i, j, t;
for (i=0; i<pArr->cnt; ++i)
{
for (j=i+1; j<pArr->cnt; ++j)
{
if (pArr->pBase[i] > pArr->pBase[j])
{
t = pArr->pBase[i];
pArr->pBase[i] = pArr->pBase[j];
pArr->pBase[j] = t;
}
}
}
}
3.2 【链表】离散存储
# include<stdio.h>
typedef int ZHANGSAN; //为int在重新多取一个名字,ZHANGSAN等价于int
typedef struct Student
{
int sid;
char name[100];
char sex;
}* PSTU, STU; //等价于ST代表了struct Student, PST代表了struct Student *
int main(void)
{
int i = 10; //等价于ZHANGSAN i = 10
ZHANGSAN i = 10; //等价于 int i = 10
STU st; //struct Student st;
PSTU ps = &st; //struct Student * ps = &st;
ps->sid = 99;
printf("%d\n", ps->sid);
return 0;
}
链表定义:
n个节点离散分配
彼此通过指针相连
每个节点只有一个前驱节点,每个节点只有一个后续节点
首节点没有前驱节点,尾节点没有后续节点。
专业术语:
首节点:
第一个有效节点
尾节点:
最后一个有效节点
头节点:
头结点的数据类型和首节点的类型一样没有存放有效数据,最最前面的,是在首节点之前的,主要是为了方便对链表的操作。
头指针:(指向头)
指向头节点的指针变量
尾指针:
指向尾节点的指针
(头结点有可能很大,占的内存可能大,假设我想造一个函数
输出所有链表的值,那你如果不用头指针类型做形参,那由于
不同链表的头节点不一样大小,这样就没办法找出形参)
确定一个链表需要几个参数:(或者说如果期望一个函数对链表进行操作
我们至少需要接收链表的那些信息???
只需要一个参数:头指针,因为通过它我们可以推出
链表的所有信息。
(链表的程序最好一定要自己敲出来)
分类:
单链表
双链表:
每一个节点有两个指针域
循环链表
能通过任何一个节点找到其他所有的节点
非循环链表
(java中变成垃圾内存则会自动释放,但是C和C++则不会,所以要手动释放,否则会引起内存泄露。delete等于free)
算法:
遍历
查找
清空
销毁
求长度
排序
删除节点
r = p->pNext;
p->pNext = p->pNext->pNext;
free(r);
插入节点
把q所指向的节点插到p所指向的节点后面
方法一:先临时定义一个指向p后面节点的指针r
r = p->pNext; //r指向p后面的那个节点
p->pNext = q;
q->pNext = r;
方法二:
q->pNext = p->pNext;
p->pNext = q;
自己定义一个链表
# include <stdio.h>
# include <malloc.h>
# include <stdlib.h>
typedef struct Node
{
//定义链表节点
int data; //数据域
struct Node * pNext; //指针域,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 pHead, int pos, int val); //在pHead所指向链表的第pos个节点的前面插入一个新的结点,该节点的值是val, 并且pos的值是从1开始
//PNODE pHead指向一个链表,int pos表示插入节点的位置,int val表示插入节点的值
bool delete_list(PNODE pHead, int pos, int * pVal); //删除链表第pos个节点,并将删除的结点的值存入pVal所指向的变量中, 并且pos的值是从1开始
void sort_list(PNODE); //对链表进行排序
int main(void)
{
PNODE pHead = NULL; //等价于 struct Node * pHead = NULL;
int val;
pHead = create_list(); //create_list()功能:创建一个非循环单链表,并将该链表的头结点的地址付给pHead
traverse_list(pHead);
//insert_list(pHead, -4, 33);
if ( delete_list(pHead, 4, &val) )
{
printf("删除成功,您删除的元素是: %d\n", val);
}
else
{
printf("删除失败!您删除的元素不存在!\n");
}
traverse_list(pHead);
//int len = length_list(pHead);
//printf("链表的长度是%d\n", len);
//sort_list(pHead);
//traverse_list(pHead);
/* if ( is_empty(pHead) )
printf("链表为空!\n");
else
printf("链表不空!\n");
*/
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 ", 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) //类似于数组中的: a[i] > a[j]
{
t = p->data;//类似于数组中的: t = a[i];
p->data = q->data; //类似于数组中的: a[i] = a[j];
q->data = t; //类似于数组中的: a[j] = t;
}
}
}
return;
}
//在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;
//如果程序能执行到这一行说明p已经指向了第pos-1个结点,但第pos-1个节点是否存在无所谓
//分配新的结点
PNODE pNew = (PNODE)malloc(sizeof(NODE));
if (NULL == pNew)
{
printf("动态分配内存失败!\n");
exit(-1);
}
pNew->data = val;
//将新的结点存入p节点的后面
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;
//如果程序能执行到这一行说明p已经指向了第pos-1个结点,并且第pos个节点是存在的
PNODE q = p->pNext; //q指向待删除的结点
*pVal = q->data;
//删除p节点后面的结点
p->pNext = p->pNext->pNext;
//释放q所指向的节点所占的内存
free(q);
q = NULL;
return true;
}
算法:狭义的算法是与数据的存储方式密切相关
广义的算法是与数据的存储方式无关
泛型:(给你一种假象,只不过牛人从内部都弄好了)
利用某种技术达到的效果就是:不同的存储方式,执行的操作是一样的
算法的真正学法:很多算法你根本解决不了!!!!!!因为很多都属于数学上的东西,所以我们把答案找出来,如果能看懂就行,但是大部分人又看不懂,分三步,按照流程,语句,试数。这个过程肯定会不断地出错,所以不断出错,不断改错,这样反复敲很多次,才能有个提高。实在看不懂就先背会。
链表的优缺点:
优点:
空间没有限制
插入删除元素很快
缺点:
存取速度很慢。
3.3 【栈】-线性结构的两种常见应用之一 (存储数据的结构)
定义
一种可以实现“先进后出” 的存储结构
栈类似于箱子
分类
静态栈 (类似于用数组实现)
动态栈 (类似于用链表实现)
算法(往里放,从里取)
出栈
压栈(参看Java中线程的例子,成产消费的例子)
# 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, * PSTACK; //PSTACK 等价于 struct STACK *
void init(PSTACK);//初始化
void push(PSTACK, int );
void traverse(PSTACK);
bool pop(PSTACK, int *);
void clear(PSTACK pS);
int main(void)
{
STACK S; //STACK 等价于 struct Stack,只定义两个垃圾值ptop和pbottem,不能叫做栈
int val;
init(&S); //目的是造出一个空栈,pTop和pBottom都指向一个头结点
push(&S, 1); //压栈
push(&S, 2);
push(&S, 3);
push(&S, 4);
push(&S, 5);
push(&S, 6);
traverse(&S); //遍历输出
clear(&S);
//traverse(&S); //遍历输出
if ( pop(&S, &val) )
{
printf("出栈成功,出栈的元素是%d\n", val);
}
else
{
printf("出栈失败!\n");
}
traverse(&S); //遍历输出
return 0;
}
void init(PSTACK pS)
{
pS->pTop = (PNODE)malloc(sizeof(NODE));//将节点地址放入pTop,此时只有一个节点
if (NULL == pS->pTop)
{
printf("动态内存分配失败!\n") ;
exit(-1);
}
else
{
pS->pBottom = pS->pTop;//令pTop和pBottom都指向同一个值
pS->pTop->pNext = NULL; //或者pS->Bottom->pNext = NULL;pS指向的s节点的pTop成员指向的头节点的指针域为空,因为它是最后一个元素的下一个元素
}
}
void push(PSTACK pS, int val)//添加元素,将val的压进栈中
{
PNODE pNew = (PNODE)malloc(sizeof(NODE));//在指针域创建一个新节点
pNew->data = val;//将数据放入新节点数据域
pNew->pNext = pS->pTop; //pS->Top不能改成pS->Bottom,新的节点的指针域要指向更接近bottom的那个节点,有多个节点时压栈只能放入栈顶,将Top放入新的节点的指针域
pS->pTop = pNew;//把新的节点地址放入pTop内
return;
}
void traverse(PSTACK pS)
{
PNODE p = pS->pTop;//创造一个节点指向pTop,然后边输出边不断往下移动,直到移动到pBottom
while (p != pS->pBottom)
{
printf("%d ", 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)
{//如果栈为空则出栈失败
if ( empty(pS) ) //pS本身存放的就是S的地址
{
return false;
}
else
{
//要想将pTop下移一位,然后删掉之前的节点不能直接pTop=pTop->pNext,因为删掉节点时,原来的Top节点内存未释放而pTop->pNext还在原来top节点的指针域中,所以需要定义一个r指向原来的top节点
PNODE r = pS->pTop;
*pVal = r->data;
pS->pTop = r->pNext;
free(r);
r = NULL;
return true;
}
}
//clear清空
void clear(PSTACK pS)
{
if (empty(pS))
{
return;
}
else
{
PNODE p = pS->pTop;//p指向Top
PNODE q = NULL;//q先不赋值
while (p != pS->pBottom)//只要p不是指向最后一个元素,即p指向一个有效元素
{
q = p->pNext;//q指向p下面一个节点
free(p);//删掉p
p = q;
}
pS->pTop = pS->pBottom;
}
}
应用
函数调用
中断
表达式求值(用两个栈,一个存放数字,一个存放符号)
内存分配
缓冲处理
迷宫
3.4 【队列】线性结构的两种常见应用之二
定义:
一种可是实现“先进先出”的存储结构
分类:
链式队列:用链表实现
front 头部 删除节点 节点方向是从front指向rear方向
rear 尾部 添加节点
静态队列:用数组实现
静态对流通常都必须是循环队列,为了减少
内存浪费。
循环队列的讲解:
1、 静态队列为什么必须是循环队列
静态队列有数组实现,数组中front在下方指向第一个元素,rear在上方指向最后一个元素上面一个元素,当插入元素时,rear上移,当删除元素时,front上移,这样下方的数组永远都用不到了造成浪费,所以采用循环队列,当上移到数组最上面元素时,如果继续上移就跑到数组最 低端,循环使用
2、 循环队列需要几个参数来确定及其含义
需要2个参数来确定,2个参数不同场合有不同的含义
front 如果front指向第一个元素,那么rear就指向最后一个元素的后一个元素;如果front指向第一个元素之前的一个元素,那么rear就指向最后一个元素。错开是为了方便对链表的操作
rear
3、 循环队列各个参数的含义
2个参数不同场合不同的含义?
建议初学者先记住,然后慢慢体会
1)队列初始化
front和rear的值都是零
2)队列非空
front代表队列的第一个元素
rear代表了最后一个有效元素的下一个元素
3)队列空
front和rear的值相等,但是不一定是零
4、 循环队列入队伪算法讲解
两步完成:
1)将值存入r所代表的位置
2)将r后移,正确写法是 rear = (rear+1)%数组长度
错误写法:rear=rear+1;
5、 循环队列出队伪算法讲解
front = (front+1) % 数组长度
6、 如何判断循环队列是否为空
如果front与rear的值相等,
则队列一定为空
7、 如何判断循环队列是否已满
预备知识:
front的值和rear的值没有规律,
即可以大,小,等。
两种方式:
1、多增加一个表标识的参数,用来计算个数,如果个数等于数组长度,则循环队列就满了
2、少用一个队列中的元素(才一个,不影响的)
通常使用第二种方法
如果r和f的值紧挨着,则队列已满
用C语言伪算法表示就是:
if( (r+1)%数组长度 == f )
已满
else
不满
# 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); //入队,QUEUE *表示往哪个队里面放值,val表示放入的值
void traverse_queue(QUEUE *);
bool full_queue(QUEUE *);
bool out_queue(QUEUE *, int *);//出队
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, 6);
en_queue(&Q, 7);
en_queue(&Q, 8);
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存放Q的地址,*pQ = Q
{
pQ->pBase = (int *)malloc(sizeof(int) * 6);//pBase代表数组第一个元素的地址,pBase可以近似理解为一个24字节的数组
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;
}
}
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;
}
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;
}
}
队列算法:
入队
出队
队列的具体应用:
所有和事件有关的操作都有队列的影子。
(例如操作系统认为先进来的先处理)
3.5 【递归】
【递归的思想是软件思想的基本思想之一,在树和图论上面,几乎全是用递归来实现的,最简单,像求阶乘这种没有明确执行次数的问题,都是用递归来解决】
定义:
一个函数自己直接或间接调用自己(一个函数调用另外一个函数和他调用自己是一模一样的,都是那三步,只不过在人看来有点诡异。)
当一个函数的运行期间调用另一个函数时,在运行被调函数之前,系统需要完成3件事:
1. 将所有实际参数,返回地址等信息传递给被调函数保存
2. 为被调用的函数的局部变量分配储存区
3. 将控制转移到被调用函数的入口。
而从被调用函数返回调用函数之前,系统也应完成三件事:
1. 保存被调用函数的计算结果;
2. 释放被调函数的数据区;
3. 依照被调用函数保存的返回地址将控制转移到调用函数。
当有多个函数构成嵌套调用时,按照“后调用先返回”的原则,上述函数之间的信息传递和控制转移必须通过“栈”来实现,即系统将整个程序运行时所需的数据空间安排在一个栈中,每当调用一个函数时,就为它在栈顶分派一个储存区,每当从一个函数退出时,就释放它的储存区,则当前正在运行的函数的数据区必在栈顶。
递归满足的三个条件:
1、递归必须得有一个明确的终止条件
2、该函数处理的数据规模必须在递减
3、这个转化必须是可解的。
循环和递归:
理论上循环能解决的,肯定可以转化为递归,但是这个过程是复杂的数学转化过程,递归能解决不一定能转化为循环,我们初学者只要把经典的递归算法看懂就行,至于有没有能力运用看个人。
递归:
易于理解
速度慢
存储空间大
循环
不易于理解
速度快
存储空间小
举例:
1.求阶乘
阶乘的循环实现
# include <stdio.h>
int main(void)
{
int val;
int i, mult=1;
printf("请输入一个数字: ");
printf("val = ");
scanf("%d", &val);
for (i=1; i<=val; ++i)
mult = mult * i;
printf("%d的阶乘是:%d\n", val, mult);
return 0;
}
阶乘的递归实现
# include <stdio.h>
//假定n的值是1或大于1的值
long f(long n)
{
if (1 == n)//倒着写是为了防止等号写成=,倒着写写成=系统必报错
return 1;
else
return f(n-1) * n;
}
int main(void)
{
printf("%ld\n", f(100));
return 0;
}
2.1+2+3+4+。。。+100的和
# include <stdio.h>
long sum(int n)
{
if (1 == n)
return 1;
else
return n + sum(n-1);
}
int main(void)
{
printf("%ld\n", sum(100));
return 0;
}
3.汉诺塔
【汉诺塔】这不是线性递归,这是非线性递归!
n=1 1
n=2 3
n=3 7
.........
.........
n=64 2的64次方减1【这是个天文数字,就算世界上最快的计算机
也解决不了,汉诺塔的负责度是2的n次方减1】问题很复杂,但真正解决
问题的编码只有三句。
# include <stdio.h>
void hannuota(int n, char A, char B, char C)
{
/*
如果是1个盘子
直接将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.走迷宫(CS的实现)
递归的运用:
树和森林就是以递归的方式定义的
树和图的很多算法都是以递归来实现的
很多数学公式就是以递归的方式定义的
斐波拉契序列
1 2 3 5 8 13 21 34。。。
为何数据结构难学:因为计算机内存是线性一维的,而我们要处理的数据都是比较复杂的,那么怎么把这么多复杂的数据保存在计算机中来保存本身就是一个难题,而计算机在保存线性结构的时候比较好理解,尤其是数组和链表只不过是连续和离散的问题,线性结构是我们学习的重点,因为线性算法比较成熟,无论C++还是Java中都有相关的工具例如Arraylist.Linkedlist,但是在Java中没有树和图,因为非线性结构太复杂了,他的操作远远大于线性结构的操作。即使SUN公司也没造出来。list代表链表,ArrayList代表用数组实现的链表,Linkedlist用链表实现的链表。
小复习一下:_
逻辑结构:(就是在你大脑里面能产生的,不考虑在计算机中存储)
线性结构(用一根直线穿)
在计算机内的存储方式:
数组
链表
栈和队列是一种特殊的线性结构,是具体应用。
(操作受限的线性结构,不受限的应该是在任何地方可以增删改查
可以用数组和链表实现。只要把链表学会,栈和队列都能搞定,数
组稍微复杂一些。)
非线性:
树
图
物理结构:
数组
链表
4 非线性结构
(都要靠链表去实现)
4.1 【树】
树定义
专业定义:
1、有且只有一个称为根的节点
2、有若干个互不相交的子树,这些子树本身也是一棵树
通俗定义:
1、树是由节点和边组成
2、每个节点只有一个父节点但可以有多个子节点
3、但有一个节点例外,该节点没有根节点,此节点称为根节点
专业术语
节点 父节点 子节点
子孙 堂兄弟
深度:
从根节点到最底层节点的层数称之为深度
根节点是第一层
叶子节点;(叶子就不能劈叉了)
没有子节点的节点
非终端节点:
实际就是非叶子节点。
根节点既可以是叶子也可以是非叶子节点
度:
子节点的个数称为度。(一棵树看最大的)
树分类:
一般树
任意一个节点的子节点的个数都不受限制
二叉树(有序树)
任意一个节点的子节点的个数最多两个,且子节点
的位置不可更改。
分类:
一般二叉树
满二叉树
在不增加树的层数的前提下。无法再多添加一个节点的二叉树就是满二叉树。
完全二叉树
如果只是删除了满二叉树最底层最右边的连续若干个节点,这样形成的二叉树就是完全二叉树。
满二叉树是完全二叉树的一个特例
森林
n个互不相交的树的集合
一般的二叉树要以数组的方式存储,要先转化成完全二叉树,因为如果你只存有效节点(无论先序,中序,后序),则无法知道这个树的组成方式是什么样子的。
树的存储(都是转化成二叉树来存储)
二叉树的存储
连续存储【完全二叉树】
优点:
查找某个节点的父节点和子节点(也包括判断有咩有)速度很快
缺点:
耗用内存空间过大
链式存储
一般树的存储
双亲表示法
求父节点方便
孩子表示法
求子节点方便
双亲孩子表示法
求父节点和子节点都很方便
二叉树表示法
把一个普通树转化成二叉树来存储
具体转换方法:
设法保证任意一个节点的
左指针域指向它的第一个孩子
有指针域指向它的下一个兄弟
只要能满足此条件,就可以把一个普通树转化成二叉树
一个普通树转化成的二叉树一定没有右子树
森林的存储
先把森林转化为二叉树,再存储二叉树
二叉树操作
遍历
先序遍历【先访问根节点】
先访问根节点
再先序访问左子树
再先序访问右子树
中序遍历【中间访问根节点】
中序遍历左子树
再访问根节点
再中序遍历右子树
后序遍历【最后访问根节点】
先后序遍历左子树
再后序遍历右子树
再访问根节点
已知两种遍历序列求原始二叉树
通过先序和中序 或者 中序和后续我们可以
还原出原始的二叉树
但是通过先序和后续是无法还原出原始的二叉树的
换种说法:
只有通过先序和中序, 或通过中序和后序
我们才可以唯一的确定一个二叉树
应用
树是数据库中数据组织的一种重要形式(例如图书馆的图书分类一层一层往下分。)
操作系统子父进程的关系本身就是一棵树
面向对象语言中类的继承关系本身就是一棵树
赫夫曼树(树的一个特例)
图
模块三:查找和排序
折半查找
排序:
冒泡排序
1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2. 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
3. 针对所有的元素重复以上的步骤,除了最后一个。
4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
插入排序
插入排序是指在待排序的元素中,假设前面n-1(其中n>=2)个数已经是排好顺序的,现将第n个数插到前面已经排好的序列中,然后找到合适自己的位置,使得插入第n个数的这个序列也是排好顺序的。按照此法对所有元素进行插入,直到整个序列排为有序的过程,称为插入排序
选择排序
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
快速排序
(1)首先设定一个分界值,通过该分界值将数组分成左右两部分。
(2)将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。
(3)然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
(4)重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。
归并排序
第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置
第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
重复步骤3直到某一指针超出序列尾
将另一序列剩下的所有元素直接复制到合并序列尾
(先两个两个排,在四个四个排,再八个八个排)
排序和查找的关系
排序是查找的前提
排序是重点
Java中容器和数据结构相关知识
Iterator接口
Map
哈希表(与Java关系比较大)
再次讨论什么是数据结构:
数据结构研究是数据结构的存储和数据的操作的一门学问
数据的存储分为两部分:
个体的存储
个体关系的存储
从某个角度而言,数据的存储最核心的就是个体关系的存储,个体的存储可以忽略不计。
再次讨论到底什么是泛型:
同一种逻辑结构,无论该逻辑结构物理存储是什么样子的,我们都可以对它执行相同的操作(例如都是线性结构或者用数组实现的树和用链表实现的树。利用重载技术。)