--由于博主还未系统地学习过数据结构,只是闲时自学,可能理解有误,欢迎指出。
本篇主要通过题目来讲解
- 链表
基本特性:1)内存一般不连续,一块块连接
2)查找元素较慢,只能通过指针逐个移动查找。但这个缺点一般可以通过数组模拟链表来解决--数组的下标表示不同的内存编号,数组的内容表示该内存的内容,由此可以实现类似的下标查询。
3)插入删除的操作相较于内存连续的数组更加方便
大意:
一个学校里老师要将班上NNN个同学排成一列,同学被编号为1∼N1\sim N1∼N,他采取如下的方法:
-
先将111号同学安排进队列,这时队列中只有他一个人;
-
2−N2-N2−N号同学依次入列,编号为i的同学入列方式为:老师指定编号为i的同学站在编号为1∼(i−1)1\sim (i -1)1∼(i−1)中某位同学(即之前已经入列的同学)的左边或右边;
扫描二维码关注公众号,回复: 7119064 查看本文章 -
从队列中去掉M(M<N)M(M<N)M(M<N)个同学,其他同学位置顺序不变。
在所有同学按照上述方法队列排列完毕后,老师想知道从左到右所有同学的编号。
非常典型的链表结构--由于链表的内存不连续,某个元素的删除对其他元素的相互联系影响不大。同时链表的题目我觉得能不用指针就不用指针,能用数组模拟就用数组模拟(数组模拟方便查询)。涉及到前方插入和后方插入所以使用双向链表。
双向链表注意点:前后兼顾,每次插入删除时要同时考虑被操作的节点的pre和nxt节点。
--链表的节点创建:
利用数组模拟链表
@para pre 上一个节点的编号(注意是“内存编号”, 不是内容)
@para nxt 下一个节点的编号
@para id 本节点的内容
p[maxn]数组存的可以看作是链表的每一个内存块,顺序还没有定
struct Peo { int pre, nxt; int id; }p[maxn]; //结构体双向链表
--插入操作:
链表的插入操作就是将一个新的内存与原本链表的相邻两个内存建立新的联系(中间插入时),或与首尾建立联系。链表内存之间的联系通过内存的编号。
/*
@para id 插入的新内存的编号<==>数组的下标
@para k 要插入位置的相邻的内存编号
@note 链表内存块之间的联系时通过-->“内存编号”
记录链表的root和tail的变化,方便遍历或者删除确定区间
*/
void _insert(int k, int P, int id) { if(P == 1) { //后入 p[id].nxt = p[k].nxt; //新内存的下一个联系为原内存的下一个联系 p[id].pre = k; //新内存的上一个联系为原内存 p[p[k].nxt].pre = id; //原内存的下一个联系的内存 的上一个联系更新为新内存 p[k].nxt = id; //原内存的下一个联系为要插入的新内存 if(k == tail) tail = id; //记录链表的尾部方便之后的访问 } else { //前入 p[id].nxt = k; p[id].pre = p[k].pre; p[p[k].pre].nxt = id; p[k].pre = id; if(k == root) root = id; } return ; }
--删除操作:
删除可以看作是覆盖,(假设a-b-c)将b删除其实就是用c覆盖掉b,即将a的下一个联系重设为c,由于是双向链表-->前后兼顾,所欲还要将c的上一个联系重设为a。
//注意如果不是用数组模拟而是用指针
//那么a(为指针的情况)被舍弃后最好释放a的内存--delete a;
void _dele(int a) { if(out[a] || !sz) return; out[a] = true, sz -= 1; if(a == root) root = p[a].nxt; else if(a == tail) tail = p[a].pre; else { p[p[a].pre].nxt = p[a].nxt; p[p[a].nxt].pre = p[a].pre; } return; }
#include <cstdio> #include <iostream> using namespace std; const int maxn = 1e5 +2; struct Peo { int pre, nxt; int id; }p[maxn]; //结构体双向链表 bool out[maxn]; int pos[maxn], sz, root, tail; void _insert(int k, int P, int id) { if(P == 1) { //后入 p[id].nxt = p[k].nxt; p[id].pre = k; p[p[k].nxt].pre = id; p[k].nxt = id; if(k == tail) tail = id; } else { //前入 p[id].nxt = k; p[id].pre = p[k].pre; p[p[k].pre].nxt = id; p[k].pre = id; if(k == root) root = id; } return ; } void _dele(int a) { if(out[a] || !sz) return; out[a] = true, sz -= 1; if(a == root) root = p[a].nxt; else if(a == tail) tail = p[a].pre; else { p[p[a].pre].nxt = p[a].nxt; p[p[a].nxt].pre = p[a].pre; } return; } int n, m; int main() { scanf("%d", &n); root = tail = 1; for(int i=2; i<=n; ++i) { int k, p; scanf("%d %d", &k, &p); _insert(k, p, i); } sz = n; scanf("%d", &m); while(m--) { int a; scanf("%d", &a); _dele(a); } if(sz > 0) { while(root != tail) { printf("%d ", root); root = p[root].nxt; } printf("%d\n", tail); } else puts(""); return 0; }
大意:
n个人(n<=100)围成一圈,从第一个人开始报数,数到m的人出列,再由下一个人重新从1开始报数,数到m的人再出圈,……依次类推,直到所有的人都出圈,请输出依次出圈人的编号.
单向链表的数据结构,在之前讲了下双向链表之后,单向链表相比更加简单只需要顾后。
节点的建立:
struct Peo { int id; //当前内存的内容 int nxt; //下一个内存的“编号” }p[maxn];
这道题主要讲讲链表的遍历:
单向链表只能从头到尾遍历,一般用一个新的变量(这里设为now吧)来表示当前遍历到的内存编号,初始now为链表首节点内存的编号0,之后按需移动now = list[now].nxt(now指向了下一个内存编号)。
/* 模拟链表 */ #include <iostream> #include <cstdio> using namespace std; const int maxn = 100 + 4; struct Peo { int id; int nxt; }p[maxn]; int pre, sz; int n, m; int main() { scanf("%d %d", &n, &m); sz = n; for(int i=0; i<n-1; ++i) { //i为人的顺序号,并不是编号 p[i].id = i+1; p[i].nxt = i+1; } p[n-1].id = n; p[n-1].nxt = 0; int now = 0; bool first = true; while(sz > 1) { if(first) { for(int j=0; j<m-2; ++j) { now = p[now].nxt; } first = false; } else for(int j=0; j<m-1; ++j) { now = p[now].nxt; } cout << p[p[now].nxt].id << ' '; p[now].nxt = p[p[now].nxt].nxt; sz -= 1; } if(sz) cout << p[now].id << endl; return 0; }