一、概述
数据结构整体的思维导图,上传的xmind版:数据结构思维导图。如下图片格式,可能不是很清晰。系列是以大话数据结构书为基准进行的读记、总结与扩展。
数据结构基本概念总结
数据结构 | |
基本概念:是相互之间存在一种或多种特定关系的数据元素的集合。是一门研究非数值计算的程序设计问题中的操作对象,以及它们之间的关系和操作等相关问题的学科 | |
数据 | 描述客观事物的符号,是计算机中可以操作的对象,能被计算机识别并输入给计算机处理的符号集合 |
数据元素 | 是组成数据有一定意义的基本单位,在计算机中通常作为整体处理。也被称为记录。 |
数据项 | 一个数据元素可以由若干个数据项组成。是数据不可分割的最小单位。 |
数据对象 | 是性质相同的数据元素的集合,是数据的子集 |
逻辑结构 | |
集合 | 集合结构中的数据元素除了同属于一个集合外,它们之间没有其他关系。 |
线性结构 | 线性结构中的数据元素之间是一对一的关系 |
树 | 树形结构中的数据元素之间存在一种一对多的层次关系 |
图 | 图形结构的数据元素是多对多的关系。 |
其实就是线性和非线性两种。线性结构包括:数组、链表、栈、队列、哈希表;非线性结构包括: 树、堆、图、哈希表。(哈希表底层数组,解决冲突可以使用链表) | |
物理结构 | |
是指数据的逻辑结构在计算机中的存储形式。数据元素的存储形式有两种:顺序存储和链式存储,其对应的特点就是存储地址的连续与分散。 | |
顺序存储 | 是把数据元素存放在地址连续的存储单元里,数据间的逻辑关系和物理关系是一致的(依次排队) |
链式存储 | 把数据放在任意的存储单元,这组单元可以连续也可以不连续,数据的存储不能感应逻辑关系。 |
数据类型:是指一组性质相同的值的集合及定义在此集合上的一些操作的总称(数据类型决定其可以进行哪些类被的操作)。抽象数据类型:抽象是指抽取事物具有的普遍性的本质,抽象数据类型是指一个数学模型及定义在该模型上的一组操作。
二、算法
算法基本概念
算法及特点 | |
含义 | 是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作 |
输入 | 算法有零个或者多个输入 |
输出 | 算法至少有一个或多个输出 |
有穷性 | 指算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成。(不能死循环)。 |
确定性 | 算法的每一步骤都具有确定的含义,不会出现二义性。(一定条件下算法只有一条执行路径 |
可行性 | 算法的每一步都必须是可行的,也就是说,每一步都能够通过执行有限次数完成 |
算法设计要求 | |
正确性 | 算法的正确性是指算法至少应该具有输入、输出和加工处理无歧义性、能正确反映问题的需求、能够得到问题的正确答案。(1、无语法错误。2、对合法输入产生正确结果。3、对非法输入产生满足要求的结果。4、满足测试数据)。 |
健壮性 | 当输入数据不合法时,算法也能做出相关处理,而不是产生异常或莫名其妙的结果。 |
算法分析 | |
算法分析的目的就是找的高效的解决算法,评估算法的指标有时间复杂度与空间复杂度。对于算法的估计(事前估计和事后统计)。 | |
事前估计与统计 | 时间取决于1、算法的策略、方法。2、编译产生的代码质量。3、问题的输入规模。4、机器指令执行速度。 |
复杂度分析 | 一个程序的运行时间依赖于算法的好坏和问题的规模,问题规模是输入量)。所以可以考虑仅通过一些计算来评估算法的效率, 复杂度分析能够体现算法运行所需的时间和空间资源与输入数据大小之间的关系。它描述了随着输入数据大小的增加,算法执行所需时间和空间的增长趋势。 |
算法度量
时间复杂度
函数的渐近增长:给定两个函数 f(n)和 g(n) ,如果存在一个整数 N,使得对于所有的 n > N, f(n)总是比 g(n) 大,那么,我们说 f(n)的增长渐近快于 g(n)。判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数。
算法时间复杂度定义:在进行算法分析时,语句总的执行次数 T(n)是关于问题规模 n的函数,进而分析 T(n)随 n 的变化情况并确定 T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作: T(n)= O(f(n))。它表示随问题规模 n 的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。其中 f(n)是问题规模 n 的某个函数。
推导大 O 阶:
1.用常数 1 取代运行时间中的所有加法常数。
2.在修改后的运行次数函数中,只保留最高阶项。
3.如果最高阶项存在且不是 1,则去除与这个项相乘的常数。得到的结果就是大 O 阶。
O(1) <O(logn)< O(<n)<O(nlogn)< O() <O(
) <O(
)<O(n!)<O(
)
算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作: S(n)= O(f(n))
O(f(n)) | 举例 | note |
O(1) | | 时间复杂度分析是输入规模n的关系,即使运行时间长和n无关 |
O(logn) | | 由于每轮缩减到n的一半,因此循环次数是 ,即 |
O(n) | | 线性阶的操作数量相对于输入数据大小 n以线性级别增长,常见单层循环 |
nlog(n) | | 线性对数阶常出现于嵌套循环中,两层循环的时间复杂度分别为 O(logn) 和 O(n)例如快速排序、归并排序、堆排序. |
O( | | 相对于输入数据大小n以平方级别增长,冒泡排序 |
O( | | 其递归地一分为二,经过n次分裂 |
O(n!) | | 第一层分裂出 n 个,第二层分裂出 n−1 个,以此类推,直至第 n 层时停止分裂 |
算法的时间效率往往不是固定的,而是与输入数据的分布有关,也就有了最差、最佳、平均时间复杂度,一般使用平均时间复杂度反映算法在随机输入数据下的运行效率。 |
空间复杂度:
空间复杂度的推算方法与时间复杂度大致相同,只需将统计对象从“操作数量”转为“使用空间大小”。 空间通常只关注最差空间复杂度。
算法在运行过程中使用到的内存有:用于存储算法的输入数据,用于存储算法在运行过程中的变量、对象、函数上下文等数据,用于存储算法的输出数据。 一般情况下,空间复杂度的统计不包括输入,因为这是不可避免的需求,而在求解过程中计算你使用的结构空间的需求。比如你使用了数组或者链表,如申请大小和n一致的大小,那空间复杂度O(n)起步,还有函数调用中栈的使用。
三、线性表
线性表基本概念及存储
线性表 | |
定义 | 零个或多个数据元素(数据元素可以多个数据项)的有限序列(除第一个无前驱和最后一个无后续 |
表长 | 线性表元素个数定义为表的长度,n=0时称为空表。 |
线性表的存储结构 | |
顺序存储 | 用一连续地址存储单元依次存储线性表的元素,存取O(1),插入与删除时的时间复杂度是O(n) 平均移动n/2的元素,适合元素不多而且较多存取操作。 |
链式存储 | 在链式存储中不仅要存储元素还要存储后继元素的存储地址。(数据域与指针域组合成结点)。n个结点组合成链表。链表第一个结点的存储位置为头指针。不需要连续存储空间。 |
线性表的优缺点:无需为表的逻辑结构增加额外的存储空间,可以快速的存取表中的任何一位置的元素O(1),缺点:插入和删除移动大量的元素O(n),当线性表长度变化较大时难以确定存储空间的容量,造成存储空间的碎片。 |
顺序存储操作-数组:
线性表的基本操作(连续存储的数组):
# include <iostream>
using namespace std;
// 在数组的索引 index 处插入元素 num
void insert(int *nums, int size, int num, int index) {
//元素移动
for (int i = size - 1; i > index; i--) {
nums[i] = nums[i - 1];
}
nums[index] = num;
}
// 删除索引 index 处的元素
void remove(int *nums, int size, int index) {
// 把索引 index 之后的所有元素向前移动一位
for (int i = index; i < size - 1; i++) {
nums[i] = nums[i + 1];
}
}
//遍历元素
void prit(int *arr,int size){
for (int i = 0; i < size; i++) {
cout<<arr[i]<<" ";
}
cout<<endl;
}
int main(int argc, char const *argv[])
{
/* 初始化数组 */
int arr[5]= { 1, 2, 3, 4, 5 };
//元素访问
cout<<arr[0]<<endl; //1
//遍历数组
for(int i=0;i<sizeof(arr)/sizeof(arr[0]);i++)
cout<<arr[i]<<" ";
cout<<endl; //1 2 3 4 5
//插入元素
insert(arr,5,0,0);
prit(arr,5); //0 1 2 3 4
//删除元素
remove(arr,5,0);
prit(arr,5); //1 2 3 4 4
return 0;
}
线性表链式存储操作-链表
链表 | ||
链表是一种线性数据结构,其中的每个元素都是一个节点对象,各个节点通过指针相连接。指针记录了下一个节点的内存地址,通过它可以从当前节点访问到下一个节点。 | ||
头结点:是为了操作统一和方便设立的放在第一个元素前面,其数据域一般无意义,有了头结点对第一个元素的节点前的插入和删除操作就统一了。它不一定是链表必要。 | 头指针:是指向第一个结点的指针,如链表有头结点则指向头结点,无论链表是否为空,头指针均不为空,头指针是链表的必要元素。 | |
链表类型 | 概念 | 操作 |
单向链表 | 单向链表的节点包含值和指向下一节点的引用两项数据。我们将首个节点称为头节点,将最后一个节点称为尾节点,尾节点指向空 NONE, 查找时间复杂度O(n),插入与删除O(1), | 插入s: s->next=p->next; p->next=s; 删除q:q=p->next; p->next=q->next; 判空: head=null,带头节点head->next=null |
循环链表 | 将单链表的终端结点的指针由空改为指向头结点,就是整个单链表形成一个环形成单链表。循环链表的操作与单链表基本一致,差别在于算法中的循环条件不是p或者p->next是否为空, 而是他们是否等于头指针。 | 判空:head=NULL,带头节点head->next=head |
双向链表 | 在双向链表的结点中有两个指针域, 其一指向直接后继,另一指向直接前驱。 | 插入s:s->prior=p; s->next=p->next; p->next->prior=s; p->next=s; 删除节点p: p->prior->next=p->next; p->next->prior=p->prior; free(p); |
静态链表 | 用数组描述的链表叫静态链表,这里的数组元素包含两个数据域(data和cur)cur相当于链表的next指针叫游标。静态链表的优缺点:在插入和删除操作时只需要移动游标,不需要移动元素。缺点: 没有解决存储分配带来的表长难以确定的问题,失去顺序存储结构随机存储的特性 | |
链表的优缺点:需要为表的逻辑结构增加额外的存储空间(地址),可以快速的增加或删除表中的任何一位置的元素O(1),缺点:查找元素O(n),可灵活扩展。但更容易导致内存碎片化 |
合并循环链表(A、B):p=rearA->next(保存A的头指针); rearA->next=rearB->next->next(B的头节点非头指针); rearB->next=p(B的尾指向A的头指针); free(p);
三、栈和队列
栈
栈的基本概念
定义 | 是限定仅在表尾进行插入和删除操作的线性表。把允许插入和删除的一端称为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈。栈又称为后进先出的线性表,简称LIFO结构(线性表就有线性表的性质,但栈却不提供搜索 |
入栈 | 栈的插入操作,叫做进栈,也称压栈、入栈 |
出栈 | 栈的删除操作,叫做出栈,也称弹栈。出栈可能序列数目(通项计算C(2n,n)/(n+1) |
栈顶 | top无元素是-1,(出栈和进栈不涉及循环时间复杂度O(1))。 |
共享栈 | 两栈共享空间: top1+1 == top2 栈满。链栈基本不存在栈满。链栈的出栈操作是top=top->next. 入栈s->next=top; top->s; |
栈应用 | 递归、四则运算表达式求值(逆波兰:一种不需要括号的后缀表达式) |
栈的应用:
递归调用
1、递归(斐波那契数列),直接调用自己或者通过语句间接调用自己的函数。在递的阶段对于每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中,在归的阶段,位于栈顶的局部变量、参数和返回值地址都被弹出,用于返回调用层次中执行代码的其余部分,也就是恢复调用的状态。
表达式求值
2、四则运算表达式求值(逆波兰:一种不需要括号的后缀表达式)表达式:9 3 1 – 3 * + 10 2 / +。规则:从左到右遍历表达式中的每个数字和符号, 遇到是数字就进栈, 遇到事符号就就将栈顶两个数字取出进行计算, 运算结果进栈, 一直到最终获得结果。
后缀表达式转换
3、中缀表达式转后缀表达式:中缀表达式“9+(3-1)*3+10/2”转化为后缀表达式“9 3 1 3 – 3 * + 10 2 / +” 规则:从左到右遍历表达式的每个数字和符号,若是数字就输出,就成为后缀表达式的一部分;若是符号,则判断与其栈顶符号的优先级,是右括号或者优先级低于栈顶元素则栈顶元素以此出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式。中缀转后缀(栈用来存运算符号),计算后缀表达式的结果,栈用来进出的数字。
栈的基本操作
#include <iostream>
#include <stack>
int main() {
std::stack<int> my_stack;
// 推送元素
my_stack.push(1);
my_stack.push(2);
my_stack.push(3);
// 查看栈顶元素
std::cout << "栈顶元素: " << my_stack.top() << std::endl;
// 弹出元素
my_stack.pop();
// 查看栈的大小
std::cout << "栈的大小: " << my_stack.size() << std::endl;
// 判断栈是否为空
if (my_stack.empty()) {
std::cout << "栈为空" << std::endl;
}
return 0;
}
堆
从数据结构上讲堆是一种经过排序的树形数据结构,每个结点都有一个值。通常是指二叉堆。堆的特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。由于堆的这个特性,常用来实现优先队列,堆的存取是随意。具体在树之后进一步探索。
内存管理上堆与栈的区别 | ||
差异 | 栈 | 堆 |
申请方式 | 系统自动分配 | 人为申请开辟 |
增长方向 | 栈的生长方向向下,内存地址由高到低 | 堆的生长方向向上,内存地址由低到高 |
申请大小 | 获得的空间较小 | 获得的空间较大,堆的大小受限于计算机系统中有效的虚拟内存 |
申请效率 | 速度较快,但程序员是无法控制 | 较慢,而且容易产生内存碎片但用起来方便 |
存储内容 | 函数调用语句的下一条可执行语句的地址,函数各个参数、其中静态变量是不进栈。 | 而堆中一般是在头部用一个字节存放堆的大小,堆中的具体内容是人为安排的。 |
底层不同 | 连续的空间 | 不连续空间,这是由于系统是用链表来存储空闲内存地址的,而链表的遍历方向是由低地址向高地址。 |
队列
队列的基本概念
定义 | 只允许在一端进行插入操作,而在另一端进行删除操作的线性表。队列是一种先进先出的线性表,简称FIFO,模拟我们的排队现象 |
头、尾 | 允许插入的一端称为队尾,允许删除的一端称为队头。front指向队头元素,rear指向队尾元素的下一个位置,当front=rear时为队空。 |
循环队列 | 队列头尾相接的顺序存储结构称为循环队列,判断为空还是满,一种加标志位。队空rear=front, 若队列的最大尺寸为QueueSize,那么队列满的条件是(rear + 1) % QueueSize == front。 |
单向队列 | 队空、队满时 front==rear |
求元素数 | rear-front+Max)%Max 该公式中的 rear指向队尾元素的下一个位置,front为当前队头元素位置 |
出入队 | 入队rear+1)%QueueSize ,出队front+1)%QueueSize。 |
比较常见的消息队列产品主要是ActiveMQ、RabbitMQ、Apollo、ZeroMQ、Kafka、MetaMQ、RocketMQ、Redis(用list作数据类型)。
队列的链式存储:队列的链式存储结构其实就是线性表的单链表,只不过它只能头出尾进,我们把它简称为队列。
队列的基本操作
#include <queue>
#include <deque>
#include <algorithm>
#include <iostream>
using namespace std;
void print_deque(const deque<int>& d)
{
cout << "deque: ";
for (deque<int>::const_iterator it = d.begin(); it != d.end(); it++)
{
cout << *it << " ";
}
cout << endl;
for (int i = 0; i < d.size(); i++)
cout << d[i] << " ";
cout << endl;
}
int main()
{
// 队列
queue<int> q1;
for (int i = 0; i < 5; i++)
{
q1.push(i); //入队
}
cout<<q1.front()<<" "<< q1.back()<<endl; //取队首队尾元素
q1.pop(); // 出队操作
cout<<q1.front()<<" "<< q1.back()<<endl; //取队首队尾元素
cout<<(q1.empty()? "empty":"not empty")<<endl;
// 双端队列
deque<int> dq;
//尾插
dq.push_back(1);
dq.push_back(2);
//头插
dq.push_front(10);
dq.push_front(20);
print_deque(dq);
//尾删
dq.pop_back();
print_deque(dq);
//头删
dq.pop_front();
print_deque(dq);
dq.insert(dq.begin(), 3);
print_deque(dq);
dq.insert(dq.begin(), 3, 4);
print_deque(dq);
//删除
deque<int>::iterator it = dq.begin();
it++; //第二个元素
dq.erase(it); //dq.erase()为删除所有; dq.clear()也为清空容器所有数据
print_deque(dq);
return 0;
}