算法竞赛进阶指南读书笔记——0x13链表与邻接表

链表与邻接表

链表

双向链表

定义:支持在任意位置插入或删除,但只能按顺序依次访问其中的元素的数据结构

性质:

1.支持在任意位置插入、删除元素

2.只能按顺序访问其中的元素

存储:

s t r u c t struct struct表示节点,用 p r e v prev prev n e x t next next指向前后相邻的两个节点;建立额外的两个节点 h e a d head head t a i l tail tail代表链表头尾,避免左右两端或空链表访问越界,如图:

z3XgZq.png

操作:

1.建表 i n i t i a l i z e ( ) initialize() initialize():

初始化 h e a d head head t a i l tail tail;此时数组中已有两个元素,故 t o t tot tot更新为2,如图:

z8XcH1.png

2.插入元素 i n s e r t ( p , v a l ) insert(p,val) insert(p,val):

为了在节点 p p p后插入新节点,我们先新建节点 q q q,并完成值的写入;接着我们从后往前处理连接关系:先是 p p p-> n e x t next next,从插入后的结果来看,影响的是 p p p-> n e x t next next p r e v prev prev,再看 q q q,我们直接将它的 p r e v prev prev n e x t next next连接完成,最后是 p p p,从插入后的结果来看,影响的是 p p p n e x t next next,如图:

z8v9zD.png

至于为何按照从后往前的顺序,因为我们在节点 p p p后插入节点,新节点会成为 p p p-> n e x t next next,为了防止丢失 p p p-> n e x t next next的索引,故须按从后往前的顺序进行处理。

3.删除元素 r e m o v e ( p ) remove(p) remove(p):

删除节点 p p p会影响 p p p-> n e x t next next p r e v prev prev p p p-> p r e v prev prev n e x t next next,直接修改即可,如图:

z8vWlD.png

若为指针存储,可以将 p p p直接 d e l e t e delete delete释放内存;若为数组模拟,无法删除,直接无视即可。

4.删表 r e c y c l e ( ) / c l e a r ( ) recycle()/clear() recycle()/clear():

对于指针存储,从 h e a d head head-> n e x t next next开始依次删去 p r e v prev prev,并利用 n e x t next next向后移动 h e a d head head遍历链表,最后将 t a i l tail tail删去,如图:

z8xKtx.png

对于数组模拟,只需 m e m s e t memset memset n o d e [ ] node[] node[]并置 h e a d , t a i l , t o t head,tail,tot head,tail,tot为0即可。

实现1:指针

struct Node {
  int value;
  Node *prev, *next;
};
Node *head, *tail; // *head, *tail仅为指针,并未指向具体的对象

void initialize() {
  head = new Node();
  tail = new Node();
  head->next = tail;
  tail->prev = head;
}

void insert(Node *p, int val) {
  Node *q = new Node(); // 1.创建新对象 2. 用指针指向新对象
  q->value = val;
  p->next->prev = q;
  q->next = p->next;
  q->prev = p;
  p->next = q;
}

void remove(Node *p) {
  p->prev->next = p->next;
  p->next->prev = p->prev;
  delete p;
}

void recycle() {
  while (head != tail) {
    head = head->next;
    delete head->prev;
  }
  delete tail;
}

实现2:数组

struct Node {
  int value;
  int prev, next;
} node[N];
int head, tail, tot;

int initialize() {
  tot = 2;
  head = 1, tail = 2;
  node[head].next = tail;
  node[tail].prev = head;
}

int insert(int p, int val) {
  int q = ++tot;
  node[q].value = val;
  node[node[p].next].prev = q; // 下标本身无prev,next,需从node[]中取出对应元素来访问prev,next
  node[q].next = node[p].next; // prev,next存储的是下标,赋值时需注意类型匹配
  node[q].prev = p;
  node[p].next = q;
}

void remove(int p) {
  node[node[p].prev].next = node[p].next;
  node[node[p].next].prev = node[p].prev;
}

void clear() {
  memset(node, 0, sizeof(node));
  head = tail = tot = 0;
}

与数组比较:

随机访问任意位置插入删除
数组×
链表×

例1邻值查找

思路1:链表

由于 1 ⩽ j < i 1\leqslant j<i 1j<i,所以 a a a自然划分为 a [ 1.. i ) , a [ i ] a[1..i),a[i] a[1..i),a[i] a [ i + 1 , n ] a[i+1,n] a[i+1,n],且 a [ i + 1 , n ] a[i+1,n] a[i+1,n] a j a_j aj的寻找没有影响。

根据这样的思路,我们在考虑 a n a_n an的邻值时,可以在 a [ 1.. n ) a[1..n) a[1..n)中查找;在考虑 a n − 1 a_{n-1} an1的邻值时,需要排除 a n a_n an的影响,只能在 a [ 1.. n − 1 ) a[1..n-1) a[1..n1)中查找。

同时,我们希望 { a n } \{a_n\} {an}已经有序,并能知道 a i a_i ai邻值的原始下标。为此,我们可以采取双关键字排序,利用 p a i r pair pair a i a_i ai的值写入 f i r s t first first,将原始下标 i i i写入 s e c o n d second second,并对 f i r s t first first关键字进行排序。这样我们就能方便的找到 a i a_i ai的邻值了。

并且排序有一个非常好的性质,如果我们删除已有序序列中的任意元素,剩下的元素仍然是有序的。结合上述分析,我们从 a n a_n an开始寻找邻值,寻找完毕后,将 a n a_n an删除,再寻找 a n − 1 a_{n-1} an1的邻值,以此类推。

但是问题随即出现:在对存有 a i a_i ai值和原始下标的 p a i r pair pair< i n t , i n t int,int int,int> [ ] [] []进行排序后,原先的 a n a_n an未必处于最后方, a n − 1 a_{n-1} an1也未必与 a n a_n an相邻。而数组是不支持在任意位置插入删除元素的数据结构,所以我们需要将这两个字段的数据 ( a i 的 值 , 原 始 下 标 ) (a_i的值,原始下标) (ai,)交给能支持任意位置插入删除的链表进行维护。

同时我们注意到链表仅支持顺序访问,这对寻找邻值没有影响,因为在排好序的 p a i r pair pair< i n t , i n t int,int int,int> [ ] [] []中, p p p的邻值一定在 p p p-> p r e v prev prev p p p-> n e x t next next之间,符合顺序访问的要求。但是我们在寻找邻值时按照 a n . . a 2 a_n..a_2 an..a2的顺序进行,但原始下标为 i i i i − 1 i-1 i1的数据未必在链表上也连续,所以我们希望借助数组 b b b按照原始下标的顺序将对链表块的索引进行存储,以实现数组下标到链表位置的转换,即链表的跳跃式访问。

总结一下,我们需要做如下操作:

1.将原始数据 ( a i , i ) (a_i,i) (ai,i)存储到 p a i r pair pair< i n t , i n t int,int int,int> [ ] [] []中并按 f i r s t first first关键字排序

2.将排序后的数据存储到链表上,并使用 ∗ b [ i ] *b[i] b[i]指向原始下标为 i i i a i a_i ai所在的链表块

3.从原始下标为 n n n开始寻找邻值,找到后暂存答案,并将该元素对应的链表块删除,继续寻找前一个元素的邻值;最后输出答案

实现:

pair<int, int> p[N], ans[N];
struct Node {
  int value, pos;
  Node *prev, *next;
};
Node *head, *tail;
Node *b[N];
void initialize() {
  head = new Node();
  tail = new Node();
  head->next = tail;
  tail->prev = head;
}
Node *insert(Node *p, int val, int pos) { // 返回指向新块的指针
  Node *q = new Node();
  q->value = val;
  q->pos = pos;
  p->next->prev = q;
  q->next = p->next;
  q->prev = p;
  p->next = q;
  return q;
}
void remove(Node *p) {
  p->next->prev = p->prev;
  p->prev->next = p->next;
  delete p;
} // 无需回收,循环的同时在删除链表
int main() {
  int n, pre, nxt;
  cin >> n;
  for (int i = 0; i < n; i++) {
    cin >> p[i].first;
    p[i].second = i;
  }
  sort(p, p + n); // pair默认以first为第一关键字升序排列
  initialize();
  for (int i = 0; i < n; i++)
    if (!i)
      b[p[i].second] = insert(head, p[i].first, p[i].second);
    else
      b[p[i].second] = insert(b[p[i - 1].second], p[i].first, p[i].second);
  for (int i = n - 1; i > 0; i--) {
    if (b[i]->next == tail)
      ans[i].first = b[i]->value - b[i]->prev->value,
      ans[i].second = b[i]->prev->pos;
    else if (b[i]->prev == head)
      ans[i].first = b[i]->next->value - b[i]->value,
      ans[i].second = b[i]->next->pos;
    else {
      pre = abs(b[i]->prev->value - b[i]->value),
      nxt = abs(b[i]->next->value - b[i]->value);
      if (pre < nxt)
        ans[i].first = pre, ans[i].second = b[i]->prev->pos;
      else if (nxt < pre)
        ans[i].first = nxt, ans[i].second = b[i]->next->pos;
      else {
        ans[i].first = pre;
        ans[i].second = b[i]->prev->pos; // 前驱小于后继
      }
    }
    remove(b[i]);
  }
  for (int i = 1; i < n; i++)
    cout << ans[i].first << ' ' << ans[i].second + 1 << endl; // 存储时从0开始,题目下标从1开始
  return 0;
}

思路2:STL set

仿照以上思路,对称地考虑,我们可以将元素一个一个插入来消除后面元素对目前已有元素的干扰。这样,我们希望有这样一个数据结构,可以维护一个有序集合 S S S,且支持动态插入与前驱后继查询。而STL set为我们实现了一个二叉平衡树可以满足我们的要求。

实现:

struct rec {
  int id, value;
} cur;
bool operator<(const rec &a, const rec &b) { return a.value < b.value; }
set<rec> s;
int main() {
  int n, a;
  set<rec>::iterator pre, nxt;
  cin >> n;
  for (int i = 0; i < n; i++) {
    cin >> a;
    cur.id = i + 1, cur.value = a;
    if (i) {
      pre = --s.lower_bound(cur); // 1.
      nxt = s.upper_bound(cur);
      if (pre == s.end())
        cout << abs(a - nxt->value) << ' ' << nxt->id << endl;
      else if (nxt == s.end())
        cout << abs(a - pre->value) << ' ' << pre->id << endl;
      else {
        if (abs(a - pre->value) < abs(a - nxt->value))
          cout << abs(a - pre->value) << ' ' << pre->id << endl;
        else if (abs(a - pre->value) > abs(a - nxt->value))
          cout << abs(a - nxt->value) << ' ' << nxt->id << endl;
        else
          cout << abs(a - pre->value) << ' ' << pre->id << endl;
      }
    }
    s.insert(cur);
  }
  return 0;
}

细节:

1. s e t . l o w e r _ b o u n d ( x ) set.lower\_bound(x) set.lower_bound(x)会返回 s e t set set中第一个 ⩾ x \geqslant x x的元素, s e t . u p p e r _ b o u n d ( x ) set.upper\_bound(x) set.upper_bound(x)会返回 s e t set set中第一个 > x > x >x的元素;因此, s e t . l o w e r _ b o u n d ( x ) set.lower\_bound(x) set.lower_bound(x)的前一个元素即为 x x x的前驱,即 < x <x <x的最后一个元素,而 s e t . u p p e r _ b o u n d ( x ) set.upper\_bound(x) set.upper_bound(x)即为 x x x的后继。

值得注意的是,–需写在函数调用前,否则含义变为将返回值递减,但返回递减前的值(a–返回a,副作用使得a-=1)。

邻接表

定义:带有索引(表头)数组的多个数据链表构成的结构集合

性质:

1.数据被分成若干类,每一类数据构成一个链表

2.可通过表头数组定位到某一类数据对应的链表

存储:

h e a d head head存储当前类别的表头,每个元素的 n e x t next next指向下一个链表块,末端的链表块指向 0 0 0值,如图:

zJf6Wn.png

应用:用于树、图的存储

将图的每条边按起点分类(易于按起点遍历),同时按顺序存储权值 e d g e [ ] edge[] edge[]与终点 v e r [ ] ver[] ver[],而链表块的 v a l u e value value存储对应边的数组下标,通过 h e a d , n e x t head,next head,next指向后得到相应的 v a l u e value value,如图:

zJhX3n.png

由于 e d g e , v e r , n e x t edge,ver,next edge,ver,next是对每条边存的,我们可以将 e d g e [ i ] , v e r [ i ] , n e x t [ i ] edge[i],ver[i],next[i] edge[i],ver[i],next[i]理解为 G r a p h [ i ] . e d g e , G r a p h [ i ] . v e r , G r a p h [ i ] . n e x t Graph[i].edge,Graph[i].ver,Graph[i].next Graph[i].edge,Graph[i].ver,Graph[i].next,将下标放入对应字段的数组中实现属性的读取。

操作:

1.加边 a d d ( x , y , z ) add(x,y,z) add(x,y,z):

即加入有向边 ( x , y ) (x,y) (x,y),权值为 z z z.

由于 h e a d head head可以定位到以某一边为起点的一类边,我们利用 h e a d [ x ] head[x] head[x]进行插入,如图:

zJ4svq.png

由于我们将新边插入到 h e a d [ x ] head[x] head[x]后,故需按照从后往前的顺序,在完成权值与终点的存储后,从新边 n e x t [ t o t ] next[tot] next[tot]开始进行更新,防止丢失链表块 h e a d [ x ] head[x] head[x]的索引。从结果来看,链表块 h e a d [ x ] head[x] head[x]并没有发生改变,无需更新。

容易发现,后插入的边在链表上的位置会在先插入的边的前面;同时,不在链尾插入的原因是无法直接访问到链尾。

2.访问 x x x的出边:

h e a d [ x ] head[x] head[x]开始,利用 n e x t next next依次向后访问,直到遇到 0 0 0终止。注意 h e a d , n e x t head,next head,next存储的是边的下标, h e a d head head存储的是所有类别的边的最后一个存入的边的下标,而 n e x t next next存储的是当前边所处链表块的下一个链表块对应的边的下标。

实现:

void add(int x, int y, int z) {
  ver[++tot] = y, edge[tot] = z; // 1.
  nxt[tot] = head[x], head[x] = tot; // 2.
}

for (int i = head[x]; i; i = nxt[i]) {
  int y = ver[i], z = edge[i];
}

细节:

1.使用++ t o t tot tot,使边的下标从1开始存储,从而使得边的下标为 0 0 0意味着终止;对于无向图,可以初始化 t o t = 1 tot=1 tot=1,从2开始存储,从而可以利用成对变换找到与之反向的边(如 2 ⊕ 1 = 3 , 3 ⊕ 1 = 2 2\oplus1=3,3\oplus1=2 21=3,31=2

2.不命名为 n e x t [ ] next[] next[]:防止重名

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值