数据结构(软件)
操作系统——————————>计算机网络
计算机组成(硬件)
数据概念基础
数据元素:
即:数据的基本单位,数据元素包括数据项,多个数据元素形成的集合就是数据对象,数据对象具有相同性质的数据元素的集合。同一个数据对象中的数据元素可以组成不同的数据结构(线性或者网状)
数据结构的三要素:
-
逻辑结构:
——>集合结构
——>线性结构:一对一
——>树状结构:一对多
——>图状结构:多对多 -
数据运算:结合逻辑结构、实际需求来定义基本运算
-
物理结构(存储结构):如何实现数据结构
——>顺序存储
——>链式存储
——>索引存储
——>散列存储(哈希存储)
存储特性:
(1)顺序存储:物理上连续;非顺序存储:物理上离散
(2)数据的存储结构会影响存储空间的分配的方便程度
(3)数据的存储结构会影响对数据运算的速度
数据类型:
即:一个值得集合和定义在此集合上的一组操作的总称。
1)原子类型。其值不可以再分的数据类型,如 bool 类型
2)结构类型。其值可以在分解为若干成分的数据类型,如int类型
抽象数据类型(ADT):
即:抽象数据组织及与之相关的操作。
算法:
(1)五个特性:有穷性、确定性、可行性、输入、输出
(2)好的算法:正确性、可读性、健壮性、高效与低存储:省时、省内存;时间复杂度低、空间复杂度低
算法效率的度量:时间复杂度、空间复杂度
- 时间复杂度
能够事先预估算法的**时间开销**T(n)与问题的规模 n 的关系;
在考虑时,只考虑时间开销的最高次项(同时不考虑最高项的系数),只需要考虑O(n)数量级
根据高数知识:
实际工程中:
结论1:顺序执行的代码只会影响常数项,可以忽略;
结论2:只需挑循环中的一个基本操作分析它的执行次数与n的关系即可。同时,外层循环执行n次,嵌套两层循环,则内层共执行 n^2 次。
结论3:如果有多层嵌套循环,只需关注最深层循环了几次。
一般考虑:最坏时间复杂度,平均时间复杂度
- 空间复杂度
讨论**空间开销**(内存开销)与问题规模 n 之间的关系
(1)程序代码,大小固定,与问题规模无关。
(2)无论问题规模怎么变,算法运行所需的内存空间都是固定的常量
,算法空间复杂度为S(n)=O(1),则称该算法可以原地工作
(3)对于函数递归调用时,带来的内容开销,即当每次递归值定义变量 S(n)=O(n),即此时 空间复杂度 = 递归调用的深度
当每次递归内容定义一个数组n,递归次数为n,即S(n)=O(n^2)。
线性表
逻辑结构与基本操作
性质:
(1)相同数据类型
(2)有限序列
(3)n为表长
概念:
ai 是线性表中的“第i个元素”线性表中的位序;
a1 是 表头元素;an是表尾元素
直接前驱,直接后继
基本操作:
(1)初始化表:构建一个空的线性表,分配内从空间
(2)销毁操作:销毁线性表,并释放线性表所占用的内存空间
(3)插入操作:在表中的第i个位置上插入指定的元素e;
(4)删除操作:删除表中第i个位置的元素,并用e返回删除元素的值;
(5)按值查找操作:在表中查找查找具有给定关键字的个数;
(6)按位值找操作:获取表中的第i个位置的元素值
(7)求表长,返回线性表的长度,
(8)输出操作,按前后顺序输出线性表的所有元素
(9)判空操作,如表为空,则返回true,否则false;
存储/物理结构
- 顺序表(顺序存储)
用线性存储的方式实现的线性表。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来实现。sizeof(类型)函数求存储空间大小
特点:
(1)随机访问:能在O(1)时间内找到第 i 个元素 *
(2)存储密度高
(3)拓展容量不方便
(4)插入、删除数据元素不方便
静态分配:数组
动态分配:
C – malloc
、free
函数
即 malloc
申请一片连续存储空间,free 释放空间
顺序表的定义与动态调整
//头文件
#include<stdio.h>
int *data = NULL;
data = (int*)malloc(sizeof(int) * 10); ///定义一片连续空间,初始化大小为10
#include "cstdio"
#define InitSize 10
struct MyStruct
{
int *data;
int MaxSize;
int length;
};
//初始化线性表
int InitList(MyStruct &L)
{//申请一片连续的存储空间
L.data = (int*)malloc(InitSize * sizeof(int));
L.length = 0;
L.MaxSize = InitSize;
return 0;
}
//动态调整空间长度
void IncreaseSize(MyStruct &L,int len)
{
int *p = L.data;
L.data = (int*)malloc((L.MaxSize + len) * sizeof(int));
for (int i = 0; i < L.length; i++)//将旧数据复制给新空间
{
L.data[i] = p[i];
}
L.MaxSize = L.MaxSize + len;//更新长度
free(p);//释放顺序表的空间。
}
int main()
{
MyStruct L;
InitList(L);//初始化顺序表
//往顺序表中加入新空间
IncreaseList(L,5);
return 0;
}
顺序表的插入操作:
//插入元素
bool ListInsert(MyStruct &L, int i, int e)
{
/*
将指针指向顺序表最后+1,然后在插入地方进行i--,
*/
if (i<1 || i>L.length+1)
{
cout << "插入位置有误!" << endl;
return false;
}
if (L.length>=L.MaxSize)
{
cout << "目前的顺序表长度大于了最大尺寸" << endl;
return false;
}
for (int j = L.length; j >= i; i--)
{
L.data[j] = L.data[j - 1];//时间规模n = L.length
}
L.data[i - 1] = e;//i 的位置在与个数之间差1
return true;
}
顺序表的删除操作:
bool Listdelete(MyStruct &L, int i, int &e)
{
if (i < 1 || i>=L.length+1)
{
cout << "删除位置不合法" << endl;
return false;
}
if (L.length > L.MaxSize)
{
cout << "数据长度不对" << endl;
return false;
}
cout << "被删除的数据是:" << L.data[i-1] << endl;
e = L.data[i - 1];
for (int j = i-1; j+1 < L.length; j++)
{
L.data[j] = L.data[j + 1];//关注最深层循环语句的执行次数与问题规模 n 的关系
}
L.length--;
return true;
}
计算一下时间复杂度:
最坏情况:删除表头元素,需要将后续的n-1个元素全部向前移动;
则—————i=1,循环i-1次,最坏的复杂度=O(n);
平均情况:假设 删除任何一个元素的概率相同,即i = 1,2,3…length 的概率是p= 1/n
——————i=1时,循环n-1次;i=2,循环n-2次;i=3 ,循环n-3次。。。。
——————平均循环次数=(n-1)p+(n-2)p+…+1*p = (n-1)/2 ,时间复杂度O((n-1)/2) ==》O(n)
按位查找
按值查找
(略)
C++ ——— new
、delete
关键字
与C上面的功能相似
- 链表(链表存储)
分类:单链表、双链表、循环链表、静态链表
(1)单链表:
结点互连,一个结点包括(数据空间、存放指向下一个结点的指针)
定义的方法:使用结构体进行定义,在用malloc函数进行单个结点的连续空间的分配
单链表的结点初始化
struct LNode//定义单链表结点类型
{
int data;//结点存放的数据
struct LNode *next_node;//存放下一个结点位置的指针
};
struct LNode *p = (struct LNode *)malloc(sizeof(struct LNode));//都需要这个节省空间,c++使用new关键词效果差不多
//优化,将struct LNode重命名为LNode
typedef struct LNode LNode;
LNode *z = (LNode *)malloc(sizeof(LNode));
//优化2,
/*
将struct LNode2 重命名为 LNode2 ,或是将 指针Linklist指向struct LNode;
也就是说,LNode2 * L; 等价于 LinkList L; 建议多使用后者,可读性更强,更加强调单链表,前者更加强调结点
*/
typedef struct LNode2
{
int data2;
struct LNode2 *next2;
}LNode2,*Linklist;
对于第二种优化:
将struct LNode2
重命名为 LNode2
,或是将 指针Linklist
指向struct LNode
;
也就是说,LNode2 * L;
等价于 LinkList L;
建议多使用后者,可读性更强,更加强调单链表,前者更加强调结点
创建一个不带头结点链表
//定义一个不带头结点的单链表
typedef struct Listnode
{
int data;
struct Listnode *next;
}Listnode,*Linklist;
bool Init_List(Linklist &L)//初始化链表内存为空,参数定义上上其的引用
{
L = NULL;//让头指针指向空,防止脏数据
return true;
}
bool Linklist_isEmpty(Linklist L)
{
return (L == NULL);//判断指针指向的是否为空
}
void init_linklist()
{
Linklist L;
Init_List(L);//初始化链表
}
创建的一个带结点的单链表(此法写代码更方便)
后面的操作都是基于带结点的单链表
思路:
(1)定义一个结点(包括数据与包含下一结点位置的结构体)
(2)使用malloc的申请一个结点结构体类型的空间作为头结点
(3)并对头结点的包含下一结点位置的指针元素赋空(NULL)
//后面的链表代码都是用这个结点结构体的
typedef struct Listnode
{
int data;
struct Listnode *next;
}Listnode,*Linklist;
//带头结点
bool INIT_List(Linklist &L)
{
L = (Listnode *)malloc(sizeof(Listnode));//对头指针指向一个头结点,且该头结点不存储数据
if (L==NULL)//如果没有可分配的空间
{
return false;
}
L->next = NULL;//从头结点后的下一个结点开始初始化为空,也就是从下一个结点开始才开始存放数据
return true;
}
bool Linklist_have_node_inEmpty(Linklist L)
{
return (L->next == NULL);
}
void init_linklist()
{
Linklist L;
INIT_List(L);//初始化链表
}
心中一定要有如下的概念:
单链表的逐个添加新结点
思想上:每次添加时都将结点指针指向最后一个结点,判断依据是最后的结点所保存的位置数据是NULL
时刻记住 :链表的访问特点是不能随机访问,所以每次都要将指针遍历到最后去添加新的结点
&L 是引用指针
bool add_node(Linklist &L, Listnode *n)//加入结点
{
Linklist list = NULL;
list = L;//这个L只是便是头结点
while (list->next)//判断后面还接有结点没有,如果有就进行链表指针移动,知道地址为空为止,每次都要进行从头到尾的判断
{
list = list->next;
}
n->next = NULL;
list->next = n;//指针锁定位置后,在此位置上插入新数据
return true;
}
单链表的在中间插入结点——后插
思路:
(1)定义一个结点指针P,用循环将它移动到待插位置(比如插3,P指向2)的前一个结点去。
(2)在定义一个新结点S,将你需要加的数据存入到该新结点S
(3)先将 S 的 next 的存为 P 旧的 next,然后赋值完后再将 P 的 next 存为 S,此时S就被插进去了。
void input_data(Linklist &L,int i)//node为插入的结点,i表示插入的位置
{
if (i<1||i>6)
{
cout << "插入操作不合法" << endl;
}
Listnode *p = NULL;//定义一个空指针
p = L;//将p指向头结点
/*
假设链表长度为5,所以能够插入的位置只能在1-5内,故需要做的判断添加有,i不能小于1也不能大于6
*/
for (int j = 0; j < i - 1; j++)//记住表头0不算入,这一步就是去找插入的位置
{
p = p->next;
}
//此时创建一个空结点
Listnode *new_node = new Listnode;//这是C++的空间分配
cout << "请输入你要后插入的值:" << endl;
cin >> new_node->data;
cout << " " << endl;
new_node->next = p->next;//这两步需要深刻理解
p->next = new_node;
}
单链表的在中间插入结点——前插
思路:
(1)假设 对结点 a1 进行前插,首先需要使用后插的概念,先插入一个新结点S在 a1 的后面
(2)将 新结点 S 用来存储 a1 的数据,在用原来 旧的 a1 结点作为插入数据的插入结点,即可。
(3)所以此时在对 遍历指针p的调节上,
/*
在a1前插,思路是,使用后插的概念,将插入的结点s用来存储被前插结点a1的数据,此时s成为新的a1,旧的a1作为插入的结点
*/
void input_data_front(Linklist &L, int i,int e)// i 为前插的位置,e为前插的值
{
Listnode *S = (Listnode*)malloc(sizeof(Listnode));//申请新的一个结点
Linklist p = NULL;
p = L;
for (int j = 0; j < i; j++)
{
p = p->next;
}
S->next = p->next;
p->next = S;
S->data = p->data;
p->data = e;
}
单链表删除其中的某个结点
思路
(1)首先定义一个结点指针 p,并将 p 指向 头结点
(2)然后 根据 输入的删除位置,移动 指针 p,将其移动到 待删除结点的前一个结点
(3)然后定义新的空结点 q指针,用 q 记录下,删除结点的位置 即 p->next,用 q 获取被删除的值,
再将 p 的 next 赋值为 q 的 next(也就是被删除结点的下一个结点,这样接上了),
(4)最后在释放到q,就相当与删除了那个结点
int delete_node(Linklist &L, int i)//i 为需要删除的结点位置
{
//先将指针p指向删除位置前的结点
Linklist p = NULL;
p = L;
int j = 0;
while (p != NULL && j < i - 1)//这种方法和前面的for思想一样
{
p = p->next;
j++;
}
Listnode *q = NULL;
q = p->next;//获取被删除点的位置
int e = q->data;//获取被删除点的值
if (q->next != NULL)//这里加上if,是因为当删除的结点是最后一个结点时,结点的next是NULL
{
p->next = q->next;//将被删点的前一个结点和后一个结点接上
}
free(q);//释放点被删除结点的空间
return e;
}
单链表的遍历
思路:
(1)先定义一个结点指针p,先指向 表头结点 L 的 next
(2)使用 循环 不断迭代p,判断条件就是 此时 p 的next 是否为NULL ,是就说明已经遍历到最后一个了。
//遍历链表
void find_data(Linklist &L)
{
Linklist p = NULL;//定义一个新指针
if (!L->next)
{
cout << "链表为空" << endl;
}
else
{
p = L->next;//指针先指向第一结点
while (p)//判读此时指针作为空间是否为空,如果本身它就是空那自然它的前一个的next就是空
{
cout << p->data << endl;
p = p->next;
}
}
}
单链表的 尾插法 创建
思路:
创建一个指针 p ,用while去判断输入的值,然后每一轮循环创建一个结点s,s存新输入的值,接上此时的 p 的 next,在将 p 移动到新的 s 上,从而进入新一轮循环
Linklist create_list(Linklist &L)
{
Linklist p = NULL;
p = L;
int e;
cout << "请输入值:" << endl;
cin >> e;
while (e!=-1)
{
Listnode *s = (Listnode*)malloc(sizeof(Listnode));
s->data = e;
p->next = s;
p = s;
cout << "请输入值:" << endl;
cin >> e;
}
p->next = NULL;//最后将结点指针置空
return L;//返回创建好的单链表表头
}
单链表的 头插法 创建
思路上:
创建一个指针 p ,一直指向表头结点,每次循环创建一个新结点s,将值插入后,将s接到 L 后面,将p的next 给 s的next
(头插法输出的 输入顺序的 倒序)
Linklist create_list_front(Linklist &L)
{
Linklist p = NULL;
p = L;
int e;
cout << "请输入值:" << endl;
cin >> e;
while (e!=-1)
{
Listnode *s = (Listnode*)malloc(sizeof(Listnode));
s->data = e;
s->next = p->next;
L->next = s;
cout << "请输入值:" << endl;
cin >> e;
}
return L;
}
单链表的按位查找 和 按值查找
这个和前面的确定位置插入思路相同
//封装按位查找函数
Linklist GetElem(Linklist &L, int i)
{
Linklist p = NULL;
p = L;
int j = 0;
while (p != NULL && j < i )
{
p = p->next;
j++;
}
return p;
}
//封装按值
Linklist GetElem_value(Linklist &L, int e)
{
Linklist p = NULL;
p = L;
while (p != NULL && p->data != e)
{
p = p->next;
}
return p;
}
//计算链表的长度也是查找的思路
双链表 及其各种操作
双链表与单链表的区别只是每个结点都新加入了一个保存前驱结点位置为空间,所以编写代码的死路上吗,与单链表大同小异,就不在思路分析了,只给出代码:
//双链表
/*
与单链表的区别就是在结点的中加入存放前驱结点位置的区域
*/
namespace x3 {
int length = 0;
typedef struct LNODE
{
int data;
struct LNODE *prior, *next;//前驱 和 后继 指针
}NODE, *doubleList;
//初始化结点
bool init_double_list(doubleList &L)
{
L = (NODE*)malloc(sizeof(NODE));//创建一个结点空间
if (L == NULL)
{
return false;
}
L->prior = NULL;
L->next = NULL;
return true;
}
//判断链表是否为空
bool double_list_isempty(doubleList &L)
{
if (L->next == NULL)
{
cout << "链表为空" << endl;
return true;
}
return false;
}
//后插创建操作
doubleList create_double_list(doubleList &L)
{
doubleList p = NULL;
p = L;
int e;
cout << "请输出后插的值:(输入-1结束)" << endl;
cin >> e;
while (e != -1)
{
NODE *s = (NODE*)malloc(sizeof(NODE));
s->data = e;
p->next = s;
s->prior = p;
p = s;
length++;
cin >> e;
}
p->next = NULL;
return L;
}
//前向遍历操作
void find_front(doubleList &L)
{
doubleList p = NULL;
p = L;
while (p->next != NULL)//先将指针排到最后去
{
p = p->next;
}
while (p->prior != NULL)
{
cout << p->data << endl;
p = p->prior;
}
}
//后向遍历操作
void find_next(doubleList &L)
{
doubleList p = NULL;
p = L;
while (p->next != NULL)
{
cout << p->next->data << endl;
p = p->next;
}
}
//插入中间的操作(按位序后插入):
bool input_index(doubleList &L, int e, int i)//e 为插入的值,i插入为位置
{
doubleList p = NULL;
p = L;
if (p->next == NULL)
{
cout << "双链表为空" << endl;
return false;
}
int j = 0;
while (i <= length && j < i - 1)
{
p = p->next;
j++;
}
NODE *s = (NODE*)malloc(sizeof(NODE));
s->data = e;
if (p->next != NULL)//排除点p事最后一个结点
{
s->next = p->next;
p->next->prior = s;
p->next = s;
s->prior = p;
return true;
}
else
{
p->next = s;
s->prior = p;
return true;
}
}
//前插的前插
bool inpu_index_front(doubleList &L, int e, int i)
{
doubleList p = NULL;
p = L;
if (p->next == NULL)
{
cout << "双链表为空" << endl;
return false;
}
int j = 0;
while (i <= length && j < i)
{
p = p->next;
j++;
}
NODE *s = (NODE*)malloc(sizeof(NODE));
s->data = e;
s->next = p;
s->prior = p->prior;
p->prior->next = s;
p->prior = s;
return true;
}
}
int main()
{
x3::doubleList LIST = NULL;
x3::init_double_list(LIST);
LIST = x3::create_double_list(LIST);
cout << endl;
x3::find_front(LIST);
cout << endl;
x3::find_next(LIST);
int e, i = 0;
cout << "下面进行插入操作" << endl;
cin >> e;
cout << "位置:" << endl;
cin >> i;
cout << endl;
x3::input_index(LIST, e, i);
x3::find_next(LIST);
cout << endl;
x3::inpu_index_front(LIST, e, i);
x3::find_front(LIST);
return 0;
}
循环单链表、循环双链表
循环单链表,就是单链表的情况下,将最后一个结点的 next 指向头指针 L,从而形成一种单闭环;
循环双链表,就是双链表的情况下,一个循环是从头到尾,尾的 next 再指向 头L ;另外一个循环是从尾的 prior 到 头,再头的prior指向尾;
代码根据前面的代码可以较容易写出;
静态链表
单链表的结点时分布式的分散的存储在内存之中的,而静态链表的不同;
静态链表:分配一整片连续的内存空间,各个节点集种管理;
与单链表相同的是,每个结点还是由一个数据存储空间 和 一个存放下一个数据位置的空间;
定义一个静态链表
const int maxsize = 10;
/*
创建的思路就是 首先创建一个struct类型的结点,再使用数组定义的方式分配出一个连续的空间,需要注意的是这个连续空间中的结点的位置不一定是有序
*/
typedef struct{
int data; //存储数据
int next;//存储游标
}SLinkList[maxsize];
//或者
struct Node{
int data; //存储数据
int next;//存储游标
};
typedef struct Node Node_list[maxsize];
//或者
Node a[maxsize];//和上面的定义时一样的
//初始化时,最好将每一个结点的next指向都为特定的值,由此后续即可用于判断是否为空
//需要注意的是,静态链表的容量大小时固定、不可变的,和数组的特性一致。
总结:顺序表和链表
-
顺序表
优点:支持随机存取、存储密度高;
缺点:大片连续的空间分配不方便,一旦定义,不可改变容量 -
链表
优点:离散的小空间分配方便,改变容量方便。
缺点:不能随机存取,存取密度低。