目录
数据结构(郝斌)
预备知识
模块一 : 线性结构(线性表)
连续存储[数组]
离散存储[链表]
线性结构中两种常见应用之一 --> 栈
线性结构中两种常见应用之二 --> 队列
专题 : 递归
模块二 : 非线性结构
树
图
模块三 : 查找和排序
折半查找
一、数据结构概述
1.1 什么是数据结构
定义:
我们如何把现实中大量而复杂的问题以特定的数据类型和特定的存储结构保存到主存储器(内存)中,以及在此基础上为实现某个功能(比如查找某个元素,删除某个元素,对所有元素进行排序)而执行的相应操作,这个相应的操作也叫算法。
数据结构 = 个体的存储 + 个体的关系的存储
算法 = 对存储数据的操作(狭义的)
1.2 什么是算法
算法是解题的方法和步骤
衡量算法的标准:
-
时间复杂度
大概程序要执行的次数,而非执行的时间
-
空间复杂度
算法执行过程中大概所占用的最大内存
-
难易程度
-
健壮性
1.3 数据结构的地位
数据结构是软件中最核心的课程
程序 = 数据结构 + 算法
二、预备知识
2.1 指针
指针就是内存单元的编号
地址就是指针 指针就是地址
指针变量是存储指针(地址)的变量
址传递
指针和数组的关系 :
int a[5] = {1, 2, 3, 4, 5};
一维数组名是一个指针常量
存放的是一维数组第一个元素的地址
一维数组名指向的是数组的第一个元素
数组址传递
一个指针无论指向的是整型还是浮点型 指针变量本身所占的字节数都是固定的
32位电脑对应四个字节
64位电脑对应八个字节
无论什么类型的变量 要想改写它的值 只能发送它的地址
2.2 结构体
为什么会出现结构体
为了表示一些复杂的数据,而普通的基本数据类型变量无法满足要求
什么叫结构体
结构体是用户根据实际需要自己定义的复合数据类型
如何使用结构体
struct Student st = {1001, "侯静川", 22};
struct Student * p = &st;
//1.
p->age = 21;
//2.
(*p).age = 21;
st.age = 21;
注意事项
结构体变量不能加减乘除,但可以相互赋值
普通结构体变量和结构体指针作为函数传参的问题
2.3 动态内存的分配和释放
#include <stdio.h>
void init_Arr(int * arr, int len);
void print_Arr(int * arr, int len);
int main()
{
int len;
printf("请输入数组长度 : ");
scanf("%d", &len);
int *pArr = (int *)malloc(sizeof(int) * len);// 动态内存分配
/*
malloc函数 : 只有一个形参 形参表示请求系统分配多少个字节的空间
返回值为第一个字节的地址,但是第一个字节的地址是无意义的,即不能判断该地址后面的字节是几个字节表示一个变量
所以要将其强制转换为int*类型,即表示为int类型的地址 就可以知道从该字节地址开始 往后四个字节代表一个变量
*/
init_Arr(pArr, len);
print_Arr(pArr, len);
free(pArr);// 把pArr所指向的动态分配的字节的空间释放
return 0;
}
void init_Arr(int * arr, int len)
{
int i;
for (i = 0; i < len; i++)
{
scanf("%d", &arr[i]);
}
}
void print_Arr(int * arr, int len)
{
printf("当前数组大小为 : %d , 数组内容如下 : \n", len);
int i;
for (i = 0; i < len; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
跨函数使用内存的问题:
#include <stdio.h>
int * init_Arr(int len)
{
int i;
int * p = (int *)malloc(sizeof(int) * len);
printf("请输入数组内容 : \n");
for (i = 0; i < len; i++)
{
scanf("%d", &p[i]);
}
return p;
}
void print_Arr(int * arr, int len)
{
printf("当前数组大小为 : %d , 数组内容如下 : \n", len);
int i;
for (i = 0; i < len; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int len;
printf("请输入数组长度 : ");
scanf("%d", &len);
int * pArr = init_Arr(len);
print_Arr(pArr, len);
return 0;
}
2.4 typedef的用法
typedef就是给已有数据类型起一个别名
#include <stdio.h>
typedef long long LL;
typedef struct Student
{
int age;
char sex;
char name[100];
}Stu;
int main()
{
printf("%d", sizeof(LL));
Stu st;
Stu * pst = &st;
return 0;
}
#include <stdio.h>
// typedef struct Student
// {
// int age;
// char sex;
// char name[100];
// }* PST; // 等价与 struct Student *
typedef struct Student
{
int age;
char sex;
char name[100];
}ST, * PST;// 可以同时定义两个 ST 代表 struct Student PST代表 struct Student *
int main()
{
ST st;//等价于struct Student st;
PST pst = &st;//等价于struct Student * pst = &st;
return 0;
}
三、连续存储数组
3.1 什么叫做数组
元素类型相同,大小相等
3.2 数组的优缺点
优点 :
-
存取速度快
缺点:
-
插入删除元素慢
-
事先必须知道数组的长度
-
空间通常有限制
-
需要一大块连续的内存
3.3 算法演示
// 定义数据类型
struct Arr
{
int *pBase;//存储的数组第一个元素的地址
int len;// 数组的长度
int cnt;//当前数组有效元素的个数
// int incremenmt;// 自动增长因子 --> 需要用到allocate函数
};
// 定义操作该结构体的函数
void init_arr(struct Arr * pArr, int len);//初始化该数组 pBase指向一块数组 len初始化为该数组的长度 cnt初始化为0
bool append_arr(struct Arr * pArr, int num);//向数组的最后一个有效值后添加一个元素
bool insert_arr(struct Arr * pArr, int pos, int val);//在数组中插入一个元素 pos的值从1开始
bool delete_arr(struct Arr * pArr, int pos, int * val);//删除数组中的一个元素 删除的元素值用val来接收
void clear_arr(struct Arr * pArr);//清除数组所占空间 如需使用 则要重新调用init函数
int get(struct Arr * pArr, int index);//获取数组中的指定元素 如果不存在则返回-1 index从1开始
bool is_empty(struct Arr * pArr);//判断数组是否为空
bool is_full(struct Arr * pArr);//判断数组是否已满
void sort_arr(struct Arr * pArr, char pattern);//对数组进行排序
void show_arr(struct Arr * pArr);//遍历数组
void reverse_arr(struct Arr * pArr);//对数组元素进行反转
//两种排序算法的实现
void bubble_sort(int * arr, int len);
void select_sort(int * arr, int len);
四、离散存储链表
4.1 链表定义
-
n个节点离散分配
-
彼此通过指针相连
-
每个节点只有一个前驱节点,每个节点只有一个后继节点
-
首节点没有前驱节点,尾结点没有后继节点
专业术语:
-
首节点:第一个有效的节点
-
尾节点:最后一个有效的节点
-
头节点:第一个有效节点之前的节点,头节点并不存放有效的数据,加头节点是为了简便对链表的操作
-
头指针:指向头节点的指针变量
-
尾指针:指向尾节点的指针变量
确定一个链表需要几个参数?
确定一个链表只需要知道该链表的头指针即可,即我们利用函数对该链表进行操作只需要传递头指针,因为我们通过头指针就可以推算出链表的其他所有信息。
4.2 链表分类
-
单链表
-
双链表:每一个节点有两个指针域
-
循环链表:能通过任何一个节点找到其他节点,尾节点指针域指向头节点
-
非循环链表
4.3 链表算法
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// 定义节点数据类型
typedef struct Node
{
int data;//数据域
struct Node * next;//指针域
} NODE, * PNODE;
// 定义操作该结构体的函数
PNODE create_list();// 创建一个非循环单链表 并将该链表头节点的地址发送给pHead
void show_list(PNODE pHead);// 遍历输出单链表内容
bool is_empty(PNODE pHead);// 判断单链表是否为空
int length_list(PNODE pHead);// 返回该单链表长度 如果链表为空则返回-1
bool insert_list(PNODE pHead, int pos, int val);// 在指定位置插入元素
bool delete_list(PNODE pHead, int pos, int * val);// 删除某个元素
void sort_list(PNODE pHead, char pattern);//对链表进行排序 当输入- 则为降序 当输入+ 则为升序
//两种排序算法的实现
void bubble_sort(PNODE pHead);
void select_sort(PNODE pHead);
int main()
{
PNODE pHead = NULL;
// if (is_empty(pHead)) printf("该单链表为空\n");
// else printf("该单链表不为空\n");
pHead = create_list();
show_list(pHead);
int len = length_list(pHead);
printf("链表的长度为:%d\n", len);
insert_list(pHead, 4, 100);
show_list(pHead);
int val;
if (delete_list(pHead, 5, &val)) printf("删除节点的值是:%d\n", val);
else printf("删除失败\n");
show_list(pHead);
printf("降序排序:\n");
sort_arr(pHead, '-');
show_list(pHead);
printf("升序排序:\n");
sort_arr(pHead, '+');
show_list(pHead);
return 0;
}
PNODE create_list()
{
int i, len, val;
printf("请输入该链表节点个数:");
scanf("%d", &len);
//建立一个头节点
PNODE pHead = (PNODE)malloc(sizeof(NODE));
if (NULL == pHead)
{
printf("动态内存分配失败!");
exit(-1);
}
PNODE pTail = pHead;//尾插法 一开始尾指针指向头节点
pTail->next = NULL;
for(i = 0; i < len; i++)
{
printf("请输入第%d个节点的值:", i+1);
scanf("%d", &val);
PNODE pNew = (PNODE)malloc(sizeof(NODE));
if (NULL == pNew)
{
printf("动态内存分配失败!");
exit(-1);
}
pNew->data = val;//填充数据域
//尾插法
pNew->next = NULL;//新插入的节点为尾节点 指针域永远为NULL
pTail->next = pNew;
pTail = pNew;//尾指针永远指向最后一个节点
}
return pHead;
}
bool is_empty(PNODE pHead)
{
if (pHead == NULL || pHead->next == NULL) return true;
else return false;
}
int length_list(PNODE pHead)
{
int len = 0;
if (is_empty(pHead)) return len;
PNODE p = pHead->next;
while(p != NULL)
{
len++;
p = p->next;
}
return len;
}
bool insert_list(PNODE pHead, int pos, int val)
{
//判断插入位置合法性
if (pos < 1 || pos > length_list(pHead)+1 || pHead == NULL) return false;
//生成新节点
PNODE pNew = (PNODE)malloc(sizeof(NODE));
if (NULL == pNew)
{
printf("动态内存分配失败!\n");
exit(-1);
}
//将指针定位到插入位置的前一个节点
PNODE p = pHead;
// int i;
// for (int i = 0; i < pos-1; i++)
// p = p->next;
while(pos-- > 1) p = p->next;
//插入新节点
pNew->data = val;
pNew->next = p->next;
p->next = pNew;
return true;
}
bool delete_list(PNODE pHead, int pos, int * val)
{
//判断删除位置合法性
if (pos < 1 || pos > length_list(pHead)) return false;
//将指针定位到删除位置的前一个节点
PNODE p = pHead;
while(pos-- > 1) p = p->next;
//删除该节点
PNODE q = p->next;
p->next = q->next;
*val = q->data;
free(q);
q = NULL;
return true;
}
void sort_arr(PNODE pHead, char pattern)
{
if (pattern == '+')//升序排序
{
select_sort(pHead);
}
else if (pattern == '-')//降序排序
{
bubble_sort(pHead);
}
else
{
printf("输入匹配模式有误!");
}
}
void show_list(PNODE pHead)
{
printf("单链表内容如下:\n");
PNODE p = pHead->next;
while (p != NULL)
{
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
// 深入理解泛型 对比数组排序
/*
void bubble_sort(int * arr, int len)
{
int i, j, k;
for (i = 0; i < len; i++)
{
for (j = 0; j < len-1-i; j++)
{
// 从大到小 降序
if (arr[j] < arr[j+1])
{
k = arr[j];
arr[j] = arr[j+1];
arr[j+1] = k;
}
}
}
}
*/
void bubble_sort(PNODE pHead)
{
int i, j, k;
PNODE p, q;
int len = length_list(pHead);
for (i = 0, p = pHead->next; i < len-1; i++, p = p->next)
{
for (j = 0, q = pHead->next; j < len-1-i; j++, q = q->next)
{
if (q->data < q->next->data)
{//只交换数据域的值
k = q->data;
q->data = q->next->data;
q->next->data = k;
}
}
}
}
/*
void select_sort(int * arr, int len)
{
int i, j, k, min;
for (i = 0; i < len-1; i++)
{
min = i;
for (j = i; j < len; j++)
{
if (arr[min] > arr[j])
{
min = j;
}
}
if (min != i)
{
k = arr[min];
arr[min] = arr[i];
arr[i] = k;
}
}
}
*/
void select_sort(PNODE pHead)
{
int i, j, k;
PNODE p, q, min;
int len = length_list(pHead);
for (i = 0, p = pHead->next; i < len-1; i++, p = p->next)
{
min = p;
for (j = i+1, q = p->next; j < len; j++, q = q->next)
{
if (min->data > q->data)
{
min = q;
}
}
if (min != p)
{
k = min->data;
min->data = p->data;
p->data = k;
}
}
}
// void select_sort(PNODE pHead)
// {
// int i, j, k;
// PNODE p, q;
// int len = length_list(pHead);
// for (i = 0, p = pHead->next; i < len-1; i++, p = p->next)
// {
// for (j = i+1, q = p->next; j < len; j++, q = q->next)
// {
// if (p->data > q->data)
// {
// k = p->data;
// p->data = q->data;
// q->data = k;
// }
// }
// }
// }
郝斌给出的高效算法:
将指针定位到插入位置的前一个节点:
//郝斌给出算法:
int i = 0;
while (NULL != p && i < pos-1) //将p定位在pos-1的位置 如果该pos超出有效长度则p会指向尾节点的指针域NULL
{
p = p->next;
i++;
}
//首先判断pos值小于0的情况 再判断该指针是否因为pos超出有效长度而遍历到尾指针的指针域NULL
//如果以上情况都未出现 则认为该指针正确的指向了pos-1的位置
if (i > pos-1 || NULL == p) return false;
将指针定位到删除位置的前一个节点:
//郝斌给出的算法:
//与插入的算法不同的是 要判断p-next是否为空 为空则无法删除
//必须保证不为空才可以定位到该位置的前一个节点进行删除
int i = 0;
while (NULL != p->next && i < pos-1)
{
p = p->next;
i++;
}
if (i > pos-1 || NULL == p->next) return false;
4.4 链表的优缺点
优点:
-
空间无限制
-
插入删除元素快
缺点:
-
存取速度慢
五、复习
数据结构:
狭义:
数据结构是专门研究数据存储的问题
数据的存储包含两方面:个体的存储和个体间关系的存储
广义:
数据结构既包含数据的存储也包含数据的操作
对存储数据的操作就是算法
算法:
狭义:
算法是和数据的存储方式密切相关
广义
算法和数据的存储方式无关
这就是泛型思想(通过模板、运算符重载、指针等技术实现)
数据的存储结构有几种:
线性
连续存储【数组】
离散存储【链表】
线性结构的应用 -- 栈
线性结构的应用 -- 队列
非线性
树
图
六、栈
6.1 栈的定义
一种可以实现“先进后出”的存储结构
链式栈本质上是一个操作受限的链表
6.2 栈的分类
静态栈 【数组】
动态栈 【链表】
6.3 栈的算法
出栈
入栈【压栈】
链式栈的实现:
首先定义栈的结构体
// 定义节点结构体
typedef struct Node
{
int data;//数据域
struct Node * next;//指针域
}NODE, * PNODE;
// 定义栈结构体
typedef struct Stack
{
PNODE pTop; //指向栈顶
PNODE pBottom; // 指向栈底的下一个没有实际含义的元素
}STACK, * PSTACK;
定义操作栈的函数
void init_stack(PSTACK pS); //对栈进行初始化 造一个空栈
void clear_stack(PSTACK pS); //清空栈
void push_stack(PSTACK pS, int val);//压栈
bool pop_stack(PSTACK pS, int * val); //出栈 若栈空则出栈失败 成功则val将返回出栈元素值
void show_stack(PSTACK pS); //对栈进行遍历
bool is_empty(PSTACK pS); // 判断是否栈空
实现这些函数
void init_stack(PSTACK pS)
{
// 新建一个头节点 将栈底指针指向头节点
pS->pBottom = (PNODE)malloc(sizeof(NODE));
if (NULL == pS->pBottom)
{
printf("动态内存分配失败!");
exit(-1);
}
// 将头节点的指针域置空
pS->pBottom->next = NULL;
// 开始时 栈顶指针也指向头节点
pS->pTop = pS->pBottom;
}
void push_stack(PSTACK pS, int val)
{
//建立一个新节点
PNODE pNew = (PNODE)malloc(sizeof(NODE));
if (NULL == pNew)
{
printf("动态内存分配失败!");
exit(-1);
}
pNew->data = val;
//将新节点压入栈中
pNew->next = pS->pTop;
pS->pTop = pNew;
}
bool pop_stack(PSTACK pS, int * pVal)
{
//判断是否栈空
if (is_empty(pS)) return false;
// 出栈
PNODE p = pS->pTop;
pS->pTop = p->next;
// 释放p节点
*pVal = p->data;
free(p);
p = NULL;
return true;
}
void show_stack(PSTACK pS)
{
PNODE p = pS->pTop;
while(p != pS->pBottom)
{
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
void clear_stack(PSTACK pS)
{
PNODE p = pS->pTop;
while (p != pS->pBottom)
{
pS->pTop = p->next;
free(p);
p = pS->pTop;
}
}
bool is_empty(PSTACK pS)
{
if (pS->pBottom == pS->pTop) return true;
else return false;
}
定义主函数测试
int main()
{
STACK S; //定义一个栈
init_stack(&S);
push_stack(&S, 1);
push_stack(&S, 2);
push_stack(&S, 3);
push_stack(&S, 4);
push_stack(&S, 5);
push_stack(&S, 6);
show_stack(&S);
int val;
if (pop_stack(&S, &val)) printf("出栈成功,出栈的元素是%d\n", val);
else printf("栈空,出栈失败!\n");
show_stack(&S);
clear_stack(&S);
if (is_empty(&S)) printf("栈空");
else show_stack(&S);
return 0;
}
6.4 栈的应用
-
函数调用
-
中断
-
表达式求值
-
内存分配
-
缓冲处理
-
迷宫
七、队列
7.1 队列的定义
一种可以实现“先进先出”的存储结构
7.2 队列的分类
静态队列【数组】
动态队列【链表】链式队列
7.3 队列的算法
7.3.1 链式队列
链式队列的实现: 首先定义队列结构体
//定义节点结构体
typedef struct Node
{
int data;//数据域
struct Node * next;//指针域
}NODE, * PNODE;
//定义队列结构体
typedef struct Queue
{
PNODE pFront;// 指向队首
PNODE pRear;// 指向队尾
}QUEUE, * PQUEUE;
定义操作该结构体的函数
void init_queue(PQUEUE pQ);//初始化队列
void enqueue(PQUEUE pQ, int val);// 入队
bool dequeue(PQUEUE pQ, int * val);//出队
bool is_empty(PQUEUE pQ);//队列判空
void show_queue(PQUEUE pQ);//遍历队列
void clear_queue(PQUEUE pQ);//清空链式队列
实现这些函数
void init_queue(PQUEUE pQ)
{
//新建一个头节点 队尾、队首指针都指向该头节点
pQ->pFront = (PQUEUE)malloc(sizeof(QUEUE));
if (NULL == pQ->pFront)
{
printf("动态内存分配失败!\n");
exit(-1);
}
pQ->pRear = pQ->pFront;
pQ->pRear->next = NULL;//头节点指针域置空
}
void enqueue(PQUEUE pQ, int val)
{
//新建一个节点
PNODE p = (PNODE)malloc(sizeof(NODE));
if (NULL == p)
{
printf("动态内存分配失败!\n");
exit(-1);
}
p->data = val;
//入队
pQ->pRear->next = p;
p->next = NULL;
pQ->pRear = p;
}
bool dequeue(PQUEUE pQ, int * val)
{
if (is_empty(pQ)) return false;
//由于pFront实际指向的是头节点 所以要删除的是头节点的下一个节点
//如果p指向的是队尾节点 那么头节点的指针域会赋值为NULL
PNODE p = pQ->pFront->next;
pQ->pFront->next = p->next;
//此时需要判断p是否指向了队尾 若指向队尾 则让队尾指针也指向队首指针
if (p == pQ->pRear) pQ->pRear = pQ->pFront;
*val = p->data;
free(p);
p = NULL;
return true;
}
bool is_empty(PQUEUE pQ)
{
if (pQ->pFront == pQ->pRear) return true;
else return false;
}
void clear_queue(PQUEUE pQ)
{
//如果队列为空则退出
if (is_empty(pQ)) return;
//创建一个指针定位到队首指针的下一个节点 判断该节点是否为队尾指针
//如果该节点是队尾指针 则将队尾节点释放以后 队尾指针指向队首指针
PNODE p = pQ->pFront->next;
while (p != pQ->pRear)
{
// 如果p指向的是队尾指针 那么会赋值为NULL 头节点的指针域会变回NULL
pQ->pFront->next = p->next;
free(p);
p = pQ->pFront->next;
}
pQ->pRear = pQ->pFront;
}
void show_queue(PQUEUE pQ)
{
PNODE p = pQ->pFront->next;
while (p != NULL)
{
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
编写主函数,测试该链式队列:
int main()
{
int val;
QUEUE Q;
init_queue(&Q);
enqueue(&Q, 1);
enqueue(&Q, 2);
show_queue(&Q);
// if (dequeue(&Q, &val)) printf("出队成功,出队的元素是%d\n", val);
// else printf("队空,出队失败!\n");
// if (dequeue(&Q, &val)) printf("出队成功,出队的元素是%d\n", val);
// else printf("队空,出队失败!\n");
enqueue(&Q, 3);
enqueue(&Q, 4);
show_queue(&Q);
clear_queue(&Q);
if (dequeue(&Q, &val)) printf("出队成功,出队的元素是%d\n", val);
else printf("队空,出队失败!\n");
enqueue(&Q, 10);
enqueue(&Q, 20);
show_queue(&Q);
return 0;
}
链式队列最重要的问题:头节点的位置
首先初始化的时候我们必须建立一个头节点来完成初始化操作,此时队首指针和队尾指针都指向了该头节点
其次,我们在进行入队操作的时候,应该将新节点插入到头节点之前作为真正的队尾,也就是说,头节点的位置始终放在真正的队首,而我们队列的队首元素则是头节点的下一个节点。
在进行出队操作的时候,我们要将指针指向队首指针指向节点的下一个节点,该节点才是真正要出队的元素。
在该程序中,队首指针始终指向的是头节点,而队尾指针,在空队列的时候,指向头节点。
以下是cpp实现的链式队列:
#include <iostream>
using namespace std;
//定义节点结构体
typedef struct Node
{
int data;//数据域
struct Node * next;//指针域
}NODE, *PNODE;
//定义链式队列类
class Queue
{
public :
Queue()
{
this->pHead = this->pTail = new NODE;
this->pHead->next = NULL;
}
void enQueue(int val)
{
//新建一个节点
PNODE p = new NODE;
p->data = val;
//入队
p->next = NULL;
pTail->next = p;
pTail = p;//尾指针下移
}
void deQueue()
{
//将p定位到要出队的元素
PNODE p = pHead->next;
//出队
pHead->next = p->next;
//判断p是否为尾指针 若是则让尾指针指向头节点
if (p == pTail) pTail = pHead;
//释放出队元素
delete p;
p = NULL;
}
void showQueue()
{
PNODE p = pHead->next;
while (p != NULL)
{
cout << p->data << " ";
p = p->next;
}
cout << endl;
}
private :
PNODE pHead, pTail;//pHead指向无用的头节点 pHead->next才是队首元素
};
int main()
{
Queue Q;
Q.enQueue(1);
Q.enQueue(2);
Q.enQueue(3);
Q.deQueue();
Q.enQueue(4);
Q.enQueue(5);
Q.showQueue();
return 0;
}
7.3.2 静态队列
静态队列通过必须是循环队列
循环队列的讲解:
-
静态队列为什么必须是循环队列?
定义一个数组,队首指针指向头部,队尾指针指向尾部,当向队列中添加元素时,队尾指针上移,当向队列里删除元素时,队首指针上移。两个指针都向上移动,导致数组能利用的空间一直减少,所以使用传统数组实现的队列必须是循环队列。
-
循环队列需要几个参数来确定?
一个循环队列需要两个参数来确定 :front rear
-
循环队列各个参数的含义?
两个参数不同场合有不同的含义 1)队列初始化 :front与rear的值都是零 2)队列非空 :front代表的队列的第一个元素 rear代表的队列的最后一个有效元素的下一个元素 3)队列空 :front与rear的值相等但不一定是零
-
循环队列入队伪算法讲解 按照以下两步完成
-
循环队列出队伪算法讲解 按照以下一步完成:
-
如何判断循环队列为空?
当队列中front == rear时,循环队列为空
-
如何判断循环队列已满?
在队列中,front的值可能比rear大,也可能比rear小,也可能相等。我们规定了 front == rear 时,队列为空。那么按照之前的思路,队列已满的话,front 也== rear。
解决方式:
-
多增加一个标志位,表示数组的有效元素个数,当有效元素个数等于数组的长度时,队列已满
-
少用一个元素,当(r + 1)% 数组的长度 == f 时,则队列已满。
-
代码展示:
定义队列结构体
// 定义队列结构体
typedef struct Queue
{
int * pBase;
int front;
int rear;
}QUEUE, * PQUEUE;
定义操作结构体的函数
void init_queue(PQUEUE pQ);//初始化循环队列
bool enqueue(PQUEUE pQ, int val);//入队
bool dequeue(PQUEUE pQ, int * pVal);//出队 val保存出队的值
void show_queue(PQUEUE pQ);//遍历循环队列
bool is_full(PQUEUE pQ);//判断循环队列已满
bool is_empty(PQUEUE pQ);//判断循环队列已空
实现这些函数
void init_queue(PQUEUE pQ)
{
pQ->pBase = (int *)malloc(sizeof(int) * 6);
pQ->rear = pQ->front = 0;
}
bool enqueue(PQUEUE pQ, int val)
{
if (is_full(pQ)) return false;
//入队
pQ->pBase[pQ->rear] = val;
pQ->rear = (pQ->rear + 1) % 6;
}
bool dequeue(PQUEUE pQ, int * pVal)
{
if (is_empty(pQ)) return false;
//出队
*pVal = pQ->pBase[pQ->front];
pQ->front = (pQ->front + 1) % 6;
}
bool is_empty(PQUEUE pQ)
{
if (pQ->front == pQ->rear) return true;
else return false;
}
bool is_full(PQUEUE pQ)
{
if ((pQ->rear + 1) % 6 == pQ->front) return true;
else return false;
}
void show_queue(PQUEUE pQ)
{
int i;
for (i = pQ->front; i != pQ->rear; i = (i+1)%6)
{
printf("%d ", pQ->pBase[i]);
}
printf("\n");
}
编写主函数测试功能:
int main()
{
int val;
QUEUE Q;
init_queue(&Q);
enqueue(&Q, 1);
enqueue(&Q, 2);
enqueue(&Q, 3);
dequeue(&Q, &val);
enqueue(&Q, 4);
enqueue(&Q, 5);
enqueue(&Q, 6);
dequeue(&Q, &val);
dequeue(&Q, &val);
show_queue(&Q);
return 0;
}
7.4 队列的应用
所有和时间有关的操作都有队列的影子
八、栈与队列总结
8.1 链式栈
链式栈的思想就是先进后出 栈顶入栈出栈
我们定义一个栈顶指针、一个栈底指针
链表的头节点在栈底,头节点的指针域为空
栈底指针在整个过程中都指向头节点不动,栈顶指针控制节点入栈出栈
初始时:创建一个头节点,头节点指针域为空,栈顶指针栈底指针都指向头节点
入栈时:将新节点的指针域赋值为栈顶指针,栈顶指针上移指向新节点
出栈时:定义一个指针p赋值为栈顶指针,栈顶指针指向节点的指针域赋值为p->next,释放p指向的节点
当栈顶指针等于栈底指针时表示栈空(栈顶指针栈底指针都指向头节点)
8.2 链式队列
链式队列的思想就是先进先出 队首出队 队尾入队
我们定义一个队首指针、一个队尾指针
链表的头节点在队首,头节点的指针域指向下面一个元素
队首指针在整个过程中都指向头节点不动
队尾指针控制节点入队,队首指针控制节点出队
初始时:创建一个头节点,头节点的指针域为空,队首指针队尾指针都指向头节点
入队时:将新节点的指针域置空,队尾指针的next指向新节点,队尾指针下移指向新节点
出队时:定义一个指针p赋值为队首指针的next,队首指针的next赋值为p->next,此时判断p是否与队尾指针相等,若相等,则先将队尾指针上移指向头节点,再释放p指针指向的节点。
当队首指针等于队尾指针时表示队空(队首指针队尾指针都指向头节点)
8.3 循环队列
循环队列是由数组实现的 队首出队 队尾入队
重要的思想就是如何实现循环:利用(rear+1)% 数组长度
在结构体中定义一个数组指针,并定义front与rear来广义的表示队尾队首元素的下标
front指向的是队首元素 rear指向的是队尾元素的下一个元素
初始时:新建一个数组,并将front与rear的值置零
入队:将值存入rear代表的位置,将rear更新为(rear+1)% 数组长度
出队:取出front代表位置的元素,将front更新为(front+1)% 数组长度
判断队空:rear == front
判断队满:(rear+1)% 数组长度 == front (rear紧挨front)
九、递归
9.1 递归的定义
一个函数自己直接或间接调用自己
9.2 递归需满足三个条件
-
递归必须得有一个明确的终止条件
-
该函数所处理的数据规模必须在递减
-
这个转化必须是可解的
9.3 循环和递归的关系
递归:
-
易于理解
-
速度慢
-
存储空间大
循环:
-
不易理解
-
速度快
-
存储空间小
9.4 函数调用的细节
当在一个函数的运行期间调用另一个函数的时候,在运行被调函数之前,系统需要完成三件事:
-
将所有的实参传递给被调函数保存,将返回地址(调用该函数语句的下一个 语句的地址)传递给被调函数保存。
-
为被调函数的局部变量(包括形参)分配存储空间
-
将控制转移到被调函数的入口
从被调函数返回主调函数之前,系统需要完成三件事:
-
保存被调函数的返回结果
-
释放被调函数所占的存储空间
-
依照被调函数保存的返回地址将控制转移到调用函数
当有多个函数相互调用时,按照“后调用先返回”的原则,上述函数之间信息传递和控制转移必须借助”栈”来实现,即系统将整个程序运行时所需的数据空间安排在一个栈中,每当调用一个函数时,就在栈顶分配一个存储区,进行压栈操作,每当一个函数退出时,就释放它的存储区,进行出栈操作,当前运行的函数永远都在栈顶位置。
9.5 递归举例
求 n 的阶乘
递归的问题首先要找到递推公式,看是否符合某一个递推公式,例如n的阶乘。
n的阶乘可以由 n 乘 n - 1 的阶乘来实现,将处理的数据规模递减。
其次,找到递推公式以后,需找出递归终止条件。
当n等于0时,n的阶乘为1
#include <iostream>
#define LL long long
using namespace std;
// n! = n * (n-1)!
LL f(int n)
{
if (n == 0) return 1;
else return n * f(n-1);
}
int main()
{
cout << f(5) << endl;
return 0;
}
汉诺塔问题:
将A柱上的所有盘子借助B柱移到C柱上,每次只能移动一个盘子,且每个柱子上的盘子必须从大到小堆叠。
分析:
如果A上只有一个盘子,那么直接将盘子从A放到C
如果A上的盘子等于n且n大于1,那么分以下三个步骤解决:
第一步,先把A上n-1个盘子借助C移动到B
第二步,将A上第n个盘子移动到C
第三步,将B上n-1个盘子借助A移动到C
如果A上的盘子有3个,那么就会变成两个有2个盘子的问题,递推使得数据规模越来越小
代码如下:
#include <iostream>
using namespace std;
void hannuota(int n, char A, char B, char C)
{
if (n == 1) cout << "将编号为" << n << "的盘子从" << A << "移到" << C << endl;
else
{
hannuota(n-1, A, C, B);
cout << "将编号为" << n << "的盘子从" << A << "移到" << C << endl;
hannuota(n-1, B, A, C);
}
}
int main()
{
int n;
cin >> n;
hannuota(n, 'A', 'B', 'C');
return 0;
}
此外,树和森林就是以递归的方式定义的
树和图的很多算法都是以递归实现的
数学的很多公式也是以递归的方式定义的
十、树
以上的存储结构(逻辑结构)都是线性的结构
从现在开始,树和图都是非线性结构
10.1 树的定义
专业定义:
-
有且只有一个称为根的节点
-
有0个或多个互不相交的子树,这些子树本身也是一颗树
通俗的定义:
-
树是由节点和边组成
-
每一个节点只有一个父节点,但可以有0个或多个子节点
-
但有一个节点例外,该节点没有父节点,此节点称为根节点
专业术语:
-
节点
-
父节点
-
子节点
-
子孙
-
堂兄弟:同一层不同父节点
-
深度:树中节点的最大层次,从根节点到最底层节点的层数(根节点是第一层)
-
叶子节点:没有子节点的节点
-
非终端节点:实际就是非叶子节点
-
根节点:可以是叶子节点(空树)也可以是非叶子节点
-
度:子节点的个数
10.2 树的分类
-
一般树
任意一个节点的子节点的个数都不受限制
-
二叉树
任意一个节点的子节点的个数都是两个
且
子节点的位置不可更改,即二叉树是有序树,左子树和右子树互换位置就是不同的二叉树
-
二叉树的分类
-
一般二叉树
-
满二叉树:一颗深度为k且有2k-1个节点的二叉树称为满二叉树。在不增加树的层数前提下,无法再添加一个节点的二叉树。
-
完全二叉树:如果只是删除了满二叉树最底层最右边的连续若干个节点,这样形成的二叉树就是完全二叉树,满二叉树是完全二叉树的一个特例(一个都不删)。利用数组来存储二叉树,必须使用完全二叉树来实现
-
-
-
森林
n个互不相交的树的集合
10.3 树的存储
二叉树的存储
-
连续存储【完全二叉树】
-
优点:
查找某个节点的父节点和子节点(也包括判断有没有子节点)速度很快
-
缺点:
耗用内存空间过大
-
-
链式存储
-
优点:
耗用内存空间小
-
一般树的存储
-
双亲表示法:
-
结构体数组实现,每个元素可以存放在数组中的任意位置,每个结构体元素的一个属性来用来表示数据,另一个属性用来表示它的父节点在数组中的位置。【存放的一般树是无序的】
该方法求父节点方便
-
孩子表示法:
-
结构体数组实现,每个元素可以存放在数组中的任意位置,每个结构体元素的一个属性用来表示数据,另一个属性是一个指针域,指向一个链表,该链表中存放的是其孩子们的地址。
该方法求子节点方便
-
双亲孩子表示法
-
结构体数组实现,将前面两个表示方法组合在一起
该方法求父节点和子节点都很方便
-
二叉树表示法
把一个普通树转换为二叉树来存储
具体转换方法:将每一个节点的左指针指向它的第一个孩子,右指针指向它挨着的兄弟
森林的存储
假设有三个树,每个树的根节点分别是A、B、C
将每一个一般树转换为二叉树,然后将B当做A的兄弟挂到A的右指针上,将C当做B的兄弟挂到B的右指针上。
这样的话这个森林就转换为了一个二叉树。
10.4 树的操作
二叉树的遍历
-
先序遍历【先访问根节点】
先访问根节点,再先序访问左子树,再先序访问右子树
-
中序遍历【中间访问根节点】
中序遍历左子树,再访问根节点,再中序遍历右子树
-
后序遍历【最后访问根节点】
后序遍历左子树,再后序遍历右子树,再访问根节点
已知两种遍历序列求原始二叉树
-
已知树的先序遍历和中序遍历
-
已知树的中序遍历和后序遍历
-
已知树的先序遍历和后序遍历不能推出原始的二叉树!!!只有知道先中或者中后两种序列,才可以唯一的确定一颗二叉树
10.5 树的应用
-
树是数据库中数据组织一种重要的形式
-
操作系统子父进程的关系本身就是一颗树
-
面向对象语言中类的继承关系本身就是一颗树
-
哈夫曼编码
10.6 链式二叉树代码演示
首先,定义二叉树节点结构体:
//定义节点结构体
typedef struct BTNode
{
int data;//数据域
struct BTNode * pLchild;//左指针指向左孩子
struct BTNode * pRchild;//右指针指向右孩子
}BTNODE, * PBTNODE;
定义操作二叉树的函数:
void CreateBiTree(PBTNODE * T);//动态创建链式二叉树
void pre_order(PBTNODE T);//前序遍历
void middle_order(PBTNODE T);//中序遍历
void post_order(PBTNODE T);//后序遍历
实现操作二叉树的函数:
void CreateBiTree(PBTNODE * T)
{
int val;//用于输入该节点数据域的值
cin >> val;
if (val)//假定输入0表示该节点为空
{
*T = new BTNODE;//为该节点分配存储空间
(*T)->data = val;
CreateBiTree(&((*T)->pLchild));//递归创建该节点的左子树
CreateBiTree(&((*T)->pRchild));//递归创建该节点的右子树
}
else
{
*T = NULL;
}
}
void pre_order(PBTNODE T)
{
if (T != NULL)
{
cout << T->data << " ";
pre_order(T->pLchild);
pre_order(T->pRchild);
}
}
void middle_order(PBTNODE T)
{
if (T != NULL)
{
middle_order(T->pLchild);
cout << T->data << " ";
middle_order(T->pRchild);
}
}
void post_order(PBTNODE T)
{
if (T != NULL)
{
post_order(T->pLchild);
post_order(T->pRchild);
cout << T->data << " ";
}
}
关于动态二叉树创建函数:
首先,我们必须传一个带参的创建函数,因为要使用到递归
其次,链式二叉树只需知道了二叉树根节点的地址以后就可以确定整个二叉树,而链式栈和链式队列不同,由于操作需要受到限制,所以需要使用两个指针来指向栈或队列,我们为了传参方便所以就又定义了一个结构体来存放这两个指针,然后进行传参的时候,传的是这个指针结构体的地址。
对于该链式二叉树,我们只需要一个指针就可以确定整个二叉树,所以没有定义指针结构体来进行传参,所以同样的,这里就需要用到指针的指针来进行参数的传递了。
我们传递指向该二叉树根节点的指针的指针,是希望通过create函数,将整个二叉树动态的创建起来。我们希望改变的内容是指向该二叉树根节点的指针的值,而不是改变该二叉树的根节点的值,所以我们在跨函数的时候就需要传递该指针的指针。
测试:
将以下二叉树存入内存中并遍历出来:
首先将该二叉树转换为以下形式:节点的左指针或右指针为空时,输入0
根据先序遍历,得到输入数据应为:
1 2 0 0 3 4 0 5 0 0 0
编写main函数测试:
int main()
{
PBTNODE T = NULL;//创建一个指向二叉树根节点的指针
cout << "请输入各个节点的值,用以创建该二叉树:" << endl;
CreateBiTree(&T);//我们希望通过调用函数 T可以指向二叉树的根节点 要改变的是T的内容 所以发送的T的地址
cout << "二叉树创建完毕!" << endl;
cout << "先序遍历:";
pre_order(T);
cout << endl;
cout << "中序遍历:";
middle_order(T);
cout << endl;
cout << "后序遍历:";
post_order(T);
cout << endl;
return 0;
}
结果为:
测试成功!!!
十一、数据结构代码总结
单链表的创建
// 定义节点数据类型
typedef struct Node
{
int data;//数据域
struct Node * next;//指针域
} NODE, * PNODE;
int main()
{
PNODE pHead = NULL;
pHead = create_list();
}
单链表创建的时候,我们在主函数定义一个指向第一个节点的指针,通过create函数返回头节点的地址来给指针赋值,完成单链表的创建。
链式二叉树的创建
//定义节点结构体
typedef struct BTNode
{
int data;//数据域
struct BTNode * pLchild;//左指针指向左孩子
struct BTNode * pRchild;//右指针指向右孩子
}BTNODE, * PBTNODE;
int main()
{
PBTNODE T;//创建一个指向二叉树根节点的指针
CreateBiTree(&T);//我们希望通过调用函数 T可以指向二叉树的根节点 要改变的是T的内容 所以发送的T的地址
}
链式二叉树创建的时候,我们同样也可以和单链表创建一样的方式,通过create函数返回根节点的地址来给指针赋值,但是,动态创建二叉树的过程需要使用到递归,这样就要求create函数需要有形参。那么我们就要想怎么才能将指针传进create函数中并改变指针的值,所以就用到了指针的指针。将create函数的形参改为指针的指针,发送main函数中指针的地址就可以完成了。
链栈的创建
// 定义节点结构体
typedef struct Node
{
int data;//数据域
struct Node * next;//指针域
}NODE, * PNODE;
// 定义栈结构体
typedef struct Stack
{
PNODE pTop; //指向栈顶
PNODE pBottom; // 指向栈底的下一个没有实际含义的元素
}STACK, * PSTACK;
int main()
{
STACK S; //定义一个栈
init_stack(&S);
}
链栈在创建的时候就有所不同了,因为一个指针参数根本不能完成栈所需的操作。所以我们定义了两个指针,而两个指针在进行参数传递的时候又太麻烦,所以将这两个指针定义到了一个结构体中。
链栈的创建和链式二叉树的创建思路一样,也是想在init函数中来改变指针的值,所以要发送两个指针的地址,由于这两个指针用一个结构体定义了,所以我们发送结构体的地址即可。
链式队列的创建
//定义节点结构体
typedef struct Node
{
int data;//数据域
struct Node * next;//指针域
}NODE, * PNODE;
//定义队列结构体
typedef struct Queue
{
PNODE pFront;// 指向队首
PNODE pRear;// 指向队尾
}QUEUE, * PQUEUE;
int main()
{
int val;
QUEUE Q;
init_queue(&Q);
}
链式队列的创建方式与链栈一样,这里不再赘述。
十二、查找和排序
12.1 二分查找
与线性查找不同,二分查找只适用于有序的数组。
其实现代码如下:
#include <iostream>
using namespace std;
//若找到 则返回索引值 若未找到 则返回-1
int binary_search(int * arr, int len, int val);
int main()
{
int res, val;
int arr[10] = {-98, -34, 2, 34, 54, 66, 79, 105, 210, 333};
cout << "输入您要查找的值:";
cin >> val;
res = binary_search(arr, 10, val);
if (res != -1)
{
cout << "值为" << val << "的元素在数组的第" << res << "个位置上" << endl;
}
else
{
cout << "未找到值为" << val << "的元素" << endl;
}
return 0;
}
int binary_search(int * arr, int len, int val)
{
int begin = 0;
int end = len - 1;
int flag = -1;//默认未找到
while(begin <= end)
{
int mid = (begin + end) / 2;
if (arr[mid] == val)
{
flag = mid;
break;
}
if (arr[mid] < val) begin = mid + 1;
if (arr[mid] > val) end = mid - 1;
}
return flag;
}
12.2 排序
-
冒泡排序
-
插入排序
-
选择排序
-
快速排序
-
归并排序
排序和查找的关系:
排序是查找的前提
排序是重点
排序算法的实现:冒泡、插入、选择、快排
冒泡排序:
void bubble_sort(int *arr, int len)
{
int i, j, k;
for (i = 0; i < len-1; i++)
{
for (j = 0; j < len-1-i; j++)
{
if (arr[j] < arr[j+1])
{
k = arr[j];
arr[j] = arr[j+1];
arr[j+1] = k;
}
}
}
}
选择排序:
void select_sort(int *arr, int len)
{
int i, j, k, max;
for (i = 0; i < len-1; i++)
{
max = i;
for (j = i+1; j < len; j++)
{
if (arr[max] < arr[j])
{
max = j;
}
}
if (max != i)
{
k = arr[max];
arr[max] = arr[i];
arr[i] = k;
}
}
}
插入排序:
void insert_sort(int *arr, int len)
{
int i, j, value;
for (i = 0; i < len-1; i++)
{
value = arr[i+1];
for (j = i+1; j > 0 && value > arr[j-1]; j--)
{
arr[j] = arr[j-1];
}
arr[j] = value;
}
}
快速排序:
通过一趟排序将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分关键字小,则分别对这两部分继续进行排序,直到整个序列有序。核心思想就是如何找到选定元素在整个数组中的确定位置 加 递归。
int find_pos(int *arr, int low, int high)
{
int value = arr[low];
while (low < high)
{
while (low < high && arr[high] <= value) high--;
arr[low] = arr[high];
while (low < high && arr[low] >= value) low++;
arr[high] = arr[low];
}
arr[low] = value;
return low;
}
void quick_sort(int *arr, int low, int high)
{
int pos;
if (low < high)
{
pos = findPos(arr, low, high);
quick_sort(arr, low, pos-1);
quick_sort(arr, pos+1, high);
}
}