文章目录
1.链表的定义与逻辑
链表和顺序表虽然同为线性表,皆为线性逻辑结构,但在物理存储结构上却不相同。
顺序表的底层逻辑数组本身就是存储在连续的内存空间中的
而链表则是⼀种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过元素本身存储的指针部分链接次序实现的,即元素整体组成一个一个的结点,通过他们指针域里存储的指针连接。
相比于顺序表,链表更加适合有对元素频繁插入删除或者查询元素的情境中。
typedef struct Node
{
int data; //元素的数据域
struct Node* next; //元素的指针域
} Node;
2.对链表的手动实现
2.1单链表
定义
Tips:搓个结构体不方便,用两个绑定的数组实现即可。
如下:
const int N = 1e5 + 10;
int h; // 头指针,指向头结点的位置
int id; // 下⼀个元素分配的位置
int e[N], ne[N]; // 数据域和指针域,指针域定义在全局默认为0,当做空指针
头插
要在链表中头插一个元素,必须先把目标插入元素中的指针域填入头结点中存储的第一个元素的指针,再把头结点中的指针更新为指向此次插入元素的指针,顺序不能颠倒,否则会导致原先第一个节点的链接丢失。
如下:
void front_push(int x)
{
id++;
e[id] = x;
//在下一个物理位置上填入元素
ne[id] = ne[h]; //插入元素指针指向后继
ne[h] = id; //头结点指针指向头插元素
}
遍历链表
void print()
{
for(int i = ne[h]; i; i = ne[i])//从第一个元素开始遍历,直到遇见空指针域的元素为止
{
cout << e[i] << " ";
}
cout << endl;
}
按值查找
有两种策略:
- 遍历整个链表+if
int find(int x)
{
for(int i = ne[h]; i; i = ne[i])
{
if(e[i] == x)
return i;
}
return -1;//没找到就返回-1下标
}
- 创建一个新数组(map),用来存储每个元素的下标位置,以空间换时间
局限性:
- 存储数据的值不能过大,开这么大数组不好
- 不能存储重复元素的位置
int map[N];
void front_push(int x)
{
id++;
e[id] = x;
map[x] = id;//存入数据的位置
ne[id] = ne[h];
ne[h] = id;
}
int find(int x)
{
return map[x];
}
在任意位置之后插入元素
这里的任意位置代表逻辑位置,不是物理位置。
逻辑和头插一样,不过头结点换成想要的结点即可
如下:
void insert_anyone_back(int p, int x)
{
id++;
e[id] = x;
map[x] = id;
ne[id] = ne[p]; //x指向p的后面
ne[p] = id; //p指向x
}
删除任意位置之后的元素
只要让目标位置的指针指向下个元素指向的下个元素就行了。
但如果要删除最后一个位置的元素的话会给ne[ ]传入0,指向头结点,导致链表循环,结构被破坏。
如下:
void erase(int p)
{
if(ne[p]) //特判ne[p]不是空指针,即p位置不是最后一个位置的元素
{
map[e[ne[p]]] = 0; //清空map记录的p位置下一个元素的位置记录
ne[p] = ne[ne[p]]; //跳过被删除元素
}
}
EX-为什么不尾插,尾删,任意位置插入?
时间复杂度太高了,没必要而已~
其他数据结构可以更好完成任务~
3.1双链表
多开一个数组,存储前一个元素的存储位置就行了。
const int N = 1e5 + 10;
int h; // 头结点存储位置
int id; // 下⼀个元素分配的位置
int e[N]; // 数据域
int pre[N], ne[N]; // 前后指针域
头插
顺序如下:
- 先处理插入元素的前后指针域
- 再处理插入元素前驱的后指针域
- 最后处理头结点的前指针域
void front_push(int x)
{
id++;
e[id] = x;
map[x] = id;
ne[id] = ne[h];
pre[id] =