首页 > 其他分享 >数据结构

数据结构

时间:2023-10-13 18:26:32浏览次数:33  
标签:结点 下标 idx int 哈希 数据结构 sf

目录

二、数据结构

2.1 链表

  • 单链表最常用的是邻接表($n$个单链表),主要用于存储图和树
  • 双链表用于优化某些问题

2.1.1 单链表

竞赛或笔试时往往不用结构体+指针的方式实现链表,而是用数组模拟,这样时间效率更高。

单链表在数组中的存储:

对于每个结点,$\rm e[i]$存储其值,$\rm ne[i]$存储其$\rm next$指针,表示其指向的下一个结点,空节点下标用$-1$表示

const int N = 1e5 + 10;
int e[N], ne[N], head, idx;//head是头结点的next指针,表示头结点指向的下一个结点(就是首元结点!)的下标。idx表示当前结点的下标(理解为指向待插入位置的指针)。

//初始化
void init()
{
    head = -1;//表示链表中没有元素,头结点指向空
    idx = 0;//表示当前可利用第0号结点
}

//头插法
void add_to_head(int x) 
{
    e[idx] = x;//将x的值存储到当前结点
    ne[idx] = head;//让当前结点指向原来head的下一个结点(即首元结点)
    head = idx;//让头指针指向该结点
    idx ++;//idx已经被用过了(已经被编号了),∴待插入的位置变为idx+1.
}

//将x插入到下标为k的结点后面
void add(int x, int k)
{
    e[idx] = x;
    ne[idx] = ne[k];
    ne[k] = idx;
    idx ++;
}

//将下标是k的点后面的那个点删掉
void remove(int k) //注意remove是系统函数,需要注意改变一下名称
{
    ne[k] = ne[ne[k]];//让下标是k的点直接指向后面的第2个点(这里不用++k是因为k未必连续,ne[k]的下一个数未必是ne[k+1].)
}

for (int i = head; i != -1; i = ne[i]) cout << e[i] << " ";//遍历方式

例题:单链表

Leetcode中常用的链表操作:

① 合并两个链表:

void mergeList(ListNode* l1, ListNode* l2) {
        ListNode* l1_tmp;
        ListNode* l2_tmp;
        while (l1 != nullptr && l2 != nullptr) {
            l1_tmp = l1->next;
            l2_tmp = l2->next;

            l1->next = l2;
            l1 = l1_tmp;

            l2->next = l1;
            l2 = l2_tmp;
        }
    }

② 反转链表:

ListNode* reverseList(ListNode* head) {
        ListNode* prev = nullptr;
        ListNode* curr = head;
        while (curr != nullptr) {
            ListNode* nextTemp = curr->next;
            curr->next = prev;
            prev = curr;
            curr = nextTemp;
        }
        return prev;
    }

③ 寻找链表中间结点:(注:快慢指针在链表题中很重要!

ListNode* middleNode(ListNode* head) {
        ListNode* slow = head;
        ListNode* fast = head;
        while (fast->next != nullptr && fast->next->next != nullptr) {
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }

2.1.2 双链表

const int N = 1e5 + 10;
int e[N], l[N], r[N], idx;//e[N],idx含义都与单链表中相同,l[i],r[i]表示当前结点前一个点的下标和后一个点的下标。

//初始化:我们认为0是左端点,1是右端点,有且仅有这两个结点。(当做哨兵,不存储元素的值!)
void init()
{
    r[0] = 1, l[1] = 0;
    idx = 2;//0和1都被占用,从2开始可用
}

//在下标为k的结点右边插入x
void insert(int k, int x)
{
    e[idx] = x;
    r[idx] = r[k];
    l[idx] = k;
    l[r[k]] = idx;
    r[k] = idx;//注意这几步的顺序!  
    idx ++;
}

//删除下标为k的点
void delet(int k)
{
    r[l[k]] = r[k];
    l[r[k]] = l[k];
}

如要在下标为$k$的结点左边插入$x,$则可调用$\rm insert(l[k], x).$

注意不是$\rm insert(k-1,x),$因为下标未必连续,且中间可能进行了其他操作,$k-1$也未必就在$k$的左边。

例题:双链表


2.2 栈

int stk[N], tt = 0;//tt表示栈顶

stk[ ++ tt] = x;// 向栈顶插入一个数

tt -- ;// 从栈顶弹出一个数

stk[tt];// 求栈顶的值

if (tt > 0)// 判断栈是否为空,如果 tt > 0,则表示不为空

栈的经典应用:表达式求值 (模板题,建议背一下)

后缀表达式求值

补充:后缀表达式求值的方法

① 从左到右读入后缀表达式,若读到的是操作
数,将它压入堆栈。

② 若读到的是运算符,就从堆栈中连续弹出两
个元素(操作数),进行相应的运算,并将
结果压入栈中。

③ 读入结束符时,栈顶元素就是计算结果。


2.3 队列

int q[N], hh = 0, tt = -1; //hh表示队头,tt表示队尾

q[++ tt] = x;//向队尾加入一个元素

hh++;//从队头弹出一个数

q[hh];//取出队头的值
q[tt];//取出队尾的值

if (hh <= tt) //队列不为空

2.4 单调栈

主要用于四种情况(实际情况下还可能更多,如“寻找一个数右边最接近的小于等于它的数”

$1.$ 寻找一个数左边最接近的小于它的数 例题:AcWing 830. 单调栈

while (n--)
{
    int x;
    cin >> x;
    while (tt && stk[tt] >= x) tt--;//这里是≥,与要求的“小于”相反!
    if (tt) cout << stk[tt] << " ";//栈顶元素就是左侧第一个比它小的元素
    else cout << "-1 ";//如果栈空,说明没有比其小的值
    stk[++tt] = x;
}

$2.$ 寻找一个数左边最接近的小于它的数的下标 (注意下标是从0开始还是从1开始!从而判断数组开头是0还是1

只需对栈做一点小小的修改就能应对情形二。注意到之前我们寻找的是元素所以让栈去保存元素,现在我们寻找下标,所以让栈去保存元素的下标就可以了。

for (int i = 0; i < n; i++) cin >> a[i];

    for (int i = 0; i < n; i++) {
        while (!stk.empty() && a[stk.top()] >= a[i]) stk.pop();  // 仅有两处修改
        if (!stk.empty()) ans[i] = stk.top();
        else ans[i] = -1;
        stk.push(i);  // 仅有两处修改
    }

    for (int i = 0; i < n; i++) cout << ans[i] << ' ';

$3.$ 寻找一个数右边最接近的大于它的数

之前我们是在一个数的左边去寻找,所以让栈去保存这个数左边的所有数,类似地,现在需要让栈去保存这个数右边的所有数。

考虑将数组翻转(实际上不可能翻转,而是倒序遍历),因此情形三变成了「寻找一个数左边第一个大于它的数」,于是归结为情形一。

for (int i = 0; i < n; i++) cin >> a[i];

    for (int i = n - 1; i; i--) {
        while (!stk.empty() && stk.top() <= a[i]) stk.pop();//符号为≤,与要求的“大于”相反!
        if (!stk.empty()) ans[i] = stk.top();
        else ans[i] = -1;
        stk.push(a[i]);
    }

    for (int i = 0; i < n; i++) cout << ans[i] << ' ';

$4.$ 寻找一个数右边最接近的大于它的数的下标(注意下标是从0开始还是从1开始!从而判断数组开头是0还是1

for (int i = 0; i < n; i++) cin >> a[i];

    for (int i = n - 1; i; i--) {
        while (!stk.empty() && a[stk.top()] <= a[i]) stk.pop();
        if (!stk.empty()) ans[i] = stk.top();
        else ans[i] = -1;
        stk.push(i);
    }

    for (int i = 0; i < n; i++) cout << ans[i] << ' ';

看上去有两重循环,其实每一个元素最多进栈一次出栈一次,所以复杂度仍是线性的。

本质上单调栈维护的是数列所有前缀的后缀最值。(一个下标 $i$ 是当前 $a$ 的后缀最大值下标当且仅当:对于所有的$ i < j \leq |a|,$都有 $a_i > a_j,$其中 $|a|$表示当前$a$的元素个数。)可以通过调整比较条件里的大于号、小于号、大于等于号、小于等于号来用这一数据结构维护所有前缀的后缀最大、最小、非严格最大、非严格最小值。


2.5 单调队列

常见模型:用于求滑动窗口中的最大值/最小值(用双端队列$\rm deque$来实现) 例题:滑动窗口


2.6 KMP算法

例题:KMP字符串


2.7 Trie树

高效地存储和查找字符串集合的数据结构

  • $\rm son[N][26]$表示当前节点的儿子(一维表示节点总数,二维表示节点与节点之间的关系(谁是谁儿子))(本题全是小写字母,所以数组第二维最多是26);$\rm cnt[]$表示以当前结点结尾的串有多少个;$\rm idx$表示当前用到了哪个下标(相当于下标分配器,给每个节点一个唯一编号)

例:比如$[0][1]=3, [0]$表示根节点,$[1]$表示它有一个儿子$'b’,$这个儿子的下标是$3,$接着如果有一个$[3][2]=8 ; $说明根节点的儿子$'b’$也有一个儿子$’c’,$这个孙子的下标就是$8,$下标由$\rm idx$分配。这样传递下去,就是一个字符串。

  • 下标是0的点,既是根节点,又是空节点(一个结点没有子节点则使其指向0)
  • $\rm Trie$树本质上是多叉树,$\rm son[N][26]$就是分配的N个节点给$\rm trie$使用,同时每个节点有$26$个状态,每次只有一个状态是成立的
const int N = 1e5 + 10;

int son[N][26], cnt[N], idx;

//插入操作
void insert(char str[])
{
    int p = 0;//从根节点开始遍历
    for (int i = 0; str[i]; i++) {
        int u = str[i] - '0';//把当前字母映射到子节点的编号
        if (!son[p][u]) son[p][u] = ++ idx;//没有子节点就创建一个
        p = son[p][u];//走到p的子节点
    }
    cnt[p] ++;//以该点为末尾的单词数量++.
}


//查询操作
void query(char str[]) 
{
    int p = 0;
    for (int i = 0; str[i]; i++) {
        int u = str[i] - '0';
        if (!son[p][u]) return 0;
        p = son[p][u];
    }   
    return cnt[p];
}

例题1


2.8 并查集

1.将两个集合合并
2.询问两个元素是否在一个集合当中

基本原理:每个集合用一棵树来表示,树根的编号就是整个集合的编号。每个结点存储它的父结点,$p[x]$表示$x$的父结点。

$\rm question1:$
如何判断树根:if (p[x] == x) (认为祖宗节点的父节点是自己本身)

$\rm question2:$
如何求x的集合编号:while (p[x] != x) x = p[x];

$\rm question3:$
如何合并两个集合:px是x的集合编号,py是y的集合编号,p[px] = py.

优化:路径压缩

对于结点x,当其迭代找到它的根结点(集合编号)时,便将其整条路径上的结点全都直接指向根结点。做完路径压缩优化之后,时间复杂度可以达到几乎$O(1)$.

其实还有另一个优化称为按秩合并,但由于其对于性能的优化并不特别明显且代码量需增加几乎一倍,所以实际上用的很少.

例题1、合并集合

例题2、连通块中点的数量


2.9 手写堆

操作:(前三个操作可以用$\sf STL$中的堆来实现)

(这里以小根堆为例)

  1. 插入一个数:heap[++size] = x; up(size); (因为是在最后一个位置插入x,所以是up操作向上调整)
  2. 求集合当中的最小值:heap[1]; ($O(1)$)
  3. 删除最小值:
heap[1] = heap[size];
size--;
down(1);//向下调整

小根堆第一个元素为最小值,但是数组的第一个元素不好删,故先把heap[1]赋成最后一个元素的值,再删除最后那个元素即可。

  1. 删除任意一个元素:
heap[k] = heap[size];
size--;
down(k);//
up(k); //这两条语句只会执行一条,因为heap[k]只有在变大的时候才会执行down(k),变小时才执行up(k)
  1. 修改任意一个元素:
heap[k] = x;
down(k);
up(k);

堆是一棵完全二叉树

以小根堆为例,其每个结点都小于等于其左右儿子的值,因此根结点即是最小值。

堆状数据结构(完全二叉树)的存储: 一维数组
根结点下标为$1$,$x$的左儿子下标为$2x$,右儿子下标为$2x+1$. (如果根结点下标从0开始,则0的左儿子下标为2*0仍为0,不太方便)

核心操作:up(), down()(某个元素变大——向下调整;某个元素变小——向上调整)时间复杂度:$O(log_n)$ (与树的高度成正比)

void down(int u) {
    int t = u;
    if (u * 2 <= mySize && h[u * 2] < h[t]) t = 2 * u;//左儿子存在且比根结点小
    if (u * 2 + 1 <= mySize && h[u * 2 + 1] < h[t]) t = 2 * u + 1;//右儿子存在且比根结点小
    if (u != t) {//如果根结点不是最小的,就交换根结点和儿子中的最小值
        swap(h[u], h[t]);
        down(t);//递归处理
    }
}
//这个递归的含义是为一个相对‘大’的数向下找到其在树中合适的位置

void up(int u) {
    while (u / 2 && h[u / 2] > h[u]) {//父节点存在且比子节点大,则交换
        swap(h[u / 2], h[u]);
        u /= 2;
    }
}
//down要和两个子节点比较,而up只用和父节点比较,可以迭代到祖宗节点,故用循环进行迭代即可。

应用:堆排序(把一组数放入堆中,每次取堆顶元素)

:将一组数建堆,可以通过insert的方式,但时间复杂度是$O(nlog_n)$(insert一次是$O(log_n)$,n次就是$O(nlog_n)$。for (int i = n / 2; i >= 1; i--) down(i);这样的复杂度是$O(n)$的(可由数学证明得出)

从$\dfrac{n}{2}$开始down的原因:要进行down操作时必须满足左儿子和右儿子已经是个堆。开始创建堆的时候,元素是随机插入的,所以不能从根节点开始down,而是要找到满足下面三个性质的结点:

1.左右儿子满足堆的性质。

2.下标最大(①因为要向上遍历,这样才能逐一满足每个结点的左右儿子都满足堆;②使down操作不会向下过深,节约时间)

3.不是叶结点(叶结点一定满足堆的性质)

换言之,n是最大值,而$\dfrac{n}{2}$是n的父节点,所以$\dfrac{n}{2}$是最大的有子节点的父节点(树的倒数第二层),从$\dfrac{n}{2}$向前遍历就可将heap数组遍历一遍。

例题:模拟堆


2.10 哈希

$$
哈希表:
\begin{cases}
存储方式(解决冲突的方式):\begin{cases}
开放寻址法\
拉链法\
\end{cases}\
字符串哈希
\end{cases}
$$

【补充】离散化是一种特殊的哈希方式

基本操作:插入、查找(极少会删除,即使要删也是在对应元素上用bool值做一下标记)

Hash函数一般将比较复杂(较大)的定义域(如$-109$~$109$)映射为较好维护的值域(如 $0$~$10^5$)。由于值域变简单、范围变小,有可能造成两个不同的原始信息被Hash函数映射为相同的值($k_i ≠k_j$时可能有$h(k_i)=h(k_j)$),我们将这种情况称为哈希冲突。我们将具有不同关键字但具有相同哈希地址的元素称为「同义词」,这种冲突也称为同义词冲突。

当Hash函数设计较好时,原始信息会比较均匀地分配到各个表头之后,从而使每次查找、统计的时间降低到“原始信息总数除以表头数组长度”。若原始信息总数和表头数组长度均是$O(N)$级别且Hash函数分布均匀,几乎不产生冲突,那么每次查找、统计的时间复杂度期望为$O(1)$. 哈希算法是一种期望算法.

2.10.1 整数哈希

当key为整数时,h(key)称为整数哈希。

设哈希表要存储的元素个数为n,一般寻找大于等于n且最接近n的质数(设为m),然后开一个长度为m的数组。

事实上,若质数m能够离2的幂尽可能的远,则冲突概率可以进一步降低,这里我们可以根据 $n$ 的数量级来给出一个简化版的表格:

$n$ $m$
$10^3$ $1543$
$10^4$ $12289$
$10^5$ $196613$
$10^6$ $1572869$

注意到k可能是负数,所以我们需要将哈希函数修改成:$h(k)=(k \bmod m+m)\bmod m$来确保$h(k) ≥0$。(若直接$(k+m)\bmod m$则不行,因为若$k$取$-109$,$m$为$105$量级,则$k+m$仍是负数)

2.10.1.1 拉链法

拉链法是把所有的同义词用单链表链接起来的方法。在这种方法中,哈希表的每个单元存储的不再是元素本身,而是相应同义词单链表的头指针(注意是头指针而不是头节点)。

对于单链表,我们可以采用数组的方式进行实现。此外,使用拉链法时,$m$ 的大小通常和 $n$ 差不多。例如,若 $n \leq 10^5$ ,我们可以寻找大于等于 $10^5$的第一个质数,即 $m = 100003$。

2.10.1.2 开放寻址法

开放寻址法就是在插入一个关键字为 $k$ 的元素时,若发生哈希冲突,则通过某种哈希冲突解决函数(也称为再哈希)得到一个新空闲地址再插入该元素的方法。通常使用线性探测法。(一维数组从前向后遍历)

在开放寻址法中,$m$通常取$n$的$2$~$3$倍左右,防止数据冲突(如$n \leq 10^5$则$m$可取$196613$)

如果当成蹲坑:
开放寻址法:如果看到一个坑里有人就往前找没人的坑
拉链法:认准一个坑无论有没有人都在厕所门口排队

例题:模拟散列表
对于代码中一些特殊数据的解释

2.10.2 字符串哈希(解决字符串类型题目的利器!)

通常用于快速判断两个字符串是否相等!时间复杂度为$O(1)$

采用字符串前缀哈希法,通过记录字符串前缀的哈希值来计算出某一段字符串的哈希值。
将一个字符串看成是一个p进制数,如$\rm ABCD$看成$\rm (1234)_p$,即$\rm 1×p3+2×p2+3×p1+4×p0$。由于字符串可能会有很多位,因此计算后的结果还需$\rm \bmod Q$,可将字符串映射为一个0~Q-1的整数。

注:
①一般情况下,不能将某一个字母映射成$0$:若不然,则“A”, “AA”等的hash值均为$0$,便会造成冲突。
②经验值:取$\rm p = 131$或$13331$,$\rm Q = 2^{64}$,这样冲突的概率较低。
③哈希值$h[]$直接用$\rm unsigned$ $\rm long$ $\rm long$存储,自然溢出就相当于取模$2^{64}$.

子串$ str[l$~$ r]$的计算:先将$str[0$~$ l-1]$与$str[0$~$r]$的高位对齐$(×p^{l-r+1})$,再用$h[r]$减去即可。

(手动模拟一下即可,设字符串“AB”,从左到右为高位到低位1到0,而“ABCDE”从左到右为4到0,现欲求CDE的哈希值,只需将“AB”的1、0转化为“ABCDE”中的“AB”(4、3)。)

一般KMP除了求循环节,其他的都可以用字符串哈希来做hhh

例题:字符串哈希


2.11 STL补充 (其他部分见《算法竞赛进阶指南》)

1、$\sf pair<>$支持比较运算,排序时以$\sf first$为第一关键字,$\sf second$为第二关键字(按字典序)

pair<int, string> p;
p = make_pair(10, "yxc");//构造pair
p = {20, "abc"};//在C++ 11中可以这么写

有时候也会有$\sf pair<int, pair<int, int>>$的写法

2、$\sf string$支持$\sf size()/length()$、$\sf empty()$、$\sf clear()$操作

string a = "abc";
a += "de";
a += 'f';//可以加字符串也可以直接加字符
printf("%s", a.c_str());//若要用printf的方式输出字符串,则应输出该字符串对应字符数组的起始地址。

$\sf substr()$的用法:($\sf string$ $\sf s$ $=$ “$0123456789$”)
①$\sf string$ $\sf sub1 = s.substr(5);$ //只有一个数字5表示从下标为5开始一直到结尾:sub1 = “56789”;
②$\sf string$ $\sf sub2 = s.substr(5, 3);$//从下标为5开始截取长度为3位:sub2 = "567";

3、$\sf stack$:只支持$\sf size()$、$\sf empty()$,无$\sf clear()$
$\sf push()$:向栈顶插入一个元素
$\sf pop()$:弹出栈顶元素
$\sf top()$:返回栈顶元素

4、$\sf set$,$\sf map$,$\sf multiset$,$\sf multimap$,基于平衡二叉树(红黑树),动态维护有序序列【均有$\sf lower_bound$和$\sf upper_bound$,但增删改查的时间复杂度是$O(log_n)$。】
$\sf unordered_set$,$\sf unordered_map$,$\sf unordered_multiset$,$\sf unordered_multimap$,基于Hash实现【与上面类似,但增删改查的时间复杂度是$O(1)$且不支持$\sf lower_bound$和$\sf upper_bound$以及迭代器的$ ++/–-$,因为内部无序。】

标签:结点,下标,idx,int,哈希,数据结构,sf
From: https://www.cnblogs.com/pangyou/p/17762856.html

相关文章

  • 数据结构——左偏树/可并堆学习笔记
    引入作为树形数据结构的一员——堆,对于取极值拥有着优秀的复杂度,但是,合并两个堆却成为了一个问题。除了朴素算法外,还有什么算法可以合并两个堆呢?正文那么,可并堆是个啥呢?简单来说,它是一个支持合并操作的二叉堆(好像是废话)。首先,简单介绍一下二叉堆的性质,学过的读者可自行跳过。......
  • 数据结构之队列(循环队列)
    循环队列又称为环形队列,有如下4个特点:在循环队列的定义中规定了两个索引指针:front和rear。front指向第一个有效元素的位置,而rear可以理解为用来记录队尾元素的下一个位置。当队列为空时,front==rear;当队列满时,(rear+1)%n=front.这样可以让队列看起来像一个环状......
  • php教程:变量、数据类型、运算符、数据结构、条件判断、循环、函数和面向对象
    变量<?php$x=5;$y=6;$z=$x+$y;echo$z;?>变量作用域全局变量在所有函数外部定义的变量,拥有全局作用域。要在一个函数中访问一个全局变量,需要使用global关键字。<?php$x=5;$y=10;functionmyTest(){global$x,$y;$y=$x+$y;}myTest();echo$y;//输出1......
  • 《流畅的Python》 读书笔记 第二章数据结构(2) 231011
    2.5对序列使用+和*通常+号两侧的序列由相同类型的数据所构成,在拼接的过程中,两个被操作的序列都不会被修改,Python会新建一个包含同样类型数据的序列来作为拼接的结果+和*都遵循这个规律,不修改原有的操作对象,而是构建一个全新的序列l1=[1,2,3]l2=[4,5,6]print(id(l......
  • 基础数据结构
    链表#链节点classNode:def__init__(self,item=0,next=None):self.item=itemself.next=next#链表classLinkedList:def__init__(self):self.head=Nonedefcreate(self,data):self.head=Node(data[0])......
  • 十天学完基础数据结构-第二天(数据结构简介)
    什么是数据结构?在计算机科学中,数据结构是一种组织和存储数据的方式。它定义了数据的布局,以及对这些数据执行的操作。你可以把数据结构看作是计算机内存中的特定组织方式,就像图书馆中书籍的排列一样。数据结构可以是各种形式,包括数组、链表、栈、队列、树、图等等。每种数据结构都有......
  • Redis——底层和数据结构
    数据结构简单动态字符串SDS可以认为在Redis中所有的东西最终都是字符串。Redis是C语言实现的,但是Redis没有直接使用C语言中的字符串,C语言字符串是字符数组实现的,存在很多问题:1、获取字符串的长度需要运算,时间复杂度达到O(n)。2、非二进制安全,无法保存\0字符(被识别成结束标识)......
  • 数据结构的关键码序列的理解概述
    1、关键码序列的理解所谓关键码序列,就是出现在二叉排序树中的,对二叉排序树的各个结点进行排序的一个结点序列。依据左子树的各个结点的值都小于父结点的值,右子树的各个结点的值都大于父结点的值的条件进行排序。2、习题解决一般都是给我们一个二叉排序树的图,让我们去判断选......
  • 05_数据结构与算法
    Sort排序算法sort包中实现了四种基本排序算法:插入排序、归并排序、堆排序、快速排序。但是它们不公开,只供sort包内部自己使用,所以在需要实现数据排序时不必考虑使用哪一种排序方法,只要实现了sort.Interface定义的三个方法:获取数据集合长度Len()、比较两个元素大小Less()、交......
  • 页帧的数据结构设计
    前言页帧page是物理内存管理的基本单位,structpage记录了任意时刻page的所有状态,因此每一个物理页帧都需一个对应的structpage结构体记录状态,对于内存多计算机系统来说需要的structpage本身就需要大量内存进行存储,因此该结构体中每增加一个变量带来的代价会很大,需要仔细控制该......