数据
数据是信息的载体,是描述客观事物属性的数、字符及所有能输入到计算机 中并被计算机程序识别和处理的符号的集合。数据是计算机程序加工的原料。
数据元素、数据项
数据元素是数据的基本单位,通常作为一个整体进行考虑和处理。 一个数据元素可由若干数据项组成,数据项是构成数据元素的不可分割的最小单位。
数据对象
数据对象是具有相同性质的数据元素的集合,是数据的一个子集。
数据类型
数据类型是一个值的集合和定义在刺激和上的一组操作的总称
数据结构
数据结构是相互之间存在一种或多种特定关系的数据元素的集合。
与数据的存储无关,独立于计算机。
顺序存储
把逻辑上相邻的元素存储在物理位置 上也相邻的存储单元中,元素之间的关系由存储 单元的邻接关系来体现。优点:实现随机存取;缺点:只能使用相邻的一整块存储单元,可能会产生外部碎片。
链式存储
逻辑上相邻的元素在物理位置上可以 不相邻,借助指示元素存储地址的指针来表示元 素之间的逻辑关系。优点:不会出现碎片现象,充分利用所有存储单元;缺点:每个元素因存储指针占用额外的存储单元,且只能实现顺序存取。
索引存储
在存储元素信息的同时,还建立附加 的索引表。索引表中的每项称为索引项,索引项 的一般形式是(关键字,地址)。优点:检索速度快;缺点:附加的索引表占用额外空间。
散列存储
根据元素的关键字直接计算出该元素 的存储地址,又称哈希(Hash)存储。优点:检索、增加、删除节点速度都很快;缺点:若散列函数不好,可能出现元素存储单元的冲突,解决冲突会增加时间开销。
针对于某种逻辑结构,结合实际需求,定义基本运算。运算的定义针对逻辑结构,运算的实现针对存储结构。
算法(Algorithm)是对特定问题求解步骤的一种描述,它是指令的有限序列,其中的每条指令 表示一个或多个操作
程序=数据结构+算法
“好”算法的特质
一个语句的频度是指该语句在算法中被重复执行的次数。算法中所有语句的频度之和记为T(n),它是该算法问题规模n的函数,时间复杂度主要分析T(n)的数量级。算法中基本运算(最深层循环内的语句)的频度与T(n)同数量级,因此通常采用算法中基本运算的频度f(n)来分析算法的时间复杂度。因此,算法的时间复杂度记为
T ( n ) = O ( f ( n ) ) T(n)=O(f(n)) T(n)=O(f(n))
取f(n)中随n增长最快的项,将其系数置为1作为时间复杂度的度量。
O ( 1 ) < O ( log 2 n ) < O ( n ) < O ( n l o g 2 n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1)
加法规则
T ( n ) = T 1 ( n ) + T 2 ( n ) = O ( f ( n ) ) + O ( g ( n ) ) = O ( max ( f ( n ) , g ( n ) ) ) T(n)=T_{1}(n)+T_{2}(n)=O(f(n))+O(g(n))=O(\max (f(n), g(n))) T(n)=T1(n)+T2(n)=O(f(n))+O(g(n))=O(max(f(n),g(n)))
乘法规则
T ( n ) = T 1 ( n ) × T 2 ( n ) = O ( f ( n ) ) × O ( g ( n ) ) = O ( f ( n ) × g ( n ) ) T(n)=T_{1}(n) \times T_{2}(n)=O(f(n)) \times O(g(n))=O(f(n) \times g(n)) T(n)=T1(n)×T2(n)=O(f(n))×O(g(n))=O(f(n)×g(n))
结论:
最坏时间复杂度:最坏情况下算法的时间复杂度
平均时间复杂度:所有输入示例等概率出现的情况下,算法的期望运行时间
最好时间复杂度:最好情况下算法的时间复杂度
只需关注存储空间大小 与问题规模相关的变量
算法的空间复杂度S(n)定义为该算法所耗费的存储空间,它是问题规模n的函数。记为
S ( n ) = O ( g ( n ) ) S(n)=O(g(n)) S(n)=O(g(n))
一个程序在执行时除需要存储空间来存放本身所用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为实现计算所需信息的辅助空间。若输入数据所占空间只取决于问题本身,和算法无关,则只需分析除输入和程序之外的额外空间。
算法原地工作是指算法所需的辅助空间为常量,即O(1)。
一般函数递归带来的空间复杂度位递归调用的深度
线性表是具有相同数据类型的n (n≥0)个数据元素的有限序列
顺序表-用顺序存储的方式实现线性表。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关 系由存储单元的邻接关系来体现。逻辑顺序与物理顺序相同。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UsvVWeNZ-1649239793327)(https://cdn.jsdelivr.net/gh/girlsdontget341/image@master/img/202203231125225.png)]
#define MaxSize 100
typedef struct {
int data[MaxSize];//静态数组存放数据元素
int length;
}SqList;//静态分配顺序表 内存满了无能为力
静态分配 给各个数据元素分配连续的存储空间 大小为maxsize*sizeof(int)
void InitList(SqList &l)
{
for(int i=0;i
for (int i = 0; i < MaxSize; i++)
{
printf("data[%d]=%d\n",i , L.data[i]);
}//错误 内存中遗留脏数据 没有分配内存 且访问数据表应该i
若不为线性表设置初始值,会输出乱码。。
#define InitSize 10//初始长度
typedef struct
{
int *data;//利用指针动态分配
int MAXSIZE;//最大容量
int length;//当前长度
}SqList;//动态分配顺序表
void IncreaseSize(SqList &l, int len)//增加动态数组长度
{
int *p=l.data;//p指针接收旧存储空间
l.data = new int[l.MAXSIZE + len];//分配增加表长后的内存空间
for(int i=0;i
void InitList(SqList &l)
{
l.data = new int [InitSize];//申请一片连续的存储空间
l.length = 0;
l.MAXSIZE = InitSize;//最大值为初始值
}
顺序表特点
①随机访问,即可以在 O(1) 时间内找到第 i 个元素。
②存储密度高,每个节点只存储数据元素
③拓展容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高)
④插入、删除操作不方便,需要移动大量元素
bool ListInsert(SqList &l,int i,int e)//插入操作。在表L中的第i个位置上插入指定元素e
{
if(i<1||i>l.length+1)//判断插入范围 可插入最后一个
return false;
if(l.length>l.MAXSIZE)
return false;
if(l.length==l.MAXSIZE)
IncreaseSize(l,1);
for(int j=l.length;j>=i;j--)//从表后遍历
{
l.data[j]=l.data[j-1];
}
l.data[i-1]=e;
l.length++;
return true;
}
最好情况:新元素插入到表尾,不需要移动元素 i = n+1,循环0次;最好时间复杂度 = O(1)
最坏情况:新元素插入到表头,需要将原有的 n 个元素全都向后移动 i = 1,循环 n 次;最坏时间复杂度 = O(n);
平均情况:平均时间复杂度 = O(n)
bool ListDelete(SqList &l, int i, int &e)//删除操作。删除表L中第i个位置的元素,
//并用e返回删除元素的值。
{
if(i<1||i>l.length)
return false;
e = l.data[i-1];//删除元素赋给e
for(int j=i;j
注:注意引用“&”的作用
时间复杂度与插入相同。
// 按位查找:查找第i个位置的元素
int GetElem(SqList &L, int i) {
return L.data[i - 1];
}
// 按值查找:查找值为i的元素位置
int LocateElem(SqList &L, int i) {
for (int j = 0; j < L.length; j++) {
if (L.data[j] == i) {
return j + 1;
}
}
return 0; // 没有查找到则返回0
}
按位查找时间复杂度O(1)
按值查找时间复杂度O(n)
线性表的链式存储又称单链表。
typedef struct LNode{
int data;//存放一个数据元素
struct LNode *next;//指向下一节点的指针
}LNode,*LinkList;//LNode *等价 LinkList
//不带头节点的单链表
bool InitList(LinkList &l)
{
l =NULL;//定义空表 防止内存脏数据
return true;
}
bool Empty(LinkList l)//判断链表是否为空
{
return(l==NULL);
}
//带头节点的单链表
bool InitList(LinkList &l)
{
l = new LNode;
if (l==NULL)
{
return false;
}
l->next =NULL;
//l->next = l//循环单链表
return true;
}
bool Empty(LinkList l)//判断链表是否为空
{
return(l->next==NULL);
}
带头结点的单链表 头节点不存放数据 只是为了操作方便
bool InsertNextNode(LinkList p,int e)//后插 在p节点 后插入元素e O(1)
{
if(p==NULL)
return false;
LNode *s = new LNode;
if(s==NULL)//防止内存分配失败
return false;
s->data = e;//顺序不能错
s->next = p->next;
p->next = s;
return true;
}
bool InsertList(LinkList &l, int i, int e)//在第i个位置插入元素e O(n)
{
if(i<1)
return false;
//为不带头节点 多加的代码主义第一个节点的情况
// if(i==1)
// {
// LNode *s =new LNode;
// s->data = e;
// s->next= l;
// l= s;
// return true;
// }
LNode *p =GetElem(l,i-1);
return InsertNextNode(p,e);
}
bool InsertPriorNode(LNode *p,int e)//在p节点之前插入元素e O(1)
{
if(p==NULL)
return false;
LNode *s = new LNode;
if(s==NULL)
return false;
s->next = p->next;//偷天换日
p->next = s;
s->data = p->data;
p->data = e;
return true;
}//大概思路是后插s节点,把s节点变成p节点,前p节点变成元素e
bool ListDelete(LinkList &l, int i ,int &e)//删除表中第i个位置的元素 O(n)
{
if(i<1)
return false;
LNode *p = GetElem(l,i-1);
if(p==NULL)
return false;
LNode *q = p->next;
e = q->data;
p->next = q->next;
delete q;
return true;
}
bool DeleteNode(LNode *p)//删除指定节点p O(1)
{
if(p==NULL)
return false;
LNode *q = p->next;//注:如果p是最后一个节点,第二行会出现问题 此时只能给出头指针从前往后寻找o(n)
p->data = q->data;
p->next = q->next;
delete q;
return true;
}
LNode * GetElem(LinkList l,int i)//O(n) 按位查找
{
if(i<0)
return NULL;
LNode *p;//指针p指向当前扫描到的节点
int j=0;//记录当前指向的是第几个节点(不带头结点时j=1)
p = l;
while(p!=NULL&&jnext;
j++;
}
return p;
}
LNode * LocateElem(LinkList l,int e)//按值查找 直至找到==e节点 O(n)
{
LNode *p = l->next;
while(p!=NULL&&p->data!=e)
p=p->next;
return p;
}
int Length(LinkList l)//表长
{
LNode *p = l;
int len=0;
while (p->next !=NULL)
{
p=p->next;
len++;
}
return len;
}
LinkList List_TailInsert(LinkList &l)//正向尾插法建立单链表 O(n)
{
int x;
l = new LNode;//建立头节点l
l->next = NULL;
LNode *r=l,*s;//r为尾指针
cin>>x;
while (x!=-1)//让-1作为链表结束的值
{
s = new LNode;
s->data = x;
r->next = s;
r = s;//指向新建的节点
cin>>x;
}
r->next = NULL;//尾指针节点为空
return l;
}
LinkList List_HeadInsert(LinkList &l)//头插法建立链表 其实是输入顺序的逆序!!
{
int x;
LNode *s;
l = new LNode;
l->next =NULL;
cin>>x;
while (x!=-1)
{
s = new LNode;
s->data = x;
s->next = l->next;
l->next = s;
cin>>x;
}
return l;
}
双链表节点中有两个指针next和prior,分别指向后继节点和前驱节点
单链表:无法逆向检索,有时候不太方便
双链表:可进可退,存储密度更低一丢丢
typedef struct DNode{
int data;
struct DNode *prior,*next;
}DNode,*DLinkList;
bool InitDLinkList(DLinkList &l)
{
l = new DNode;
if(l = NULL)
return false;
l->next = NULL;
l->prior = NULL;
// l->next = l;循环双链表
// l->prior = l;
return true;
}
bool InsertNextNode(DNode *p, DNode *s)//在p节点后加s节点
{
if(p==NULL||s==NULL)
return false;
s->next = p->next;
if(p->next!=NULL)//防止p是最后一个节点
p->next->prior = s;
s->prior = p;
p->next = s;
return true;
}
bool DeleteNextNode(DNode *p)//删除p节点的后继节点
{
if(p==NULL)
return false;
DNode *q = p->next;
if(q==NULL)
return false;
p->next = q->next;
if(q->next!=NULL)
q->next->prior = p;
delete q;
return true;
}
void DestroyList(DLinkList &l)//销毁链表
{
while (l->next!=NULL)
{
DeleteNextNode(l);
}
delete l;
l = NULL;
}
表位指针不指向NULL,指向头节点。
初始化时改为l->next = l;即可。
循环单链表从一个结点出发可以找到其他任何一个节点。
循环双链表头节点的prior指针指向尾节点,尾节点的next指针指向头节点。
初始化时l->next=l;l->prior=l;
基本操作与上述相同;
静态链表借助数组来描述线性表的链式存储结构,结点也有数据域data和指针域next,与前面所讲的链表中的指针不同的是,这里的指针是结点的相对地址(数组下标),又称游标。和顺序表一样,静态链表也要预先分配一块连续的内存空间。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KL5ycIuX-1649239793328)(https://cdn.jsdelivr.net/gh/girlsdontget341/image@master/img/202203231609891.png)]
#define MAXSIZE 10
typedef struct Node{
int data;
int next;//下一个元素的下标
}SLinkList [MAXSIZE];//可用 SLinkList 定义“一个长度为 MaxSize 的Node 型数组”
优点:增、删 操作不需要大量移动元素
缺点:不能随机存取,只能从头结点开始依次往后查找;容量固定不可变
都属于线性表,都是线性结构
存储结构(顺序表/链表)
优点:支持随机存取、存储密度高
缺点:大片连续空间分配不方便,改变容量不方便
优点:离散的小空间分配方便,改变容量方便
缺点:不可随机存取,存储密度低
空间分配
顺序表:需要预分配大片连续空间。 若分配空间过小,则之后不 方便拓展容量;若分配空间 过大,则浪费内存资源
链表:只需分配一个头结点(也可 以不要头结点,只声明一个 头指针),之后方便拓展
增/删
顺序表:
插入/删除元素要将后续元素都后移/前移
时间复杂度 O(n),时间开销主要来自移动元素
若数据元素很大,所需时间代价很高
链表:
修改指针即可;
时间复杂度 O(n),时间开销主要来自查找目标元素
查找元素代价更低。
查
顺序表:按位查找:O(1) 按值查找:O(n)
若表内元素有序,可在 O(log2n) 时间内找到。
链表:按位查找/按值查找均为O(1)
综上:
表长难以预估,经常增加/删除元素 ----链表
表长可预估,查询(搜索)操作较多 ----顺序表
栈(stack)是只允许在一端进行插入或删除的线性表
逻辑结构:与普通线性表相同
栈顶:允许进行插入和删除的那一端
栈底:固定的,不允许插入和删除的那一端
空栈:不含任何元素的空表
操作特性:先进后出(LIFO)
数学性质:
n个不同元素进栈,出栈元素不同排列的个数为 1 n + 1 C 2 n n \frac{1}{n+1} C_{2 n}^{n} n+11C2nn(称为卡特兰数)
#define MAXSIZE 10//定义栈中元素最大个数
typedef struct {
int data[MAXSIZE];//静态数组存放栈中元素
int top;//栈顶指针
}SqStack;
(以下操作默认栈顶指针top指向-1即栈顶元素) 若top为0即指向栈顶元素下一个单元,语句将变化
void InitStack(SqStack &s)//初始化
{
s.top = -1;//初始化栈顶指针为-1
}
bool IsEmpty(SqStack s)//判断是否为空栈
{
if(s.top==-1)
return true;
else
return false;
}
栈长:s.top+1;
进栈操作
//进栈操作
bool Push(SqStack &s, int x)
{
if(s.top == MAXSIZE - 1)
return false;
s.data[++s.top] = x;//++s.top先执行 等价于s.top=s.top+1;s.data[s.top]=x
return true;
}
出栈操作
//出栈操作
bool Pop(SqStack &s, char &x)
{
if(s.top==-1)
return false;
x = s.data[s.top--];//s.top--后执行 等价于x=s.data[s.top]; s.top=s.top-1;
return true;
}
读栈顶元素
//读栈顶元素
bool GetTop(SqStack &s, char &x)
{
if(s.top==-1)
return false;
x=s.data[s.top];
return true;
}
利用栈底位置相对不变的特性,可让两个顺序栈共享一个一维数组空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸。
两个栈的栈顶指针都指向栈顶元素,top0=-1时0号栈为空,top1=MaxSize时1号栈为空;仅当两个栈顶指针相邻(top1-top0=1)时,判断为栈满。当0号栈进栈时top0 先加1再赋值,1号栈进栈时top1 先减1再赋值;出栈时则刚好相反。
共享栈是为了更有效地利用存储空间,两个栈的空间相互调节,只有在整个存储空间被占满时才发生上溢。其存取数据的时间复杂度均为O(1),所以对存取效率没有什么影响。
队列也是一种操作受限的线性表,只允许在表一端进行插入,而在表的另一端进行删除。
操作特性:先进先出(FIFO)
队头(Front)。允许删除的一端,又称队首。
队尾(Rear)。允许插入的端。
空队列。不含任何元素的空表。
#define MAXSIZE 10 //队列中最大个数
typedef struct{
int data[MAXSIZE]; //存放队列元素
int front,rear; //队头指针和对位指针
}SqQueue;
初始状态(队空条件):Q.front==Q.rear==0。
进队操作:队不满时,先送值到队尾元素,再将队尾指针加1。
出队操作:队不空时,先取队头元素值,再将队头指针加1。
上述办法可以判别对空,但显然不能用q.rear=MAXSIZE作为判断队满的条件,如下。
显然仍有存储空间。
由此,引入循环队列,把顺序队列想成一个环状的空间,即把存储队列元素的表从逻辑上视为一个坏,称为循环队列。
初始时:Q.front=Q.rear=0。
队首指针进1:Q.front=(Q.frontt1)%MaxSize。
队尾指针进1:Q.rear=(Q.rear+1) %MaxSize。
队列长度:(Q.rear+MaxSize-Q.front) %MaxSize。
判断队满
牺牲一个存储单元,约定“队头指针在队尾指针的下一位置”作为标志
类型中新增表示队列元素个数的数据成员size。则队空的条件为Q.size==0,队满的条件为Q.size=MAXSIZE;
类型中增设tag 数据成员,以区分是队满还是队空。tag等于0时,若因删除导致Q.front==Q.rear,则为队空;tag等于1时,若因插入导致Q.front==Q.rear,则为队满。每次删除都令tag为0,插入都令tag为1。
初始化、判空(注意初始化时front和rear指针指向的位置)
//循环队列
void InitQueue(SqQueue &q)
{
q.front = q.rear = 0;//初始化均为0
}
bool IsEmpty(SqQueue q)
{
if(q.front == q.rear)
return true;
else
return false;
}
进队
bool EnQueue(SqQueue &q, int x)
{
if((q.rear+1)%MAXSIZE==q.front)//判断队满
return false;
q.data[q.rear]=x;
q.rear = (q.rear+1)%MAXSIZE;//队尾+1
return true;
}
出队
bool DeQueue(SqQueue &q, int &x)
{
if(q.rear==q.front)//判断对空
return false;
x = q.data[q.front];
q.front = (q.front+1)%MAXSIZE;//队头+1
return true;
}
typedef struct LinkNode{//链式队列节点
int data;
struct LinkNode *next;
}LinkNode;
typedef struct{//链式队列
LinkNode *front,*rear;//头尾指针
}LinkQueue;
void InitQueue(LinkQueue &q)//带头结点
{
q.front = q.rear =new LinkNode;
q.front->next = nullptr;//头尾指针指向头结点
}
void InitQueue(LinkQueue &q)//不带头结点
{
q.front = q.rear = nullptr; //初始置空
}
入队
void EnQueue(LinkQueue &q,int x)//带头结点
{
LinkNode *s = new LinkNode;
s->data = x;
s->next = nullptr;
q.rear->next = s;
q.rear =s;
}
void EnQueue(LinkQueue &q,int x)
{
LinkNode *s = new LinkNode;
s->data = x;
s->next = nullptr;
if(q.front == nullptr){//不带头结点需特别处理空队列入队
q.front = s;
q.rear = s;
}else{
q.rear->next = s;
q.rear =s;
}
}
出队
bool DeQueue(LinkQueue &q,int &x)//带头结点
{
if(q.front==q.rear)
return false;
LinkNode *p = q.front->next;
x=p->data;
q.front->next = p->next;
if(q.rear==p)//最后一个节点出队
q.rear=q.front;
free(p);
return true;
}
bool DeQueue(LinkQueue &q,int &x)//不带头结点
{
if(q.front==nullptr)
return false;
LinkNode *p = q.front;//p指向出队的结点
x=p->data;
q.front = p->next;
if(q.rear==p)
q.rear=nullptr;
q.front=nullptr;
free(p);
return true;
}
判断输出序列合法性 思路同栈。
假设表达式中允许包含两种括号:圆括号和方括号,其嵌套的顺序任意均为正确的格式,[()或([())或((]均为不正确的格式。
算法:
1)初始设置一个空栈,顺序读入括号。
2)若是右括号,则或者使置于栈顶的最急迫期待得以消解,或者是不合法的情况(括号序
列不匹配,退出程序)。
3)若是左括号,则作为一个新的更急迫的期待压入栈中,自然使原有的在栈中的所有未消
解的期待的急迫性降了一级。算法结束时,栈为空,否则括号序列不匹配。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VQSsyixb-1649239793331)(https://cdn.jsdelivr.net/gh/girlsdontget341/image@master/img/202204012208006.png)]
bool bracketCheck(char str[], int length)
{
SqStack s;
InitStack(s);//初始化一个栈
for(int i=0; i
中缀表达式:普通的算数表达式
后缀表达式(逆波兰表达式):运算符在两个操作数后面(无界限符)
前缀表达式(波兰表达式):运算符在两个操作数前面(无界限符)
中缀转后缀手算:
①确定中缀表达式中各个运算符的运算顺序
②选择下一个运算符,按照「左操作数右操作数运算符」的方式组合成一个新的操作数
③如果还有运算符没被处理,就继续②
注:为保证手算和机算结果相同,采用“左优先”原则(即只要左边能优先计算就先算左边的)
后缀表达式的手算方法:
从左向右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应运算。(注意运算左右顺序)
后缀表达式的计算(机算):
用栈实现后缀表达式的计算:
①从左往右扫描下一个元素,直到处理完所有元素
②若扫描到操作数则压入栈,并回到①;否则执行③
③若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到①
注:先弹出的栈顶元素是右操作数,符合“后进先出”。
中缀转前缀手算:
同后缀原理,采用“右优先原则”
前缀表达式计算(机算):
用栈实现变为从右向左扫描,别的同后缀。
中缀转后缀表达式(机算):
中缀表达式的计算(栈)
函数调用的特点:最后调用的函数最先执行结束(LIFO)
递归算法可以把原始问题转换为属性相同,但规模较小的问题。
缺点:可能造成多层递归,造成效率低下
优点:代码简单,易理解。
数组是由n个相同数据元素构成的有限序列。
数组是线性表的推广。
存储结构
一维数组:LOC + i ∗ +i^{*} +i∗ sizeof(ElemType)
M行N列二维数组b[i][j]:
对称矩阵:只存放上(下)三角元素即可
三角矩阵(上(下)三角的元素均为常量):存储方式与对称矩阵类似,只需额外多一块空间存储常量元素
三对角矩阵:
存储对应关系均可自己推导,不列举公式
稀疏矩阵:非零元素远远少于矩阵元素的个数
存储策略:三元组<行,列,值>
十字链表法