如果您想了解如何理解Kademlia节点操作的时间复杂度和kafka节点数的知识,那么本篇文章将是您的不二之选。我们将深入剖析如何理解Kademlia节点操作的时间复杂度的各个方面,并为您解答kafk
如果您想了解如何理解Kademlia节点操作的时间复杂度和kafka节点数的知识,那么本篇文章将是您的不二之选。我们将深入剖析如何理解Kademlia节点操作的时间复杂度的各个方面,并为您解答kafka节点数的疑在这篇文章中,我们将为您介绍如何理解Kademlia节点操作的时间复杂度的相关知识,同时也会详细的解释kafka节点数的运用方法,并给出实际的案例分析,希望能帮助到您!
本文目录一览:- 如何理解Kademlia节点操作的时间复杂度(kafka节点数)
- 002-数据结构之算法的时间复杂度和空间复杂度
- 1-07Python列表与字典操作的时间复杂度
- C语言 超详细讲解算法的时间复杂度和空间复杂度
- C语言算法的时间复杂度和空间复杂度
如何理解Kademlia节点操作的时间复杂度(kafka节点数)
我现在正在通过阅读经典论文Kademlia:基于XOR度量的对等信息系统来学习Kademlia网络。我想了解其操作的复杂性,但仍然无法弄清楚。
在 3证明草图 部分中,本文给出了两个定义:
- 节点深度(h) :160 − i,其中i是非空桶的最小索引
- 节点y在节点x中的存储桶高度 :x将插入y的存储桶的索引减去x的 最低有效空存储桶 的索引。
还有三个结论:
- 对于具有n个节点的系统,以给定的概率,任何给定节点的高度都将在 log n 的常数之内。
- 在第k个最接近的节点中,最接近ID的节点的存储区高度可能在 log k 的常数之内。
- 如果该节点的h个 最重要的k个桶 中没有一个为空,则查找过程将在每个步骤中找到一个接近一半的节点(或者距离更短一点),从而以 h-log k个 步骤打开该节点。
所以我的问题是:
- 什么是 “最低有效空桶” 和 “最高有效k桶” ?
- 如何以视觉方式解释 深度 和 铲斗高度 ?
- 如何理解第二个和第三个结论,例如,为什么 log k 和 h-log k ?
答案1
小编典典自从我真正阅读本文以来已经有一段时间了,所以我主要是从我的实施经验中将它们拼凑在一起,而不是尝试将我头脑中的概念与本文的正式定义相匹配,因此请采用再加一点盐
什么是“最低有效空桶”和“最高有效K桶”?
这基本上是指按相对于节点自身ID的XOR距离排序的存储桶
如何以视觉方式解释深度和铲斗高度?
每个存储桶覆盖一个键空间区域。例如,对于键空间的1/16,从0x0000 简化为2个字节到0x0FFF。这可以用类似于CIDR的掩码0x0 /
4(4个前缀位)表示。那或多或少是一个桶的深度。
有几种组织路由表的方法。“正确”的方法是根据存储桶代表的最低ID将其表示为树或排序列表。这种方法允许进行任意的桶拆分操作,这是某些路由表优化所要求的,也可以用于实现节点多宿主。
简化的实现可以改为使用固定长度的数组,并将每个存储桶放置在相对于节点自己的ID的共享前缀位的位置。也就是说,数组中的位置0将具有0个共享的前缀位,这是最远的存储桶,该存储桶覆盖了键空间的50%,而存储桶中最高有效位是节点自身ID的反向MSB。
在那种情况下,深度就是数组的位置。
如何理解第二个和第三个结论,例如,为什么选择log k和h-log k?
假设您正在寻找距离您自己节点的ID最远的ID。这样,您将只有一个存储桶覆盖键空间的该部分,即它将覆盖键空间的一半,并且最高有效位与您的不同。因此,您从该存储桶中询问一个(或几个)节点。由于其ID位与您的查找目标具有相同的第一位,因此它们或多或少地保证将其分成两个或多个,即,至少具有目标空间的键空间覆盖率的两倍。因此,他们可以提供至少1位更好的信息。
当您查询更靠近目标的节点时,它们还将在目标区域附近具有更好的键空间覆盖范围,因为这也更接近其自己的节点ID。
冲洗,重复直到找不到更近的节点。
由于每个跃点一次至少可减少1位距离,因此您基本上需要一个O(log(n))跃点计数,其中n是网络规模。由于网络大小基本上决定了节点之间的平均距离,因此也决定了家用存储桶所需的存储桶深度。
如果目标密钥更接近您自己的ID,则您将需要较少的步骤,因为您将更好地覆盖密钥空间的该区域。
由于 k 是一个常数(每个桶的节点数),因此 log k
也是一个常数。将存储桶中的节点数增加一倍,它将具有给定键空间区域的两倍分辨率,因此(概率)将产生一个比k /
2大小的存储桶更接近目标一点的节点。也就是说,对于每个希望保存的跃点,每增加一个比特,就必须使每个存储桶的条目数增加一倍。
编辑:这是一个实际的单宿主bittorrent DHT路由表的可视化,按其前缀排序,即不相对于本地节点ID:
Node ID: 2A631C8E 7655EF7B C6E99D8A 3BF810E2 1428BFD4buckets: 23 / entries: 173000... entries:8 replacements:800100... entries:8 replacements:00010100... entries:8 replacements:20010101000... entries:8 replacements:400101010010... entries:8 replacements:7001010100110000... entries:8 replacements:30010101001100010... entries:8 replacements:300101010011000110000... entries:8 replacements:1001010100110001100010... entries:3 replacements:00010101001100011000110... entries:6 replacements:00010101001100011000111... entries:6 replacements:00010101001100011001... entries:8 replacements:2001010100110001101... entries:8 replacements:100101010011000111... entries:8 replacements:200101010011001... entries:7 replacements:00010101001101... entries:8 replacements:0001010100111... entries:8 replacements:0001010101... entries:8 replacements:100101011... entries:7 replacements:0001011... entries:8 replacements:00011... entries:8 replacements:801... entries:8 replacements:81... entries:8 replacements:8
002-数据结构之算法的时间复杂度和空间复杂度
一、概述
对于同一个问题来说,可以有多种解决问题的算法。尽管算法不是唯一的,但是对于问题本身来说相对好的算法还是存在的,这里可能有人会问区分好坏的标准是什么?这个要从「时效」和「存储」两方面来看。
好的算法应该具备时效高和存储低的特点。这里的「时效」是指时间效率,也就是算法的执行时间,对于同一个问题的多种不同解决算法,执行时间越短的算法效率越高,越长的效率越低;「存储」是指算法在执行的时候需要的存储空间,主要是指算法程序运行的时候所占用的内存空间。
1.1、不同写法影响的算法
实现:1+2+…+99+100
算法一、
int i, sum = 0, n = 100; // 执行1次
for( i=1; i <= n; i++ ) // 执行了n+1次
{
sum = sum + i; // 执行n次
}
算法二、
int sum = 0, n = 100; // 执行1次
sum = (1+n)*n/2; // 执行1次
第一种算法:执行了1+(n+1)+n=2n+2次。
第二种算法:是1+1=2次
如果我们把循环看做一个整体,忽略头尾判断的开销,那么这两个算法其实就是n和1的差距。
二、时间复杂度
2.1、算法效率的度量方法
2.1.1、事后统计方法
这种方法主要是通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序的运行时间进行比较,从而确定算法效率的高低。
2.1.2、事前分析估算方法【推荐】
在计算机程序编写前,依据统计方法对算法进行估算。
经过总结,我们发现一个高级语言编写的程序在计算机上运行时所消耗的时间取决于下列因素:
- 算法采用的策略,方案
- 编译产生的代码质量
- 问题的输入规模
- 机器执行指令的速度
2.2、大O表示法
「数量级」函数用来描述当规模 n 增加时,T(n) 函数中增长最快的部分,这个数量级函数我们一般用「大 O」表示,记做 O(f(n))。它提供了计算过程中实际步数的近似值,函数 f(n) 是原始函数 T(n) 中主导部分的简化表示。
在上面的求和函数的那个例子中,T(n) = n + 1,当 n 增大时,常数 1 对于最后的结果来说越来不越没存在感,如果我们需要 T(n) 的近似值的话,我们要做的就是把 1 给忽略掉,直接认为 T(n) 的运行时间就是 O(n)。这里你一定要搞明白,这里不是说 1 对 T(n) 不重要,而是当 n 增到很大时,丢掉 1 所得到的近似值同样很精确。
再举个例子,比如有一个算法的 T(n) = 2n^2+ 2n + 1000,当 n 为 10 或者 20 的时候,常数 1000 看起来对 T(n) 起着决定性的作用。但是当 n 为 1000 或者 10000 或者更大呢?n^2 起到了主要的作用。实际上,当 n 非常大时,后面两项对于最终的结果来说已经是无足轻重了。与上面求和函数的例子很相似,当 n 越来越大的时候,我们就可以忽略其它项,只关注用 2n^2 来代表 T(n) 的近似值。同样的是,系数 2 的作用也会随着 n 的增大,作用变得越来越小,从而也可以忽略。我们这时候就会说 T(n) 的数量级 f(n) = n^2,即 O(n^2)。
2.3、最好情况、最坏情况和平均情况
某个特定的数据集能让算法的执行情况极好,这就是最「最好情况」,而另一个不同的数据会让算法的执行情况变得极差,这就是「最坏情况」。不过在大多数情况下,算法的执行情况都介于这两种极端情况之间,也就是「平均情况」。因此一定要理解好不同情况之间的差别,不要被极端情况给带了节奏。
对于「最优情况」,没有什么大的价值,因为它没有提供什么有用信息,反应的只是最乐观最理想的情况,没有参考价值。「平均情况」是对算法的一个全面评价,因为它完整全面的反映了这个算法的性质,但从另一方面来说,这种衡量并没有什么保证,并不是每个运算都能在这种情况内完成。而对于「最坏情况」,它提供了一种保证,这个保证运行时间将不会再坏了,**所以一般我们所算的时间复杂度是最坏情况下的时间复杂度**,这和我们平时做事要考虑到最坏的情况是一个道理。
2.4、各种数量级函数
对比表格
序号 | 函数名 | f(n) | 阶 | 说明 | 举例 | 典型代码 |
1 | 常数函数 | 1 | O(1) | 普通语句 | 将两个数相加 大多数Java操作所需的时间为常数 |
a=b+c; |
2 | 对数函数 | 2log2n+2 | O(logn) | 二分策略 | 二分查找 运行时间和问题规模成对数关系的程序的经典例子就是二分查找。 对数的底数和增长的数量级无关 |
|
3 | 线性函数 | 2n+1 | O(n) | 循环 | 找出最大元素 例如单个for循环,增长数量级是线性的 它的运行时间和N成正比 |
|
4 | nlogn函数 | 2nlog2n+2n+2 | O(nlogn) | 分治 | 归并排序 |
|
5 | 二次函数 | 2n2+2n+2 | O(n2) | 双层循环 | 检查所有元素 双层for循环嵌套,例如Selection.sort() 和Insertion.sort() |
|
6 | 三元函数 | 2n3+2n+2 | O(n3) | 三层循环 | 检查所有三元组 三层for循环嵌套 |
|
7 | 指数函数 | 2n | O(2n) | 穷举查找 | 检查所有子集 |
对比图
在上图中,我们可以看到当 n 很小时,函数之间不易区分,但是当 n 增大时,能看到很明显的区别
O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n)<O(n!)
注:2=log2(4)=log3(9)=log5(25)=lg100
说明:log2(4) 其实是22=4以2为底4的对数是2 ,lg 是指以10为底即log10,ln是以自然对数为底即loge(XX),其中e是一个无限不循环小数,其值约等于2.71828182845
n!标识n的阶乘,n!=1*2*3…(n-2)*(n-1)*n,如0!=1,3!=1*2*3=6
Ο(1)表示基本语句的执行次数是一个常数,一般来说,只要算法中不存在循环语句,其时间复杂度就是Ο(1)。其中Ο(log2n)、Ο(n)、 Ο(nlog2n)、Ο(n2)和Ο(n3)称为多项式时间,而Ο(2n)和Ο(n!)称为指数时间。计算机科学家普遍认为前者(即多项式时间复杂度的算法)是有效算法,把这类问题称为P(Polynomial,多项式)类问题,而把后者(即指数时间复杂度的算法)称为NP(Non-Deterministic Polynomial, 非确定多项式)问题。
一般情况下,对一个问题(或一类算法)只需选择一种基本操作来讨论算法的时间复杂度即可,有时也需要同时考虑几种基本操作,甚至可以对不同的操作赋予不同的权值,以反映执行不同操作所需的相对时间,这种做法便于综合比较解决同一问题的两种完全不同的算法。
2.5、求解算法的时间复杂度
⑴ 找出算法中的基本语句;
算法中执行次数最多的那条语句就是基本语句,通常是最内层循环的循环体。
⑵ 计算基本语句的执行次数的数量级;
只需计算基本语句执行次数的数量级,这就意味着只要保证基本语句执行次数的函数中的最高次幂正确即可,可以忽略所有低次幂和最高次幂的系数。这样能够简化算法分析,并且使注意力集中在最重要的一点上:增长率。
⑶ 用大Ο记号表示算法的时间性能。
将基本语句执行次数的数量级放入大Ο记号中。
如果算法中包含嵌套的循环,则基本语句通常是最内层的循环体,如果算法中包含并列的循环,则将并列循环的时间复杂度相加。例如:
for (i=1; i<=n; i++)
x++;
for (i=1; i<=n; i++)
for (j=1; j<=n; j++)
x++;
第一个for循环的时间复杂度为Ο(n),第二个for循环的时间复杂度为Ο(n2),则整个算法的时间复杂度为Ο(n+n2)=Ο(n2)。
2.6、在计算算法时间复杂度时有以下几个简单的程序分析法则
(1).对于一些简单的输入输出语句或赋值语句,近似认为需要O(1)时间
(2).对于顺序结构,需要依次执行一系列语句所用的时间可采用大O下"求和法则"
求和法则:是指若算法的2个部分时间复杂度分别为 T1(n)=O(f(n))和 T2(n)=O(g(n)),则 T1(n)+T2(n)=O(max(f(n), g(n)))
特别地,若T1(m)=O(f(m)), T2(n)=O(g(n)),则 T1(m)+T2(n)=O(f(m) + g(n))
(3).对于选择结构,如if语句,它的主要时间耗费是在执行then字句或else字句所用的时间,需注意的是检验条件也需要O(1)时间
(4).对于循环结构,循环语句的运行时间主要体现在多次迭代中执行循环体以及检验循环条件的时间耗费,一般可用大O下"乘法法则"
乘法法则: 是指若算法的2个部分时间复杂度分别为 T1(n)=O(f(n))和 T2(n)=O(g(n)),则 T1*T2=O(f(n)*g(n))
(5).对于复杂的算法,可以将它分成几个容易估算的部分,然后利用求和法则和乘法法则技术整个算法的时间复杂度
另外还有以下2个运算法则:(1) 若g(n)=O(f(n)),则O(f(n))+ O(g(n))= O(f(n));(2) O(Cf(n)) = O(f(n)),其中C是一个正常数
2.7、常见复杂度示例
(1)、O(1)
Temp=i; i=j; j=temp;
以上三条单个语句的频度均为1,该程序段的执行时间是一个与问题规模n无关的常数。算法的时间复杂度为常数阶,记作T(n)=O(1)。注意:如果算法的执行时间不随着问题规模n的增加而增长,即使算法中有上千条语句,其执行时间也不过是一个较大的常数。此类算法的时间复杂度是O(1)。
(2)、O(n2)
a、 交换i和j的内容
sum=0; (一次)
for(i=1;i<=n;i++) (n+1次)
for(j=1;j<=n;j++) (n2次)
sum++; (n2次)
解:因为Θ(2n2+n+1)=n2(Θ即:去低阶项,去掉常数项,去掉高阶项的常参得到),所以T(n)= =O(n2);
b、
for (i=1;i<n;i++)
{
y=y+1; ①
for (j=0;j<=(2*n);j++)
x++; ②
}
解: 语句1的频度是n-1
语句2的频度是(n-1)*(2n+1)=2n2-n-1
f(n)=2n2-n-1+(n-1)=2n2-2;
又Θ(2n2-2)=n2
该程序的时间复杂度T(n)=O(n2).
一般情况下,对步进循环语句只需考虑循环体中语句的执行次数,忽略该语句中步长加1、终值判别、控制转移等成分,当有若干个循环语句时,算法的时间复杂度是由嵌套层数最多的循环语句中最内层语句的频度f(n)决定的。
(3)、O(n)
a=0;
b=1; ①
for (i=1;i<=n;i++) ②
{
s=a+b; ③
b=a; ④
a=s; ⑤
}
解: 语句1的频度:2,
语句2的频度: n,
语句3的频度: n-1,
语句4的频度:n-1,
语句5的频度:n-1,
T(n)=2+n+3(n-1)=4n-1=O(n).
(4)、O(log2n)
i=1; ①
hile (i<=n)
i=i*2; ②
解: 语句1的频度是1,
设语句2的频度是f(n), 则:2^f(n)<=n;f(n)<=log2n
取最大值f(n)=log2n,
T(n)=O(log2n )
(5)、O(n3)
for(i=0;i<n;i++)
{
for(j=0;j<i;j++)
{
for(k=0;k<j;k++)
x=x+2;
}
}
解:当i=m, j=k的时候,内层循环的次数为k当i=m时, j 可以取 0,1,...,m-1 , 所以这里最内循环共进行了0+1+...+m-1=(m-1)m/2次所以,i从0取到n, 则循环共进行了: 0+(1-1)*1/2+...+(n-1)n/2=n(n+1)(n-1)/6所以时间复杂度为O(n3).
2.8、 常用的算法的时间复杂度和空间复杂度
常用的排序算法的时间复杂度和空间复杂度
排序法 |
最差时间分析 |
平均时间复杂度 |
稳定度 |
空间复杂度 |
备注 |
说明 |
冒泡排序 |
O(n2) |
O(n2) |
稳定 |
O(1) |
n小时较好 |
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。
所以,如果两个元素相等,无操作;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,
这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
|
插入排序 |
O(n2) |
O(n2) |
稳定 |
O(1) |
大部分已经排序时较好 |
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。 比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面, 否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。 所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。 |
选择排序 |
O(n2) |
O(n2) |
不稳定 |
O(1) |
n小时较好 |
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推, 直到第n - 1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小, 而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。 举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了, 所以选择排序不是一个稳定的排序算法。 |
二叉树排序 |
O(n2) |
O(n*log2n) |
不一定 |
O(n) |
||
快速排序 |
O(n2) |
O(nlogn) |
不稳定 |
O(log2n)~O(n) |
n大时较好 | 快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数组下标, 一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] > a[center_index]。如果i和j都走不动了,i <= j, 交换a[i]和a[j],重复上面的过程,直到i > j。 交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候, 很有可能把前面的元素的稳定性打乱. 比如序列为5 3 3 4 3 8 9 10 11,现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱, 所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻。 |
堆排序 |
O(nlogn) |
O(nlogn) |
不稳定 |
O(1) |
n大时较好 | 我们知道堆的结构是节点i的孩子为2 * i和2 * i + 1节点,大顶堆要求父节点大于等于其2个子节点, 小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n / 2开始和其子节点共3个值选择最大(大顶堆) 或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n / 2 - 1, n / 2 - 2, ... 1这些个父节点选择元素时, 就会破坏稳定性。有可能第n / 2个父节点交换把后面一个元素交换过去了,而第n / 2 - 1个父节点把后面一个相同的元素没 有交换, 那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。 |
希尔排序 |
O(ns)1<s<2) |
O(nlogn) |
不稳定 |
O(1) |
s是选分组 | 希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快; 当元素基本有序了,步长很小, 插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比O(n^2)好一些。 由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中, 相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。 |
归并排序 |
O(n*log2n) |
O(n*log2n) |
稳定 |
O(1) |
n大时较好 | 归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换), 然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时, 1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定性。那么,在短的有序序列合并的过程中, 稳定是是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面, 这样就保证了稳定性。所以,归并排序也是稳定的排序算法。 |
基数排序 |
O(logRB) | O(logRB) | 稳定 |
O(n) | B 是真数(0-9) R是基数(个十百) |
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的, 先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。 基数排序基于分别排序,分别收集,所以其是稳定的排序算法。 |
查找算法时间复杂度
查找 |
平均时间复杂度 |
查找条件 |
算法描述 |
顺序查找 |
O(n) |
无序或有序队列 |
按顺序比较每个元素,直到找到关键字为止 |
二分查找(折半查找) |
O(logn) |
有序数组 |
查找过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜素过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。 如果在某一步骤数组为空,则代表找不到。 |
二叉排序树查找 |
O(logn) |
二叉排序树 |
在二叉查找树b中查找x的过程为: 1. 若b是空树,则搜索失败 2. 若x等于b的根节点的数据域之值,则查找成功; 3. 若x小于b的根节点的数据域之值,则搜索左子树 4. 查找右子树。 |
哈希表法(散列表) |
O(1) |
先创建哈希表(散列表) |
根据键值方式(Key value)进行查找,通过散列函数,定位数据元素。 |
分块查找 |
O(logn) |
无序或有序队列 |
将n个数据元素"按块有序"划分为m块(m ≤ n)。 每一块中的结点不必有序,但块与块之间必须"按块有序";即第1块中任一元素的关键字都必须小于第2块中任一元素的关键字;而第2块中任一元素又都必须小于第3块中的任一元素,……。然后使用二分查找及顺序查找。 |
是
三、空间复杂度
当今硬件的存储量级比较大,一般不会为了稍微减少一点儿空间复杂度而大动干戈,更多的是去想怎么优化算法的时间复杂度。所以我们在日常写代码的时候就衍生出了用「空间换时间」的做法,并且成为常态。比如我们在求解斐波那契数列数列的时候我们可以直接用公式去递归求,用哪个求哪个,同样也可以先把很多结果都算出来保存起来,然后用到哪个直接调用,这就是典型的用空间换时间的做法
举个例子说,要判断某年是不是闰年,你可能会花一点心思来写一个算法,每给一个年份,就可以通过这个算法计算得到是否闰年的结果。
另外一种方法是,事先建立一个有2050个元素的数组,然后把所有的年份按下标的数字对应,如果是闰年,则此数组元素的值是1,如果不是元素的值则为0。这样,所谓的判断某一年是否为闰年就变成了查找这个数组某一个元素的值的问题。
第一种方法相比起第二种来说很明显非常节省空间,但每一次查询都需要经过一系列的计算才能知道是否为闰年。第二种方法虽然需要在内存里存储2050个元素的数组,但是每次查询只需要一次索引判断即可。
1-07Python列表与字典操作的时间复杂度
list内置操作的时间复杂度
del slice
dict内置操作的时间复杂度
C语言 超详细讲解算法的时间复杂度和空间复杂度
1.前言
1.1 什么是数据结构?
数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。
1.2 什么是算法?
算法(Algorithm):就是定义良好的计算过程,他取一个或一组的值为输入,并产生出一个或一组值作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。
2.算法效率
2.1 如何衡量一个算法的好坏
如何衡量一个算法的好坏呢?比如对于以下斐波那契数列:
long long Fib(int N) { if(N < 3) return 1; return Fib(N-1) + Fib(N-2); }
斐波那契数列的递归实现方式非常简洁,但简洁一定好吗?那该如何衡量其好与坏呢?
2.2 算法的复杂度
算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计 算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度
2.3 复杂度在校招中的考察
3.时间复杂度
3.1 时间复杂度的概念
时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。
一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度
// 请计算一下Func1中++count语句总共执行了多少次? void Func1(int N) { int count = 0; for (int i = 0; i < N ; ++ i) { for (int j = 0; j < N ; ++ j) { ++count; } } for (int k = 0; k < 2 * N ; ++ k) { ++count; } int M = 10; while (M--) { ++count; } printf("%d\n", count); }
Func1 执行的基本操作次数 :F(N) = N^2+2*N+10
(‘^’在此章节表示为数乘)
- N = 10 F(N) = 130
- N = 100 F(N) = 10210
- N = 1000 F(N) = 1002010
实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。
3.2 大O的渐进表示法
大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
推导大O阶方法:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。使用大O的渐进表示法以后,Func1的时间复杂度为:O(N^2)
- N = 10 F(N) = 100
- N = 100 F(N) = 10000
- N = 1000 F(N) = 1000000
通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。
另外有些算法的时间复杂度存在最好、平均和最坏情况:
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
例如:在一个长度为N数组中搜索一个数据x
最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)
3.3 常见时间复杂度计算举例
实例1
// 计算Func2的时间复杂度? void Func2(int N) { int count = 0; for (int k = 0; k < 2 * N ; ++ k) { ++count; } int M = 10; while (M--) { ++count; } printf("%d\n", count); }
实例 2
// 计算Func3的时间复杂度? void Func3(int N, int M) { int count = 0; for (int k = 0; k < M; ++ k) { ++count; } for (int k = 0; k < N ; ++ k) { ++count; } printf("%d\n", count); }
实例 3
// 计算Func4的时间复杂度? void Func4(int N) { int count = 0; for (int k = 0; k < 100; ++ k) { ++count; } printf("%d\n", count); }
实例 4
// 计算strchr的时间复杂度? const char * strchr ( const char * str, int character );
实例 5
// 计算BubbleSort的时间复杂度? void BubbleSort(int* a, int n) { assert(a); for (size_t end = n; end > 0; --end) { int exchange = 0; for (size_t i = 1; i < end; ++i) { if (a[i-1] > a[i]) { Swap(&a[i-1], &a[i]); exchange = 1; } } if (exchange == 0) break; } }
实例 6
// 计算BinarySearch的时间复杂度? int BinarySearch(int* a, int n, int x) { assert(a); int begin = 0; int end = n-1; while (begin <= end) { int mid = begin + ((end-begin)>>1);//防溢出 if (a[mid] < x) begin = mid+1; else if (a[mid] > x) end = mid-1; else return mid; } return -1; }
实例 7
// 计算阶乘递归Fac的时间复杂度? long long Fac(size_t N) { if(0 == N) return 1; return Fac(N-1)*N; }
实例 8
// 计算斐波那契递归Fib的时间复杂度? long long Fib(size_t N) { if(N < 3) return 1; return Fib(N-1) + Fib(N-2); }
实例答案及分析:
实例1基本操作执行了2N+10次,通过推导大O阶方法知道,时间复杂度为 O(N)
实例2基本操作执行了M+N次,有两个未知数M和N,时间复杂度为 O(N+M)
实例3基本操作执行了10次,通过推导大O阶方法,时间复杂度为 O(1)
实例4基本操作执行最好1次,最坏N次,时间复杂度一般看最坏,时间复杂度为 O(N)
实例5基本操作执行最好N次,最坏执行了(N*(N+1)/2)次(N-1 + N-2 + N-3…+2+1),通过推导大O阶方法+时间复杂度一般看最坏,时间复杂度为 O(N^2)。
实例6基本操作执行最好1次(第一次就找到了),最坏O(logN)次(全找了一遍但没找到的情况),时间复杂度为 O(logN) ps:logN在算法分析中表示是底数为2,对数为N。有些地方会写成lgN。(建议通过折纸查找的方式讲解logN是怎么计算出来的)(假设找了x次才找完,则共有2^x个数据。反过来讲就是N个数据最多要找logN(底数为2)次)
实例7通过计算分析发现基本操作递归了N次,时间复杂度为O(N)。
实例8通过计算分析发现基本操作递归了2^N 次,时间复杂度为O(2N)。(1+2+4+8……+2(n-1) 再减一些次数(忽略不计))(建议画图递归栈帧的二叉树)
总结:我们想要分析算法的时间复杂度,一定要去看思想,,不能只去看程序是几层循环。 递归算法时间复杂度的计算:
1.每次函数调用是O(1),那么就看递归次数
2.每次函数调用不是O(1),就把每次的调用次数相加。
4.空间复杂度
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 。
空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
实例 1
// 计算BubbleSort的空间复杂度? void BubbleSort(int* a, int n) { assert(a); for (size_t end = n; end > 0; --end) { int exchange = 0; for (size_t i = 1; i < end; ++i) { if (a[i-1] > a[i]) { Swap(&a[i-1], &a[i]); exchange = 1; } } if (exchange == 0) break; } }
实例 2
// 计算Fibonacci的空间复杂度? // 返回斐波那契数列的前n项 long long* Fibonacci(size_t n) { if(n==0) return NULL; long long * fibArray = (long long *)malloc((n+1) * sizeof(long long)); fibArray[0] = 0; fibArray[1] = 1; for (int i = 2; i <= n ; ++i) { fibArray[i] = fibArray[i - 1] + fibArray [i - 2]; } return fibArray; }
实例 3
// 计算阶乘递归Fac的空间复杂度? long long Fac(size_t N) { if(N == 0) return 1; return Fac(N-1)*N; }
实例4
// 计算斐波那契递归Fib的空间复杂度? long long Fib(size_t N) { if(N < 3) return 1; return Fib(N-1) + Fib(N-2); }
实例答案及分析:
实例1使用了常数个额外空间,所以空间复杂度为 O(1)
实例2动态开辟了N个空间,空间复杂度为 O(N)
实例3递归调用了N次,开辟了N个栈帧,每个栈帧使用了常数个空间。空间复杂度为O(N) 4.先说答案空间复杂度为O(N) ,这是因为它与栈帧的开辟与销毁有关。栈帧销毁后再开辟还是用的同一块空间,它递归2^N次,开辟N个空间算出数值后销毁空间,然后再开辟,总共用了N块空间
总结: 时间一去不复返,是累积的 空间回收以后可以重复利用
5. 常见复杂度对比
一般算法常见的复杂度如下:
总结:这是数据结构(用c语言实现)的第一节课,内容还算简单,可以抓紧时间复习c教程中的指针和结构体等知识,之后的学习会更灵活的运用这些知识。大家加油!
到此这篇关于C语言 超详细讲解算法的时间复杂度和空间复杂度的文章就介绍到这了,更多相关C语言 时间复杂度内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!
- C语言数据结构时间复杂度及空间复杂度简要分析
- C语言三分钟精通时间复杂度与空间复杂度
- C语言数据结构与算法之时间空间复杂度入门
- C语言数据结构与算法时间空间复杂度基础实践
- C语言 详细解析时间复杂度与空间复杂度
- C语言数据结构通关时间复杂度和空间复杂度
- C语言算法的时间复杂度和空间复杂度
C语言算法的时间复杂度和空间复杂度
1.算法效率
1.1 如何衡量一个算法的好坏
如何衡量一个算法的好坏呢?比如对于以下斐波那契数列:
long long Fib(int N) { if (N < 3) return 1; return Fib(N - 1) + Fib(N - 2); }
斐波那契数列的递归实现方式非常简洁,但简洁一定好吗?那该如何衡量其好与坏呢?
1.2算法的复杂度
算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。
2.时间复杂度
2.1 时间复杂度的概念
时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。
下面举个例子:
请计算一下Func1中++count语句总共执行了多少次?
void Func1(int N) { int count = 0; for (int i = 0; i < N; ++i) { for (int j = 0; j < N; ++j) { ++count; } } for (int k = 0; k < 2 * N; ++k) { ++count; } int M = 10; while (M--) { ++count; } printf("%d\n", count); }
Func1 执行的基本操作次数 : F(N) = N^2 + 2*N + 10
代入数字计算一下:
N = 10 F(N) = 130
N = 100 F(N) = 10210
N = 1000 F(N) = 1002010
可以发现当N越来越大的时候,数字的大小主要取决于N^2了。
实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。
2.2 大O的渐进表示法
大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
推导大O阶方法:
- 1、用常数1取代运行时间中的所有加法常数。
- 2、在修改后的运行次数函数中,只保留最高阶项。
- 3、如果最高阶项存在且不是1,则去除与这个项相乘的常数。得到的结果就是大O阶。
使用大O的渐进表示法以后,Func1的时间复杂度为: O(N^2)
N = 10 F(N) = 100
N = 100 F(N) = 10000
N = 1000 F(N) = 1000000
通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。
另外有些算法的时间复杂度存在最好、平均和最坏情况:
- 最坏情况:任意输入规模的最大运行次数(上界)
- 平均情况:任意输入规模的期望运行次数
- 最好情况:任意输入规模的最小运行次数(下界)
例如:在一个长度为N数组中搜索一个数据x
- 最好情况:1次找到
- 最坏情况:N次找到
- 平均情况:N/2次找到
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)
2.3常见时间复杂度计算举例
实例1
计算Func2的时间复杂度:
void Func2(int N) { int count = 0; for (int k = 0; k < 2 * N; ++k) { ++count; } int M = 10; while (M--) { ++count; } printf("%d\n", count); }
基本操作执行了2*N + 10次,而通过推导大O阶方法
用常数1取代加法常数,得到2*N + 1
只保留最高阶项,得到2*N
将最高阶项的系数变为1,得到N
所以最后的时间复杂度是O(N)
实例2:计算Func3的时间复杂度
void Func3(int N, int M) { int count = 0; for (int k = 0; k < M; ++k) { ++count; } for (int k = 0; k < N; ++k) { ++count; } printf("%d\n", count); }
时间复杂度为O(N+M)
实例3:计算Func4的时间复杂度
void Func4(int N) { int count = 0; for (int k = 0; k < 100; ++k) { ++count; } printf("%d\n", count); }
用常数1替代100,时间复杂度是O(1)
实例4:计算strchr的时间复杂度
const char* strchr(const char* str, int character) { while (*str != character) { str++; } return str; }
最快执行了1次,最慢执行了N次,所以时间复杂度是O(N)
实例5:计算BubbleSort的时间复杂度
void BubbleSort(int* a, int n) { assert(a); for (size_t end = n; end > 0; --end) { int exchange = 0; for (size_t i = 1; i < end; ++i) { if (a[i - 1] > a[i]) { Swap(&a[i - 1], &a[i]); exchange = 1; } } if (exchange == 0) break; } }
第一趟冒泡排序了N - 1次,第二趟冒泡排序了N - 2次,依次类推,排序这个基本操作在最坏的情况下一共执行了(N*(N+1)/2次,而最好的情况下是数组已经排好了,此时只需要执行N次,时间复杂度取最坏的情况,所以是O(N^2)
实例6:计算BinarySearch的时间复杂度
int BinarySearch(int* a, int n, int x) { assert(a); int begin = 0; int end = n - 1; // [begin, end]:begin和end是左闭右闭区间,因此有=号 while (begin <= end) { int mid = begin + ((end - begin) >> 1); if (a[mid] < x) begin = mid + 1; else if (a[mid] > x) end = mid - 1; else return mid; } return -1; }
假如有序数组有N个数,那么查找一次就会将数组的范围缩小一半,直到最后只剩下一个数
可以这么用数字表示:
N / 2 / 2 / 2 / 2 / 2 / 2 ...... / 2 / 2 = 1
假设查找了x次,也就是每次将数组缩小一半(除以2)这个基本操作执行了x次,那么这个x与N之间的关系是2^x = N
那么x = logN,这里默认底数为2
所以时间复杂度是O(logN)
实例7:计算阶乘递归Fac的时间复杂度
long long Fac(size_t N) { if (0 == N) return 1; return Fac(N - 1) * N; }
基本操作递归了N次,所以时间复杂度为O(N)
实例8:计算斐波那契递归Fib的时间复杂度
long long Fib(size_t N) { if (N < 3) return 1; return Fib(N - 1) + Fib(N - 2); }
基本操作递归了约为2^N次,根据推到大O阶的方法,所以最后的时间复杂度为O(N)
3.空间复杂度
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度
空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
实例1:计算BubbleSort的空间复杂度
void BubbleSort(int* a, int n) { assert(a); for (size_t end = n; end > 0; --end) { int exchange = 0; for (size_t i = 1; i < end; ++i) { if (a[i - 1] > a[i]) { Swap(&a[i - 1], &a[i]); exchange = 1; } } if (exchange == 0) break; } }
可见,红框标注的地方,是在函数的内部额外创建了4个变量,也就是开辟了常数个额外空间,所以空间复杂度为O(1)
实例2:计算Fibonacci的空间复杂度
// 返回斐波那契数列的前n项 long long* Fibonacci(size_t n) { if (n == 0) return NULL; long long* fibArray = (long long*)malloc((n + 1) * sizeof(long long)); fibArray[0] = 0; fibArray[1] = 1; for (int i = 2; i <= n; ++i) { fibArray[i] = fibArray[i - 1] + fibArray[i - 2]; } return fibArray; }
在动态内存中开辟了N+1个sizeof(long long)大小的空间,所以空间复杂度为O(N)
实例3:计算阶乘递归Fac的空间复杂度
long long Fac(size_t N) { if (N == 0) return 1; return Fac(N - 1) * N; }
递归调用了N次,开辟了N个栈帧,每个栈帧使用了常数个空间,所以空间复杂度为O(N)
实例4:计算Fibonacci的空间复杂度
long long Fib(size_t N) { if (N < 3) return 1; return Fib(N - 1) + Fib(N - 2); }
每一次递归调用时,每两个子函数用的函数栈帧空间都是同一个,所以只额外开辟了N个栈帧,空间复杂度为O(N)
4.常见复杂度对比
5.复杂度的OJ练习
5.1消失的数字OJ
链接://img.jbzj.com/file_images/article/202207/202207081035315
题目描述:数组nums包含从0到n的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗?
示例 1:
输入:[3,0,1]
输出:2
示例 2:
输入:[9,6,4,2,3,5,7,0,1]
输出:8
这里给出时间复杂度都为O(N)的思路
- 1.创建一个大小为N + 1的数组,然后用-1将数组初始化,再将题目中给定数组中的数字放到新创建数组中对应下标的位置,最后将新数组中的数字遍历一遍,找出-1所对应的下标,该下标的数字就是所要找的消失的数字了。
- 2.将题目给定数组中的数字全都异或一次,再与从0到N+1的数字全部异或一次,就可以得到那个消失的数字了,其思路类似于在一个数组中寻找单身狗。
- 3.将题目给定数组进行快速排序,而后进行二分查找,找不到的那个数字即为要找的数字了。
- 4.将N+1个数字进行全都加起来,然后减去题目给定数组中的N个数字,最后得到的数字就是要找的消失的数字了。
3.2 旋转数组OJ
链接://img.jbzj.com/file_images/article/202207/202207081035316
题目描述:给你一个数组,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]
示例 2:
输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释:
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]
这里给出3种思路:
- 1.将最后一个数用一个临时变量保存起来,然后将数组中前面的数依次往后挪动,最后将临时变量中的数放到数组的第一个位置,这样的操作循环k次,最坏的情况下是k=N-1,这时时间复杂度是O(N^2),而空间复杂度是O(1),因为只开辟了1个临时变量,并且这个变量的空间是重复利用的。
- 2.额外开辟一个同样大小的数组,然后按照k的大小截取数据依次放入数组中,这种做法的时间复杂度为O(N),空间复杂度为O(N),这是以空间来换时间的做法。
- 3.根据k的大小将数组分位2个部分,第1个部分和第2个部分分别自旋,最后再将整个数组自旋一次,由于旋转交换的过程中只开辟了一个临时变量的空间,所以空间复杂度为O(1),时间复杂度为O(N)。
void reverse(int* nums, int left, int right) { while (left < right) { int tmp = nums[left]; nums[left] = nums[right]; nums[right] = tmp; ++left; --right; } } void rotate(int* nums, int numsSize, int k){ k %= numsSize; reverse(nums, 0, numsSize - 1 - k); reverse(nums, numsSize - k, numsSize - 1); reverse(nums, 0, numsSize - 1); }
到此这篇关于C语言算法的时间复杂度和空间复杂度的文章就介绍到这了,更多相关C语言时间复杂度内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!
- C语言数据结构时间复杂度及空间复杂度简要分析
- C语言三分钟精通时间复杂度与空间复杂度
- C语言数据结构与算法之时间空间复杂度入门
- C语言数据结构与算法时间空间复杂度基础实践
- C语言 超详细讲解算法的时间复杂度和空间复杂度
- C语言 详细解析时间复杂度与空间复杂度
- C语言数据结构通关时间复杂度和空间复杂度
关于如何理解Kademlia节点操作的时间复杂度和kafka节点数的介绍现已完结,谢谢您的耐心阅读,如果想了解更多关于002-数据结构之算法的时间复杂度和空间复杂度、1-07Python列表与字典操作的时间复杂度、C语言 超详细讲解算法的时间复杂度和空间复杂度、C语言算法的时间复杂度和空间复杂度的相关知识,请在本站寻找。
本文标签: