
还记得约瑟夫问题吧, 本来他们报数的顺序是固定的, 那么能否任意选择顺时针或者逆时针呢?
只需要加一个反向的指针就可以了, next之外加个prev.
其实能加一个, 也能加2个, 加3个 … 我们就可以随意使用指针, 随意建立复杂的数据关系. 理论上, 建模这块就获得了自由.
当然算法竞赛中, 数据的关系链, 我们更多的是用数据数组的下标来表示, 避免使用魔鬼般的指针.
双向链表


定义
struct Node {
int data;
Node *prev;
Node *next;
}
Node *mylist;
- 插入

s = new Node;
s->data = x;
s->next = p;
s->prev = p->prev;
p->prev->next = s;
p->prev = s;
- 删除

p->prev->next = p->next;
p->next->prev = p->prev;
delete(p);
完整程序
#include <stdio.h>
#include <stdlib.h>
// 双向循环链表
struct Node {
int data; //数据域
Node *prev; //指向前驱结点的指针
Node *next; //指向后继结点的指针
};
void init(Node **mylist)
{
*mylist = new Node;
(*mylist)->data = 0;
(*mylist)->prev = *mylist;
(*mylist)->next = *mylist;
}
// 插入元素操作
bool insert(Node *mylist, int i, int data)
{
// 判断链表是否存在
if (!mylist)
{
printf("list not exist!\n");
return false;
}
// 只能在位置1以及后面插入,所以i至少为1
if (i < 1)
{
printf("i is invalid!\n");
return false;
}
// 找到i位置所在的前一个结点
Node *front = mylist; // 这里是让front与i不同步,始终指向j对应的前一个结点
for (int j = 1; j < i; j++) {
front = front->next;
if (front == mylist)
{
printf("don't find front!\n");
return false;
}
}
// 创建一个空节点,存放要插入的新元素
Node *s = new Node;
s->data = data;
// 插入结点
s->prev = front;
s->next = front->next;
front->next->prev = s;
front->next = s;
return true;
}
// 删除元素操作
Node * deleteNode(Node *mylist, int i)
{
// 找到i位置所在的前一个结点
Node *front = mylist, *s;
for (int j = 1; j < i; j++)
{
front = front->next;
if (front->next == mylist)
{
printf("don't find front!\n");
return NULL;
}
}
s=front->next;
s->next->prev = front;
front->next = s->next;
return s;
}
// 头部插入元素操作
bool insert(Node *mylist, int data)
{
Node *head;
Node *s;
head = mylist;
// 创建存放插入元素的结点
s = new Node;
s->data = data;
// 头结点后插入结点
s->prev = head;
s->next = head->next;
head->next->prev = s;
head->next = s;
return true;
}
// 遍历链表操作
void print(Node *mylist)
{
Node *cur = mylist->next;
while (cur != mylist)
{
printf("<-->%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
int main()
{
Node *mylist;
// 初始化链表
init(&mylist);
for (int i = 0; i < 6; i++)
{
insert(mylist, i+1);
}
print(mylist);
// 插入结点
insert(mylist, 1, 9);
print(mylist);
printf("\n");
// 删除结点
deleteNode(mylist, 2);
print(mylist);
printf("\n");
return 0;
}
STL list
❝list 由双向链表(doubly linked list)实现而成,元素也存放在堆中,每个元素都是放在一块内存中,他的内存空间可以是不连续的,通过指针来进行数据的访问,这个特点使得它的随机存取变得非常没有效率,因此它没有提供 [] 操作符的重载。但是由于链表的特点,它可以很有效率的支持任意地方的插入和删除操作。❞
头文件
#include <list>
定义
list<int> a; // 定义一个int类型的列表a
list<int> a(10); // 定义一个int类型的列表a,并设置初始大小为10
list<int> a(10, 1); // 定义一个int类型的列表a,并设置初始大小为10且初始值都为1
list<int> b(a); // 定义并用列表a初始化列表b
//除此之外,还可以直接使用数组来初始化向量:
int n[] = { 1, 2, 3, 4, 5 };
list<int> a(n, n + 5);
基本操作
大小
- 容器大小:
lst.size();
- 容器最大容量:
lst.max_size();
- 更改容器大小:
lst.resize();
- 容器判空:
lst.empty();
添加函数
- 头部添加元素:
lst.push_front(const T& x);
- 末尾添加元素:
lst.push_back(const T& x);
- 任意位置插入一个元素:
lst.insert(iterator it, const T& x);
list<int> lst;
// 头部增加元素
lst.push_front(4);
// 末尾添加元素
lst.push_back(5);
// 任意位置插入一个元素
list<int>::iterator it = lst.begin();
lst.insert(it, 2);
// 任意位置插入n个相同元素
lst.insert(lst.begin(), 3, 9);
// 插入另一个向量的[forst,last]间的数据
list<int> lst2(5, 8);
lst.insert(lst.begin(), lst2.begin(), ++lst2.begin());
删除函数
- 头部删除元素:
lst.pop_front();
- 末尾删除元素:
lst.pop_back();
- 任意位置删除一个元素:
lst.erase(iterator it);
- 清空所有元素:
lst.clear();
#include <iostream>
#include <list>
using namespace std;
int main(int argc, char* argv[])
{
list<int> lst;
for (int i = 0; i < 8; i++)
lst.push_back(i);
// 头部删除元素
lst.pop_front();
// 末尾删除元素
lst.pop_back();
// 任意位置删除一个元素
list<int>::iterator it = lst.begin();
lst.erase(it);
// 删除[first,last]之间的元素
lst.erase(lst.begin(), ++lst.begin());
// 遍历显示
for (it = lst.begin(); it != lst.end(); it++)
cout << *it << " "; // 输出:3 4 5 6
cout << endl;
// 清空所有元素
lst.clear();
// 判断list是否为空
if (lst.empty())
cout << "元素为空" << endl; // 输出:元素为空
return 0;
}
访问函数
- 访问第一个元素:
lst.front();
- 访问最后一个元素:
lst.back();
#include <iostream>
#include <list>
using namespace std;
int main(int argc, char* argv[])
{
list<int> lst;
for (int i = 0; i < 6; i++)
lst.push_back(i);
// 访问第一个元素
cout << lst.front() << endl; // 输出:0
// 访问最后一个元素
cout << lst.back() << endl; // 输出:5
return 0;
}
其他函数
删除容器中相邻的重复元素:lst.unique();
#include <iostream>
#include <list>
using namespace std;
int main(int argc, char* argv[])
{
// 多个元素赋值s
list<int> lst1;
lst1.assign(3, 1);
list<int> lst2;
lst2.assign(3, 2);
// 交换两个容器的元素
// swap(lst1, lst2); // ok
lst1.swap(lst2);
// 遍历显示
cout << "交换后的lst1: ";
list<int>::iterator it;
for (it = lst1.begin(); it!=lst1.end(); it++)
cout << *it << " "; // 输出:2 2 2
cout << endl;
// 遍历显示
cout << "交换后的lst2: ";
for (it = lst2.begin(); it != lst2.end(); it++)
cout << *it << " "; // 输出:1 1 1
cout << endl;
list<int> lst3;
lst3.assign(3, 3);
list<int> lst4;
lst4.assign(3, 4);
// 合并两个列表的元素
lst4.merge(lst3); // 不是简单的拼接,而是会升序排列
cout << "合并后的lst4: ";
for (it = lst4.begin(); it != lst4.end(); it++)
cout << *it << " "; // 输出:3 3 3 4 4 4
cout << endl;
list<int> lst5;
lst5.assign(3, 5);
list<int> lst6;
lst6.assign(3, 6);
// 在lst6的第2个元素处,拼接入lst5
lst6.splice(++lst6.begin(), lst5);
cout << "拼接后的lst6: ";
for (it = lst6.begin(); it != lst6.end(); it++)
cout << *it << " "; // 输出:6 5 5 5 6 6
cout << endl;
// 删除容器中相邻的重复元素
list<int> lst7;
lst7.push_back(1);
lst7.push_back(1);
lst7.push_back(2);
lst7.push_back(2);
lst7.push_back(3);
lst7.push_back(2);
lst7.unique();
cout << "删除容器中相邻的重复元素后的lst7: ";
for (it = lst7.begin(); it != lst7.end(); it++)
cout << *it << " "; // 输出:1 2 3 2
cout << endl;
return 0;
}
迭代器
- 开始迭代器指针:
lst.begin();
- 末尾迭代器指针:
lst.end();
- 反向迭代器指针,指向最后一个元素:
lst.rbegin();
- 反向迭代器指针,指向第一个元素的前一个元素:
lst.rend();
#include <iostream>
#include <list>
using namespace std;
int main(int argc, char* argv[])
{
list<int> lst;
lst.push_back(1);
lst.push_back(2);
lst.push_back(3);
cout << *(lst.begin()) << endl; // 输出:1
cout << *(--lst.end()) << endl; // 输出:3
cout << *(lst.rbegin()) << endl; // 输出:3
cout << *(--lst.rend()) << endl; // 输出:1
cout << endl;
return 0;
}
算法
- 遍历元素
list<int>::iterator it;
for (it = lst.begin(); it != lst.end(); it++)
cout << *it << endl;
- 元素翻转
#include <algorithm>
lst.reverse();
- 元素排序
#include <algorithm>
lst.sort(); // 采用的是从小到大的排序
- 查找
find(lst.begin(), lst.end(), a);
例题 1 P1160 队列安排
题目描述
一个学校里老师要将班上 N 个同学排成一列,同学被编号为 ,他采取如下的方法:
先将1号同学安排进队列,这时队列中只有他一个人;
2-N同学依次入列,编号为i的同学入列方式为:老师指定编号为i的同学站在编号为 中某位同学(即之前已经入列的同学)的左边或右边;
从队列中去掉M(M<N)个同学,其他同学位置顺序不变。
在所有同学按照上述方法队列排列完毕后,老师想知道从左到右所有同学的编号。
输入格式
第1行为一个正整数N,表示了有N个同学。
第2-N行,第i行包含两个整数 k, p,其中 k 为小于 i 的正整数,p为 0 或者 1。若p为0,则表示将i号同学插入到k号同学的左边,p为1则表示插入到右边。
第N+1行为一个正整数M,表示去掉的同学数目。
接下来M行,每行一个正整数x,表示将x号同学从队列中移去,如果x号同学已经不在队列中则忽略这一条指令。
输出格式
1行,包含最多N个空格隔开的正整数,表示了队列从左到右所有同学的编号,行末换行且无空格。
输入输出样例
- 输入 #1复制
4
1 0
2 1
1 0
2
3
3
- 输出 #1复制
2 4 1
说明/提示
样例解释:
将同学2插入至同学1左边,此时队列为:
2 1
将同学3插入至同学2右边,此时队列为:
2 3 1
将同学4插入至同学1左边,此时队列为:
2 3 4 1
将同学3从队列中移出,此时队列为:
2 4 1
同学3已经不在队列中,忽略最后一条指令
最终队列:
2 4 1
数据范围
对于 20% 的数据,有 N≤10;
对于 40% 的数据,有 N≤1000;
对于 100% 的数据,有N, M≤100000。
分析
如果是数组的话, 一次插入和删除需要0(N)的时间, M次的话是O(NM)的复杂度, 会超时.
既然频繁的插入和删除, 那就用list好了.
但是如果每次用遍历的方法找到 k 同学, 同样是0(NM)的时间复杂度, 也会超时.
所以需要记录每个同学在列表的位置. 结合数组的随机访问和链表的O(1)的时间内的插入和删除的优点.
链表实现
#include <bits/stdc++.h>
using namespace std;
list <int> lst;
list <int> ::iterator sit[100006], it;
int a[100006];
int n, m;
int main()
{
scanf("%d", &n);
lst.push_back(1);
sit[1] = lst.begin();
int idx, dir;
for(int i = 2; i <= n; i++) {
scanf("%d %d", &idx, &dir);
if(dir == 0) {
it = lst.insert(sit[idx], i);
} else {
it = sit[idx];
it++;
it = lst.insert(it, i);
}
sit[i] = it;
}
scanf("%d", &m);
for(int i = 0; i < m; i++) {
scanf("%d", &idx);
if(a[idx] == 0)
lst.erase(sit[idx]);
a[idx] = 1;
}
for(it = lst.begin(); it != lst.end(); it++) {
if(it != lst.begin())
printf(" %d", *it);
else
printf("%d", *it);
}
printf("\n");
return 0;
}
题单
- HDU – 1276 士兵队列训练问题
- P1160 队列安排
- UVA11988 破损的键盘 Broken Keyboard (a.k.a. Beiju Text)
- UVA12657 移动盒子 Boxes in a Line