数据结构:是相互之间存在一种或多种特定关系的数据元素的集合。
目录
基本概念
数据:是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别,并输入给计算机处理的符号集合。(数据不仅仅包括整型,实型等数值类型,还包括字符及声音,图像,视频等非数值类型。
数据元素:是组成数据的,有一定意义的基本单位,在计算机中通常作为整体处理,也被称为记录。
数据项:一个数据元素可以有若干个数据项组成。
注:数据项是数据不可分割的最小单位。但数据元素才是数据结构中建立数据模型的着眼点。
数据对象:是性质相同的数据元素的集合,是数据的子集。
逻辑结构与物理结构
(1). 逻辑结构:是指数据对象中数据元素之间的相互关系。
1.根据数据元素之间关系的不同特性,通常有四种基本结构:集合结构,线性结构,树形结构和图状结构。
a. 集合结构:集合结构中的数据元素除了同属于一个集合外,它们之间没有其他关系。
b.线性结构:线性结构中的数据元素中间是一对一的关系。
c.树形结构:树形结构中的数据元素之间存在一种一对多的层次关系。
d. 图状结构:图形结构的数据元素是多对多的关系。
2.根据数据元素之间关系的不同特性,数据结构又可分为线性结构和非线性结构。
a.线性结构:线性表,栈,队列,字符串,数组和广义表。
b.非线性结构:树和图。
(2).物理结构(存储结构):是指数据的逻辑结构在计算机中的存储形式。
a.顺序存储结构:是把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的。
b.链式存储结构:是把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。
注:存储器主要是针对内存而言的,像硬盘,软盘,光盘等外部存储器的数据组织通常用文件结构来描述。
注:逻辑结构是面向问题的,而物理结构就是面向计算机的。
数组实现
单链表
826. 单链表
实现一个单链表,链表初始为空,支持三种操作:
- 向链表头插入一个数;
- 删除第 k 个插入的数后面的数;
- 在第 k 个插入的数后插入一个数。
现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。
注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:
H x
,表示向链表头插入一个数 x。D k
,表示删除第 k 个插入的数后面的数(当 k 为 0 时,表示删除头结点)。I k x
,表示在第 k 个插入的数后面插入一个数 xx(此操作中 k 均大于 0)。输出格式
共一行,将整个链表从头到尾输出。
数据范围
1≤M≤100000
所有操作保证合法。输入样例:
10 H 9 I 1 1 D 1 D 0 H 6 I 3 6 I 4 5 I 4 5 I 3 4 D 6
输出样例
6 4 6 5
#include <iostream>
using namespace std;
const int N = 100010;
// head 表示头结点的下标
// e[i] 表示节点i的值
// ne[i] 表示节点i的next指针是多少
// idx 存储当前已经用到了哪个点
int head, e[N], ne[N], idx;
// 初始化
void init()
{
head = -1;
idx = 0;
}
// 将x插到头结点
void add_to_head(int x)
{
e[idx] = x, ne[idx] = head, head = idx ++ ;
}
// 将x插到下标是k的点后面
void add(int k, int x)
{
e[idx] = x, ne[idx] = ne[k], ne[k] = idx ++ ;
}
// 将下标是k的点后面的点删掉
void remove(int k)
{
ne[k] = ne[ne[k]];
}
int main()
{
int m;
cin >> m;
init();
while (m -- )
{
int k, x;
char op;
cin >> op;
if (op == 'H')
{
cin >> x;
add_to_head(x);
}
else if (op == 'D')
{
cin >> k;
if (!k) head = ne[head];
else remove(k - 1);
}
else
{
cin >> k >> x;
add(k - 1, x);
}
}
for (int i = head; i != -1; i = ne[i]) cout << e[i] << ' ';
cout << endl;
return 0;
}
双链表
827. 双链表
实现一个双链表,双链表初始为空,支持 5 种操作:
- 在最左侧插入一个数;
- 在最右侧插入一个数;
- 将第 k 个插入的数删除;
- 在第 k 个插入的数左侧插入一个数;
- 在第 k 个插入的数右侧插入一个数
现在要对该链表进行 M 次操作,进行完所有操作后,从左到右输出整个链表。
注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:
L x
,表示在链表的最左端插入数 x。R x
,表示在链表的最右端插入数 x。D k
,表示将第 k 个插入的数删除。IL k x
,表示在第 k 个插入的数左侧插入一个数。IR k x
,表示在第 k 个插入的数右侧插入一个数。输出格式
共一行,将整个链表从左到右输出。
数据范围
1≤M≤100000
所有操作保证合法。输入样例:
10 R 7 D 1 L 3 IL 2 10 D 3 IL 2 7 L 8 R 9 IL 4 7 IR 2 2
输出样例:
8 7 7 3 2 9
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n;
int l[N], r[N], e[N], idx;
//初始化
void init()
{
l[1] = 0, r[0] = 1; //0是左端点,1是右端点
idx = 2; //idx已经用掉了0和1
}
//在节点k的右边插入一个数x
void add(int k, int x)
{
e[idx] = x;
r[idx] = r[k];
l[idx] = k;
l[r[k]] = idx;
r[k] = idx ++;
}
//删除节点k
void remove(int k)
{
l[r[k]] = l[k];
r[l[k]] = r[k];
}
int main()
{
init();
cin >> n;
while (n --)
{
string op;
int k, x;
cin >> op;
if (op == "L")
{
cin >> x;
add(0, x);
}
else if (op == "R")
{
cin >> x;
add(l[1], x);
}
else if (op == "D")
{
cin >> k;
remove(k + 1);
}
else if (op == "IL")
{
cin >> k >> x;
add(l[k + 1], x);
}
else
{
cin >> k >> x;
add(k + 1, x);
}
}
for (int i = r[0]; i != 1; i = r[i]) printf("%d ", e[i]);
return 0;
}
栈
828. 模拟栈
实现一个栈,栈初始为空,支持四种操作:
push x
– 向栈顶插入一个数 x;pop
– 从栈顶弹出一个数;empty
– 判断栈是否为空;query
– 查询栈顶元素。现在要对栈进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令为
push x
,pop
,empty
,query
中的一种。输出格式
对于每个
empty
和query
操作都要输出一个查询结果,每个结果占一行。其中,
empty
操作的查询结果为YES
或NO
,query
操作的查询结果为一个整数,表示栈顶元素的值。数据范围
1≤M≤100000,
1≤x≤10^9
所有操作保证合法。输入样例:
10 push 5 query push 6 pop query pop empty push 4 query empty
输出样例:
5 5 YES 4 NO
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n;
int stk[N], tt; // 用tt表示栈顶所在的索引 初始时tt为0,表示栈为空
int main()
{
cin >> n;
while (n --)
{
string op;
int x;
cin >> op;
if (op == "push") //栈顶所在索引往后移动一格,然后放入x
{
cin >> x;
stk[++ tt] = x;
}
else if (op == "pop") tt --;
else if (op == "query") printf("%d\n", stk[tt]);
else cout << (tt ? "NO" : "YES") << endl;
}
return 0;
}
830. 单调栈
给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。
输入格式
第一行包含整数 N,表示数列长度。
第二行包含 N 个整数,表示整数数列。
输出格式
共一行,包含 N 个整数,其中第 i 个数表示第 i 个数的左边第一个比它小的数,如果不存在则输出 −1。
数据范围
1≤N≤10^5
1≤数列中元素≤10^9输入样例:
5 3 4 2 7 5
输出样例:
思路:由于求第i个数左边的第一个比它小的数,因此若当前数小于它左边的数,则该数左边的数一定不会被输出,则移除。所以维护一个单调递增栈。
用单调递增栈,当该元素可以入栈的时候,栈顶元素就是它左侧第一个比它小的元素。
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n;
int stk[N], tt;
int main()
{
cin >> n;
while (n --)
{
int x;
cin >> x;
while (tt && stk[tt] >= x) tt --; //栈顶元素不小于当前元素x,则出栈
if (! tt) printf("%d ", -1); //栈空,即没有元素比当前元素x小 输出-1
else printf("%d ", stk[tt]); //输出栈顶,即是左边第一个比当前元素x小的数
stk[++ tt] = x; //更新栈 将当前元素x加入栈中
}
return 0;
}
队列
829. 模拟队列
实现一个队列,队列初始为空,支持四种操作:
push x
– 向队尾插入一个数 x;pop
– 从队头弹出一个数;empty
– 判断队列是否为空;query
– 查询队头元素。现在要对队列进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令为
push x
,pop
,empty
,query
中的一种。输出格式
对于每个
empty
和query
操作都要输出一个查询结果,每个结果占一行。其中,
empty
操作的查询结果为YES
或NO
,query
操作的查询结果为一个整数,表示队头元素的值。数据范围
1≤M≤100000
1≤x≤10^9
所有操作保证合法。输入样例:
10 push 6 empty query pop empty push 3 push 4 pop query push 6
输出样例:
NO 6 YES 4
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n;
// [hh, tt]之间为队列
int q[N], hh, tt = -1; //hh队头,tt队尾
int main()
{
cin >> n;
while (n --)
{
int x;
string op;
cin >> op;
if (op == "push")
{
cin >> x;
q[++ tt] = x;
}
else if (op == "query") printf("%d\n", q[hh]);
else if (op == "empty") printf("%s\n", (tt >= hh ? "NO" : "YES"));
else hh ++;
}
return 0;
}
154. 滑动窗口
给定一个大小为 n≤10^6 的数组。
有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到 k 个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为
[1 3 -1 -3 5 3 6 7]
,k 为 3。
窗口位置 最小值 最大值 [1 3 -1] -3 5 3 6 7 -1 3 1 [3 -1 -3] 5 3 6 7 -3 3 1 3 [-1 -3 5] 3 6 7 -3 5 1 3 -1 [-3 5 3] 6 7 -3 5 1 3 -1 -3 [5 3 6] 7 3 6 1 3 -1 -3 5 [3 6 7] 3 7 你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
输入格式
输入包含两行。
第一行包含两个整数 n 和 k,分别代表数组长度和滑动窗口的长度。
第二行有 n 个整数,代表数组的具体数值。
同行数据之间用空格隔开。
输出格式
输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。
输入样例:
8 3 1 3 -1 -3 5 3 6 7
输出样例:
-1 -3 -3 -3 3 3 3 3 5 5 6 7
思路(以求最小值为例)
由于输出的是滑动窗口内的最小值,若滑动窗口中当前的值小于左边的值,则该值左边的数不可能被输出,则移除。所以维护一个单调递增队列。
该队列存储原数组对应值的下标。在队列中,这些下标按照从小到大的顺序被存储,并且它们对应在数组a中的值是严格单调递增的。
当滑动窗口向右移动时,将新的元素放入队列中。
为了维持队列的性质,需要不断地将新的元素与队尾的元素相比较,如果新元素小于等于队尾元素,那么队尾的元素就可以被永久地移除,将其弹出队列。直到队列为空或者新的元素小于队尾的元素。此时将新的元素下标加入队列中。
由于队列中下标对应的元素值是严格单调递增的,因此此时队首下标对应的元素就是滑动窗口中的最小值。
窗口向右移动的时候,需要从队首弹出元素保证队列中的所有元素都是窗口中的,即队头元素在窗口的左边的时候,弹出队头。
#include <bits/stdc++.h>
using namespace std;
const int N = 1000010;
int n, k;
int a[N], q[N];
int main()
{
cin >> n >> k;
for (int i = 0; i < n; i ++) scanf("%d", &a[i]);
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++)
{
if (hh <= tt && q[hh] < i - k + 1) hh ++; //队头出队则移除
while (hh <= tt && a[q[tt]] >= a[i]) tt --; //队列为空或新的元素小于队尾的元素
q[++ tt] = i; //将新元素下标加入队列
if (i >= k - 1) printf("%d ", a[q[hh]]); //当窗口形成,输出队头对应的值(最小值)
}
puts("");
hh = 0, tt = -1; //求最大值同理
for (int i = 0; i < n; i ++)
{
if (hh <= tt && q[hh] < i - k + 1) hh ++;
while (hh <= tt && a[q[tt]] <= a[i]) tt --;
q[++ tt] = i;
if (i >= k - 1) printf("%d ", a[q[hh]]);
}
return 0;
}