转载和修改于ACwing
问题
在这一章节中,我们经常看到y总在实现各种数据结构中经常用到idx这个变量,这个变量到底有什么用呢? 为什么链表,Trie树和堆会用到idx来维护这个数据结构,而栈和队列就不用idx来维护,而是用hh和tt来维护呢?
解答
可以看出不管是链表,Trie树还是堆,他们的基本单元都是一个个结点连接构成的,可以成为“链”式结构。这个结点包含两个基本的属性:本身的值和指向下一个结点的指针。按道理,应该按照结构体的方式来实现这些数据结构的,但是做算法题一般用数组模拟,主要是因为比较快。
那就有个问题,原来这两个属性都是以结构体的方式联系在一起的,现在如果用数组模拟,如何才能把这两个属性联系起来呢,如何区分各个结点呢?
这就需要用到idx
idx的操作总是idx++,这就保证了不同的idx值对应不同的结点,这样就可以利用idx把结构体内两个属性联系在一起了。因此,idx可以理解为结点。
链表:
链表中会使用到这几个数组来模拟:
// head存储链表头指向的节点,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点 h, e[N], ne[N], idx;
hh表示头结点指针,一开始初始化指向-1,每次插入x的操作idx++。利用idx联系结构体本身的值和next指针,因此e[idx]可以作为结点的值,ne[idx]可以作为next指针。同理可以理解双链表。
//单链表
void add_to_head (int x)
{
e[idx] = x;
ne[idx] = hh;
hh = idx ++ ;
}
//双链表
// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
void insert(int a, int x)
{
e[idx] = x;
l[idx] = a;
r[idx] = r[a];
l[r[a]] = idx;
r[a] = idx ++;
}
Trie树
Trie树中有个二维数组 son[N] [26],表示当前结点的儿子,如果没有的话,可以等于++idx。Trie树本质上是一棵多叉树,对于字母而言最多有26个子结点。所以这个数组包含了两条信息。比如:son[1] [0]=2表示1结点的一个值为a的子结点为结点2;如果son[1] [0] = 0,则意味着没有值为a的子结点。这里的son[N] [26]相当于链表中的ne[N]。
Trie数的N是字符串总长度最大值
首先Trie树主要是为了解决集合问题,所以集合中的字符不会重复。
int son[N][26], cnt[N], idx;
char str[N];
void insert(char str[]/* *str */)
{
int p = 0; //从根结点开始遍历
for (int i = 0; str[i]; i ++ )
{
int u =str[i] - 'a';//取字符型的str[i]与字符’a’在ASCII码上的差值
if (!son[p][u]) son[p][u] = ++ idx; //没有该子结点就创建一个,0表示没有该子节点,非0就是该子节点的地址,通过idx来表示各个节点的地址,就可以将所有节点串起来。
p = son[p][u]; //走到p的子结点
}
cnt[p] ++; // cnt相当于链表中的e[idx]
}
int query(char *str)
{
int p = 0;
for (int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
if (!son[p][u]) return 0;
p = son[p][u];
}
return cnt[p];
}
idx相当于一个分配器,如果需要加入新的结点就用++idx分配出一个下标
关于idx为什么没有初始化:
idx作为全局变量存放在静态数据区,该区的内存值在程序初始阶段会被设置为0,所以存放在该区的值默认情况下都是被初始化为0(静态数据区中的变量只允许初始化一次,即程序开始阶段的初始化)。程序会出现莫名其妙的错误,可能是储存在栈区的局部变量,该区并不会在程序初始阶段初始为0。我们在每次调用函数的时候会在栈区顶部推入新的栈帧,但是函数调用完成并回到调用点的时候,编译器只是把指向栈帧顶部的指针指回上一个栈帧的顶部,这个过程中并没有重新将被弹出的栈帧的内存的值改为0。所以如果下次又有新的栈帧被分配到该内存上并且没有初始化变量的值,那么就很可能会使用到上一次栈帧中遗留下来的内存值,也就是垃圾值。
其他理解:
int son[N][26], cnt[N], idx
就是一个邻接表
son[N] [26]就是分配的N个节点给trie使用,同时每个节点有26个状态,每次只有一个状态是成立的,而表头就是son[0] [26]每个状态对应一个开头
而insert操作就是一个尾插法,有头节点的链表尾插就要遍历到最后一个元素。这个for循环就是在做这个操作
每次查询就是一次邻接表的遍历过程,如果路径不通就返回0
测试
#include <iostream>
using namespace std;
const int N = 100010;
int son[N][26], cnt[N], idx;
char str[N];
/*
用例:
2
abc
acd
*/
void insert(char * str)
{
int p = 0;
for(int i = 0; str[i]; i ++){
int u = str[i] - 'a';
if(!son[p][u]) son[p][u] = ++ idx;
p = son[p][u];
}
cnt[p] ++;
cout << "idx容器的值:"<< idx << endl;
cout << "p:" << p << endl;
cout << "cnt: " << cnt[p] << endl;
}
int main()
{
int n;
cin >> n;
while(n --){
cin >> str;
insert(str);
}
for(int i = 0; i <= idx; i ++)
{
for(int j = 0; j < 26; j ++){
cout << son[i][j] << ' ';
}
cout << endl;
}
system("pause");
return 0;
}
当Trie树中只有一个树枝abc:idx初始值为0(son[0] [0]=1表示1结点的一个值为a的子结点为结点1)
son[0] [0] = 1 = ++idx 存的值为1(a)
son[1] [1] = 2 存的值为2(b)
son[2] [2] = 3 存的值为3(c)
如果按树枝现在的形状插入abcde,相当于延续了树枝:
if(!son[p][u]) son[p][u] = ++ idx;
这个在son[2] [2]后执行,因为之前都有子节点,所以idx的三个值已经存好了,到son[3] [3]就要开始新的子节点的创建了,所以要++idx,作为容器存放d这个字符的的值(也就是现在用到的是那一个节点),p表示二维坐标的y轴,u表示二维坐标的x轴。
堆
堆中的每次插入都是在堆尾,但是堆中经常有up和down操作。所以结点与结点的关系并不是用一个ne[idx] [2]可以很好地维护的。但是好在堆是个完全二叉树。子父节点的关系可以通过下标来联系(左儿子2n,右儿子2n+1)。就数组模拟来说,知道数组的下标就知道结点在堆中的位置。所以核心就在于即使有down和up操作也能维护堆数组的下标(k)和结点(idx)的映射关系。 比如说:h[k] = x, h数组存的是结点的值,按理来说应该h[idx]来存,但是结点位置总是在变的,因此维护k和idx的映射关系就好啦,比如说用ph数组来表示ph[idx] = k, 那么结点值为h[ph[idx]], 儿子为ph[idx] * 2和ph[idx] * 2 + 1, 这样值和儿子结点不就可以通过idx联系在一起了吗?
if (op == "I")
{
scanf("%d", &x);
size ++ ;
idx ++ ;
ph[idx] = size, hp[size] = idx;//每次插入都是在堆尾插入
h[size] = x;//h[k], k是堆数组的下标,h存储的是结点的值,也就是链表中的e[idx]
up(size);
}
由于idx只有在插入的时候才会更新为idx ++,自然idx也表示第idx插入的元素