1、什么是数据结构
1、数据结构的起源
1968年,美国高德纳教授,《计算机程序设计艺术》第一卷《基本算法》,开创了数据结构和算法的先河。
数据结构是研究数据之间关系和操作的学科,而非计算方法。
数据结构+算法=程序 美国沃斯提出 这句话揭示了程序的本质。
2、数据结构相关基础概念
数据:所有能够输入到计算机中,能够被程序处理的描述客观事物的符号
数据项:有独立含义的数据的最小单位,也称为域
数据元素:组成数据的有一定含义的基本单位,也称为节点、结点、记录
数据结构:相互之间存在一种或多种特定关系的数据元素的集合
算法:数据结构中所具备的功能、解决某种特定问题的方法
3、数据结构的三个方面
数据的逻辑关系
数据的存储关系
数据结构的运算
二、数据结构的逻辑关系和存储关系
逻辑关系:
分类一:
集合:数据元素同属于同一个集体,但元素之间没有任何关系
线性结构:数据元素之间存在一对一关系
树形结构:数据元素之间存在一对多的关系
图型(网络)结构:数据元素之间存在多对多的关系
分类二:
线性结构是一个有序数据元素的集合。常用的线性结构有:线性表,栈,队列,双队列,数组,串。
非线性结构,其逻辑特征是一个结点元素可能有多个直接前趋和多个直接后继。常见的非线性结构有:二维数组,多维数组,广义表,树(二叉树等)。
存储(物理)关系:
顺序结构:
数据元素存储在连续的内存中,用数据元素的相对位置来表示关系(例:数组)
优点:支持随机访问、访问查找效率极高、适合用于查找数据频繁的结构
缺点:插入、删除时效率低不方便、内存空间利用率低、要求高
链式结构:
数据元素存储在彼此相互独立的内存中,每个独立的元素也叫做结点,每个结点中增加一项数据项用于存储其他相关结点的地址,以此表示节点之间的关系
优点:插入、删除更方便,空间利用率极高、对内存要求不高
缺点:不支持随机访问,只能从头到尾逐个访问、访问查找效率低、不适用于查找数据频繁的结构
注意:逻辑结构、存储结构采用哪种根据实现难度、空间、时间要求、操作习惯等方面综合考虑选择合适的结构
逻辑结构与存储结构的关系
线性表(顺序表、链表、栈和队列等) 顺序 链式
树(普通树,二叉树,线索二叉树等) 链式 顺序
图() 顺序+链式
三、数据结构的运算
1、创建数据结构 create creat
2、销毁数据结构 destory
3、清空数据结构 clean
4、数据结构排序 sort
5、插入元素 insert
6、删除元素 delete
7、访问元素 access
8、查询元素 query
9、修改元素 modify
10、遍历数据结构 ergodic show print
四、线性表-顺序表和链式表
顺序表
数据项:
存储元素的连续内存首地址
表的容量
当前元素的数量
例:
#define TYPE int
typedef struct Array
{
TYPE* ptr; // 存储元素的内存首地址
size_t cal; // 表的容量
size_t cnt; // 已存储元素的数量(也有的没有定个数)
}Array;
运算:
创建:
Array* create_array(size_t size)
{
// 分配顺序表结构的内存
Array* arr = malloc(sizeof(Array));
// 分配存储元素的内存
arr->ptr = malloc(sizeof(TYPE)*size);
// 记录容量
arr->cal = size;
// 初始化元素数量
arr->cnt = 0;
return arr;
}
、销毁
void destory_array(Array* arr)//释放指针
{
free(arr->ptr);
free(arr);
}
、清空(定数量的好处体现出来了)
void clean_array(Array* arr)
{
arr->cnt = 0;
}
、插入
bool insert_array(Array* arr,int index,TYPE val)
{
// 判断表是否已满
if(arr->cnt >= arr->cal) return false;
// 判断下标是否能让表保持连续性(防止插入下标过大)
if(index > arr->cnt) return false;
memmove(arr->ptr+index+1,arr->ptr+index,
(arr->cnt-index)*sizeof(TYPE));//从index开始整体往后挪一位
arr->ptr[index] = val;//空位补上插入数
arr->cnt++;
return true;
}
剩下的 删除、访问、查询、修改、排序、遍历 也一样封装成函数,方便调用
注意:1、要保持数据元素的连续性 2、不要越界
链式表:list
元素(结点)的数据项:
数据域:可以是任何类型的若干个数据项
指针域:指向下一个结点
#define TYPE int
// 设计链表的结点
typedef struct Node
{
TYPE data; // 结点数据域
struct Node* next; // 结点指针域
}Node;
// 创建结点
Node* create_node(TYPE data)
{
// 分配结点所需内存
Node* node = malloc(sizeof(Node));
node->data = data;
node->next = NULL;
return node;
}
有若干个结点通过指针域连接到一起就形成了链表
不带头结点的单链表:
第一个结点的数据域存储有效数据
缺点:添加、删除结点时,可能会修改指向第一个结点的指针,参数需要使用二级指针,才能更改指针指向,比较麻烦
带头结点的单链表:
第一个结点的数据域不存储有效元素,仅仅只是使用它的指针域永远指向链表的第一个数据有效的结点
优点 :添加、删除时会比不带头节点的链表更方便
注意:带头结点的单链表其他的操作要从头节点的下一个结点开始
链表也有创建、销毁、清空、插入、删除、访问、查询、修改、排序、遍历等运算
功能受限的表结构
对表结构的部分功能加以限制,形成特殊的表结构
栈:
只有一个进出口的表结构,先进后出,FILO
顺序栈:
数据域:
存储元素的内存首地址:
栈的容量:
栈顶位置:
typedef struct ArrayStack
{
TYPE* ptr; // 存储元素内存首地址
size_t cal; // 栈的容量
size_t top; // 栈顶下标 从0开始 空增栈
}ArrayStack;
运算:
创建、销毁、入栈、出栈、栈顶、栈满、栈空
栈的应用:
1、函数的调用(先进后出)、栈内存特点
2、生产者和消费者模型(仓库->栈)
3、表达式解析3-3*20/2(中缀转后缀表达式)
栈的常见题:
例:某个序列是一个栈的入栈顺序,判断哪个是正确的出栈顺序
12345入栈
12345 出栈 可以 入1出1
32145 可
31245 不可
问题:两个容量相同的顺序栈,如何能够让两个空间利用率最高 ?
让两个栈相对着入栈(入口对入口)
练习一:实现一个函数,序列a为入栈,判断序列b是否为序列a的出栈顺序?
bool is_pop_stack(int a[],int b[],int len)
{
// 创建一个栈
ArrayStack* stack = create_array_stack(len);
for(int i=0,j=0; i<len; i++)
{
// 按a的顺序一个个入栈
push_array_stack(stack,a[i]);
// 按b的顺序尝试出栈
int val = 0;
while(top_array_stack(stack,&val) &&
val == b[j])
{
pop_array_stack(stack);
j++;
}
}
// 判断栈是否为空
bool flag = empty_array_stack(stack);
destory_array_stack(stack);
return flag;
}
链式栈:
栈结构数据项:
栈顶结点
结点数量
// 链表结点
typedef struct Node
{
TYPE data;
struct Node* next;
}Node;
// 创建结点
Node* create_node(TYPE data)
{
Node* node = malloc(sizeof(Node));
node->data = data;
node->next = NULL;
return node;
}
// 设计链式栈结构
typedef struct ListStack
{
Node* top; // 栈顶结点
size_t cnt; // 结点数量
}ListStack;
运算:
创建、销毁、入栈、出栈、栈空、栈顶
问题:顺序栈和链式栈的区别(类似链表和数组)
队列:
有两个端口,一个端口只能入队,另一个只能出队
先进先出 FIFO
1、顺序队列:存储元素的内存首地址、容量、队头下标(负责出队)、对尾下标(负责入队)
创建、销毁、入队、出队、队空、队满、查队头、查队尾、数量
注意点:1、由一维数组+队头下标front+队尾下标tail组成
入队tail++,出队front++,为了队列能够反复使用,我们把队列想象成环,因此当front和tail加一后都需要用队列容量求余再重新赋值
front=(front+1)%cal;
tail=(tail+1)%cal;
如何判断队空、队满:
额外多申请一个元素的内存
队空:front==tail
队满:(tail+1)%cal==front
代价是空了一个位置不能使用,计算数量时较为麻烦(最常考)
计算数量:(tail-front+cal)%cal
另一种方式是队列结构中多增加一项数据项用于记录队列中元素的个数,判断空或满直接判断该数据即可,更方便
2、链式队列
由链表结点组成的队列结构
队头指针:
队尾指针:
结点数量:
运算:
创建、销毁、队空、入队、出队、队头、队尾
队列的应用:
1、消息队列
2、树的层序遍历使用队列
3、图的广度优先遍历使用队列
4、封装线程池、数据池
练习:
使用两个顺序栈来模拟一个队列
栈二必须空栈,栈一才能入栈二
从栈一到栈二必须一个不留
queue
{
Stack* s1;
Stack *s2;
}