一、概述
数据结构:逻辑结构+存储结构
逻辑结构:
线性逻辑结构:数据之间的关系是线性的(一对一)
线性表、栈、队列
非线性逻辑结构:
树型结构 (一对多)
图型结构 (多对多)
存储结构:
顺序存储:逻辑上相邻的元素,在存储器上的位置也是相邻的。(数组)
链式存储:逻辑上相邻的元素,在存储器上的位置可以不是相邻的,但还得实现逻辑上的相邻(利用指针)。(链表、指针开二维数组)
索引存储
散列存储(哈希)
二、算法
三、线性表
定义:
具有相同数据类型的有限(n)个数据元素(可以由多个元素组成)的序列,n==0时,空表,n>=1时,L=(a1,a2,...an),a1 表头元素,an 表尾元素,除a1以外,其余元素都有唯一的前驱元素,除an以外,其余元素都有唯一的后继元素,形成了一种一对一的关系。
数组的大小>=线性表的大小。
操作:增删查改
存储结构的实现:
顺序表 (内存中以一维数组的形式存储)
顺序存储结构,逻辑上相邻,实际也相邻。
#include<stdio.h>
#include<stdlib.h>
#define my_maxx 100
//顺序表,int,
typedef struct ArrryList {
int* arry;//指针模拟开一维数组,避免内存的浪费
int arry_max;//表的最大数量
int len;//表的当前长度
}Arry;
//初始化数组
Arry initarray() {
Arry b;
b.arry = (int*)malloc(sizeof(Arry) * my_maxx);
if (b.arry == NULL) {
printf("内存分配失败\n");
}
b.arry_max = my_maxx;
b.len = 0;
return b;
}
//顺序插入数据temp
void arry_add(Arry* p, int temp) {
//判断是否超范围
if (p->len < p->arry_max) {
p->arry[p->len] = temp;
p->len++;
}
else
{
printf("内存已满,插入失败\n");
}
}
//在第k位插入元素temp
void my_insert(Arry* p, int k, int temp) {
//判断是否超范围
if (p->len < p->arry_max) {
//将k位以后的元素后移,再插入元素temp
for (int i = p->len - 1; i >= k; i--) {
p->arry[i+1] = p->arry[i];
}
p->arry[k] = temp;
p->len++;
}
else {
printf("内存已满,插入失败\n");
}
}
//查找元素,返回索引,失败返回-1
int find(Arry* p, int temp) {
for (int i = 0; i < p->len; i++) {
if (temp == p->arry[i]) {
return i;
}
}
return -1;
}
//删除元素temp
void del(Arry* p, int temp) {
//查找,返回索引
int j = find(p, temp);
if (j == -1) {
printf("此元素不存在");
return;
}
//找到了
for (int i = j; i < p->len; i++) {
//j后面的元素前移一位,覆盖此元素
p->arry[i] = p->arry[i + 1];
}
p->len--;
return;
}
//把元素temp更改为tem
void my_change(Arry* p, int temp, int tem) {
int j = find(p, temp);
if (j == -1) {
printf("此元素不存在\n");
return;
}
p->arry[j] = tem;
}
void show(Arry a)
{
for (int i = 0; i < a.len; i++)
{
printf("%d ", a.arry[i]);
}
printf("\n");
}
int main() {
Arry a;
a = initarray();
arry_add(&a, 0);
arry_add(&a, 1);
arry_add(&a, 3);
show(a);
my_insert(&a, 2, 2);
show(a);
del(&a, 1);
show(a);
return 0;
}
优缺点
优点:查找定位元素快(通过数组下标直接访问)。
缺点:
- 增删操作复杂。
- 表的大小是确定的,不易扩展。(扩展只能声明更大的空间,然后复制)
- 由于数据大小的不可预测性,可能会造成空间的浪费。
应用场景:
⼀组地址连续的存储单元依次存储数据元素的线性结构,且不会对数据进行大量改动。
效率分析
顺序表对于插⼊、删除⼀个元素的时间复杂度是O(n)。 因为顺序表⽀持随机访问,顺序表读取⼀个元素的时间复杂度为O(1)。因为我们是通过下标访 问的,所以时间复杂度是固定的,和问题的规模⽆关。 最⼤的优点是空间利⽤率⾼。最⼤的缺点是⼤⼩固定。
链表
链式存储结构:实际可以不相邻,需要指针实现逻辑上的相邻。
单向链表
存储一个数据:数据本身+下一个数据的地址(一个结构体),即一个节点。
头指针:保存第一个节点地址的指针,以头指针命名链表。
首元节点:第一个保存实际数据的节点。
判断:
- 头指针指向头节点 ---------错
- 头指针指向首元节点---------错
- 都缺少前提
头节点:存储脏数据(可以理解不保存需要的数据)和首元节点的地址,为了使首元节点和其他节点一样(地址存储在另一个节点中),这样在操作时可以“一视同仁”。(比如,如果没有这个节点,在插入数据时,需要判断是不是在首元节点前插入)
#include<stdio.h>
#include<stdlib.h>
//单项链表
//声明节点的结构体,数据+下一个节点的地址,以及头指针(这里不考虑结构体内存对齐,4+8)
typedef struct Node {
int data;//数据
struct Node* next;//下一个节点的地址
}Node, * Linklist;
关于Linklist与Node*
Linklist L1;
Node* N1;
- L1是一个指向链表首元素的Node类型的指针,以L1的名字代指整个链表。(同理数组,数组名是指向首元素的指针)
- N1是一个节点。
- 声明头指针--Linklist,声明节点指针--Node*,增强可读性
增删查改
#include<stdio.h>
#include<stdlib.h>
typedef struct Node {
int data;//数据
struct Node* next;//下一个节点的地址
}Node, * Linklist;
//初始化函数
Node* initLinklist() {
Node* s = (Node*)malloc(sizeof(Node));
if (s == NULL) {
printf("内存分配失败\n");
}
else
{
s->next = NULL;
}
return s;
}
//增:申请一个新节点,存放数据,保存后一个节点的地址,把新节点的地址保存到前一个节点的指针域里
//头插
//L的理解
/*L,值传递,需要返回值,*L二级指针,地址传递,不用返回值*/
//但实际上L始终保存的是头节点的地址,一直不会改变,只是后面的东西变了
Linklist HeadInsert(Linklist L, int k) {
//申请新节点
Node* s = (Node*)malloc(sizeof(Node));
if (s == NULL) {
printf("内存申请失败\n");
}
else {
s->data = k;
s->next = L->next;
L->next = s;
}
return L;
}
//尾插
Linklist Rearinsert(Linklist L, int k) {
//创建一个指针指向头节点
Node* p = L;
//循环,找到尾节点
while (p->next != NULL) {
p = p->next;
}
//创建新节点
Node* s = (Node*)malloc(sizeof(Node));
if (s == NULL) {
printf("内存申请失败\n");
}
else {
s->data = k;
s->next = NULL;
p->next = s;
}
return L;
}
//查,找不到返回空
Linklist Find(Linklist L, int k) {
Node* p = L->next;
//p不能为空,否则p->data,报错,p!=NULL也保证了找不到目标数据,且&&两侧条件不能调换顺序
while (p != NULL && p->data != k) {
p = p->next;
}
return p;
}
//中间插入:在x后面插入
Linklist Midinsert(Linklist L,int x, int k) {
Node* p = Find(L, x);
if (p == NULL) {
printf("数据不存在\n");
}
Node* s = (Node*)malloc(sizeof(Node));
if (s == NULL) {
printf("内存申请失败\n");
}
else {
s->data = k;
s->next = p->next;
p->next = s;
}
return L;
}
//删
//free(pre->next)释放的不是下个节点,而是节点的指针域
Linklist Delnode(Linklist L, int k) {
//判空
if (L->next == NULL) {
printf("空链表,删除失败");
}
//找到k所在的节点,和前一个节点
Node* pre = L;
Node* p = L->next;
while (p != NULL && p->data != k) {
pre = p;
p = p->next;
}
if (p == NULL) {
printf("数据%d不存在\n",k);
}
else {
pre->next = p->next;
}
free(p);
p = NULL;//防止p成为野指针
return L;
}
//改:把x改为k
Linklist Change(Linklist L, int x, int k) {
Node* p = Find(L, x);
if (p == NULL) {
printf("数据%d不存在\n",k);
}
p->data = k;
return L;
}
优缺点
优点:
- 灵活
- 操作效率高
缺点:
- 查找不方便
- 不支持存取
双向链表
比起单向链表,多了一个指针域保存前一个节点的地址。
#include<stdio.h>
#include<stdlib.h>
//双向链表
typedef struct Node {
int data;
Node* pre;
Node* next;
}Node,*Linklist;
//初始化函数,两个指针域赋空值
Linklist Initlist( ) {
Node* p = (Node*)malloc(sizeof(Node));
if (p == NULL) {printf("内存不足,初始化失败\n");}
else {
p->pre = p->next = NULL;
}
return p;
}
//增
// 处理原则:不能使链表(单项)断裂
//头增
Linklist HeadInsert(Linklist L, int k) {
Node* s = (Node*)malloc(sizeof(Node));
if (s == NULL) {
printf("内存不足,初始化失败\n");
return L;
}
else {
s->data = k;
//先添加s,添加后:(下一节点:s->next,上一节点L)
s->next = L->next;
s->pre = L;
L->next = s;
//空节点插入,可能没有s->next->pre
if (s->next != NULL) {
s->next->pre = s;
}
}
return L;
}
//尾增
Linklist RearInsert(Linklist L, int k) {
Node* s = (Node*)malloc(sizeof(Node));
if (s == NULL) {
printf("内存不足,初始化失败\n");
return L;
}
else {
s->data = k;
Node* p = L;
while (p->next != NULL) {//找到尾节点
p = p->next;
}
s->next = p->next;
s->pre = p;
p->next = s;
}
return L;
}
//查
Linklist Find(Linklist L, int des) {
Node* s = L->next;
while (s != NULL && s->data != des) {s = s->next;}
return s;
}
//中间插入
Linklist MidInsert(Linklist L, int des, int k) {
Node* p = Find(L, des);
if (p == NULL) {
printf("没有找到目标\n");
return L;
}
Node* s = (Node*)malloc(sizeof(Node));
if (s == NULL) {
printf("内存不足,初始化失败\n");
return L;
}
s->data = k;
s->next = p->next;
s->pre = p;
p->next = s;
if (s->next != NULL) {
s->next->pre = s;
}
return L;
}
//删,前指后,后(如果存在)指前,释放目标并赋空
Linklist Delnode(Linklist L, int k) {
if (L->next == NULL) {
printf("空链表\n");
return L;
}
Node* p = Find(L, k);
if (p == NULL) {
printf("没有找到目标\n");
return L;
}
//p->next不一定为空
p->pre->next = p->next;
if (p->next != NULL) {
p->next->pre = p->pre;
}
free(p);
p = NULL;
return L;
}
//改
Linklist Change(Linklist L, int des,int k) {
Node* p = Find(L, des);
if (p == NULL) {
printf("没有找到目标\n");
}
Node* s = (Node*)malloc(sizeof(Node));
if (s == NULL) {
printf("内存不足,初始化失败\n");
return L;
}
p->data = k;
return L;
}
循环链表
循环单链表
循环双链表