`
webcenterol
  • 浏览: 950943 次
文章分类
社区版块
存档分类
最新评论

各种基本算法实现小结(五)—— 排序算法

 
阅读更多

各种基本算法实现小结(五)—— 排序算法

(均已测试通过)

* 选择排序 |____简单选择排序 |____堆排序 |____归并排序

* 交换排序 |____冒泡排序 |____快速排序

* 插入排序 |____直接插入排序 |____折半排序 |____希尔排序

* 分配排序 |____箱排序 |____基数排序

======================================================================

简单排序算法

1、 冒泡排序

测试环境:VC 6.0 (C)

运行结果:

=======================================================

2、 双向冒泡排序

据说可以提高效率,减少比较次数和交换次数

但仔细分析可得,每次while循环,都for循环比较了两次

因此每次low和high各减1,总体上比较次数并未减少,两次for交换也未减少

个人认为双向冒泡算法并未有效的减少比较次数和交换次数,但此算法也富含编程思想,值得学习

测试环境:VC 6.0 (C)

运行结果:

=======================================================

3、选择排序

测试环境:VC 6.0 (C)

运行结果:

=======================================================

中级排序算法

向部分已排好序数列插入新值,使整个数列最终都有序

1、直接插入排序

测试环境:VC 6.0 (C)

运行结果:

=======================================================

2、 折半插入排序

折半插入排序算法是一种稳定的排序算法,比直接插入算法明显减少了关键字之间比较的次数

因此速度比直接插入 排序算法快,但记录移动的次数没有变,

所以折半插入排序算法的时间复杂度仍然为O(n^2),


测 试环境:VC 6.0 (C)

运行结果:

=======================================================

3、 2-路插入排序

2-路插入排序: 是在折半插入排序的基础上再次改进,其目的是减少排序过程中记录移动的次数

测试环境:VC 6.0 (C)

运行结果:

=======================================================

4、合并插入排序(数组实现)

将两个有序数组A、B合并成另一个有序的大数组C

测试环境:VC 6.0 (C)

运行结果:

=======================================================

5、 合并插入排序(链表实现)

将两个有序链表A、B合并成另一个有序的大链表C(链表单元来自A和B)

测试环 境:VC 6.0 (C)

运行结果:

=======================================================

高级排序算法

1、 快速排序

测试环境:VC 6.0 (C)

运行结果:

=======================================================

2、 希尔排序

说明:本示例仅测试10或11个数的3趟希尔排序

由于希尔排序的增量step至今尚无精准的数学论证,无法给出科学、高效的序列函数

据严蔚敏的《数据结构(C语言版)》介绍说:希尔排序的分析是一个复杂的问题,因为它的时间是所取“增量”序列的函数,这涉及一些数学上尚未解决的难题(P272).

因此,本示例仅是实际问题实际解决的一个特例。

本算法基本思想仍是上述直接排序算法的改进,仅仅步长由1变成了step而已

如果大家想需要增添或减少数组元素个数,请一并修改input()函数中的step等趟数序列

如果大家对希尔排序算法有更好的改进,或有较好步长的函数和通用模板,希望能拿出来共同学习交流分享,谢谢!

测试环境:VC 6.0 (C)

运行结果:

=======================================================

2、 希尔排序(网摘)

在学习希尔排序算法时,看到网上有如下一段希尔排序代码,也可以直接运行

但看代码来真得很费解,感觉变量定义不够直观,算法设计也不太简洁

因此,在最大程度保留源代码时,仅对变量名和算法逻辑简单修改

力争做到变量名清晰,逻辑顺畅,达到不用注释读者也能看明白

希望对大家学习有点帮助

测试环境:VC 6.0 (C)

摘录原代码:http://apps.hi.baidu.com/share/detail/5669244(百度空间)

修改后代码:

运行结果:

=======================================================

3、 堆排序

测试环境:VC 6.0 (C)

运行结果:

=======================================================

4、 归并排序

测试环境:VC 6.0 (C)

运行结果:

=======================================================

排序算法的知识扩展(网摘)

内部排序算法的比较和实现
引用网址: http://www.cppblog.com/feosun/archive/2008/10/12/63831.html(直接摘录,尚未测试)

参考网址: http://sjjp.tjuci.edu.cn/sjjg/DataStructure/DS/web/paixu/paixu8.1.1.1.htm(数据结构)


排序是数据处理中经常使用的一种重要运算,在计算机及其应用系统中,花费在排序上的时间在系统运行时间中占有很大比重;并且排序本身对推动算法分析的发展 也起很大作用。目前已有上百种排序方法,但尚未有一个最理想的尽如人意的方法,本文介绍常用的如下排序方法的C/C++实现,并对它们进行分析和比较。

选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法,而冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法。

首先,排序算法的稳定性大家应该都知道,通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化 一下,如果Ai = Aj, Ai原来在位置前,排序后Ai还是要在Aj位置前。

其次,说一下稳定性的好处。排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就 是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排序算法稳定,对基于比较的排序算法而言,元素交换 的次数可能会少一些(个人感觉,没有证实)。

回到主题,现在分析一下常见的排序算法的稳定性,每个都给出简单的理由。

(1)冒泡排序
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无 聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改 变,所以冒泡排序是一种稳定排序算法。

(2)选择排序
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个 元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么 交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。

(3)插入排序
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开 始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相 等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳 定的。

(4)快速排序
快速排序有两个方向,左边的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] 交换的时刻。

(5)归并排序
归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有 序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定 性。那么,在短的有序序列合并的过程中,稳定是是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结 果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。

(6)基数排序
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优 先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。

(7)希尔排序(shell)
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小, 插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元 素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。

(8)堆排序
我们知道堆的结构是节点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个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。

/*
冒泡排序 插入排序 二路插入排序 希尔排序 快速排序 选择排序 归并排序 堆排序算法的
C/C++实现。

作者:feosun

日期:2008年10月12日

参考资料:数据结构(C语言版) 清华大学出版社

*/

#include <iostream>
using namespace std;


//交换两个数的值
void swap(int &a,int &b)
{
int tmp;
tmp=a;
a=b;
b=tmp;
}

//屏幕输出数组
void display(int array[],int len)
{
cout<<"the result is:"<<endl;
for (int i = 0 ;i < len;i++ )
{
cout<<array[i]<<" ";
}
cout<<endl;
}

/*
冒泡排序

算法思想:将被排序的记录数组R[1..n]垂直排列,每个记录R[i]看作是重量为R[i].key的气泡。
根据轻气泡不能在重气泡之下的原则,从下往上扫描数组 R:凡扫描到违反本原则的
轻气泡,就使其向上"飘浮"。如此反复进行,直到最后任何两个气泡都是轻者在上,
重者在下为止。

时间复杂度 o(n^2)

空间复杂度 o(1)

比较次数 n(n+1)/2
*/
void bubble_sort(int array[],int len)
{

for (int i = len-1 ;i >= 0;i-- )
{
for(int j = 0;j < i;j++)
if(array[j] > array[j+1])
swap(array[j],array[j+1]);
}
}

/*
直接插入排序

算法思想:把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元
素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,将它
插入到有序表中的适当位置,使之成为新的有序表,重复n-1次可完成排序过程。

时间复杂度 o(n^2)

空间复杂度 o(1)

比较次数 n(n+1)/2
*/
void insert_sort(int array[],int len)
{
int tmp,i,j;

for(i = 1;i < len;i++)
{
if (array[i] < array[i-1])
{
tmp = array[i];
array[i] = array[i-1];

//插入到相应位置
for (j = i-2;j >= 0;j--)
{
//往后移
if (array[j] > tmp )
array[j+1] = array[j];
else
{
array[j+1] = tmp;
break;
}
}
if(j == -1)
array[j+1] = tmp;
}
}
}

/*
2-路插入排序

算法思想:增加一个辅助空间d,把r[1]赋值给d[1],并将d[1]看成是排好序后处于中间
位置的记录。然后从r[2]开始依次插入到d[1]之前或之后的有序序列中。

时间复杂度 o(n^2)

空间复杂度 o(1)

比较次数 n(n+1)/2
*/

void bi_insert_sort(int array[],int len)
{
int* arr_d = (int *)malloc(sizeof(int) * len);

arr_d[0] = array[0];
int head = 0,tail = 0;
for (int i = 1;i < len; i++ )
{
if (array[i] > arr_d[0])
{
int j;
for ( j= tail;j>0;j--)
{
if (array[i] < arr_d[j])
arr_d[j+1] = arr_d[j];
else
break;
}
arr_d[j+1] = array[i];
tail += 1;
}

else
{
if (head ==0)
{
arr_d[len-1] = array[i];
head =len-1;
}
else
{
int j;
for (j = head;j <= len-1;j++)
{
if (array[i] > arr_d[j])
arr_d[j-1] = arr_d[j];
else
break;
}
arr_d[j-1] = array[i];
head -= 1;
}
}

}

for (int i = 0;i < len; i++)
{
int pos = (i + head );
if(pos >= len) pos -= len;
array[i] = arr_d[pos];
}

free(arr_d);
}

/*
希尔排序

算法思想:先将整个待排序记录分割成若干子序列分别进行直接插入排
序,待整个序列中的记录基本有序时,再对全体记录进行一
次直接插入排序

时间复杂度 o(n^2)

空间复杂度 o(1)

比较次数 ?
*/

void shell_insert(int array[],int d,int len)
{
int tmp,j;

for (int i = d;i < len;i++)
{
if(array[i] < array[i-d])
{
tmp = array[i];
j = i - d;
do
{
array[j+d] = array[j];
j = j - d;
} while (j >= 0 && tmp < array[j]);

array[j+d] = tmp;
}
}
}
void shell_sort(int array[],int len)
{
int inc = len;

do
{
inc = inc/2;
shell_insert(array,inc,len);
} while (inc > 1);
}

/*
快速排序

算法思想:将原问题分解为若干个规模更小但结构与原问题相似的子问题。递
归地解这些子问题,然后将这些子问题的解组合成为原问题的解。

时间复杂度 o(nlogn)

空间复杂度 o(logn)

比较次数 ?
*/

int partition(int array[],int low,int high)
{
int pivotkey = array[low];

while (low < high)
{
while(low < high && array[high] >= pivotkey)
--high;
swap(array[low],array[high]);

while(low < high && array[low] <= pivotkey)
++low;
swap(array[low],array[high]);
}

array[low] = pivotkey;

return low;
}

void quick_sort(int array[],int low,int high)
{
if (low < high)
{
int pivotloc = partition(array,low,high);
quick_sort(array,low,pivotloc-1);
quick_sort(array,pivotloc+1,high);
}
}

/*
直接选择排序

算法思想:每一趟在n-i+1个记录中选取关键字最小的记录作为有序序列中的第i个记录

时间复杂度 o(n^2)

空间复杂度 o(1) ?

比较次数 n(n+1)/2
*/

int SelectMinKey(int array[],int iPos,int len)
{
int ret = 0;

for (int i = iPos; i < len; i++)
{
if (array[ret] > array[i])
{
ret = i;
}
}
return ret;
}

void select_sort(int array[],int len)
{
for (int i = 0; i < len; i++)
{
int j = SelectMinKey(array,i,len);
if (i != j)
{
swap(array[i],array[j]);
}
}
}

/*
归并排序

算法思想:设两个有序的子文件(相当于输入堆)放在同一向量中相邻的位置上:R[low..m],R[m+1..high],先
将它们合并到一个局部的暂存向量R1(相当于输出堆)中,待合并完成后将R1复制回R[low..high]中。

时间复杂度 o(nlogn)

空间复杂度 o(n)

比较次数 ?
*/

void merge(int array[],int i,int m, int n)
{
int j,k;

int iStart = i, iEnd = n;

int arrayDest[256];

for ( j = m + 1,k = i; i <= m && j <= n; ++k)
{
if (array[i] < array[j])
arrayDest[k] = array[i++];
else
arrayDest[k] = array[j++];
}

if (i <= m)
for (;k <= n; k++,i++)
arrayDest[k] = array[i];
if(j <= n)
for (;k <= n; k++,j++)
arrayDest[k] = array[j];

for(j = iStart; j <= iEnd; j++)
array[j] = arrayDest[j];
}

void merge_sort(int array[],int s,int t)
{
int m;

if (s < t)
{
m = (s + t )/2;
merge_sort(array,s,m);
merge_sort(array,m+1,t);
merge(array,s,m,t);
}
}

/*
堆排序

算法思想:堆排序(Heap Sort)是指利用堆(heaps)这种数据结构来构造的一种排序算法。
堆是一个近似完全二叉树结构,并同时满足堆属性:即子节点的键值或索引总是
小于(或者大于)它的父节点。
时间复杂度 o(nlogn)
空间复杂度 o(1)
比较次数 较多
*/

void heap_adjust(int array[],int i,int len)
{
int rc = array[i];

for(int j = 2 * i; j <len; j *= 2)
{
if(j < len && array[j] < array[j+1]) j++;
if(rc >= array[j]) break;

array[i] = array[j]; i = j;
}
array[i] = rc;
}

void heap_sort(int array[],int len)
{
int i;
for(i = (len-1)/2; i >= 0; i--)
heap_adjust(array,i,len);
for( i = (len-1); i > 0; i--)
{
swap(array[0],array[i]); //弹出最大值,重新对i-1个元素建堆
heap_adjust(array,0,i-1);
}
}

int main() {

int array[] = {45,56,76,234,1,34,23,2,3,55,88,100};

int len = sizeof(array)/sizeof(int);

//bubble_sort(array,len); //冒泡排序

/*insert_sort(array,len);*/ //插入排序

/*bi_insert_sort(array,len);*/ //二路插入排序

/*shell_sort(array,len);*/ //希尔排序

/*quick_sort(array,0,len-1);*/ //快速排序

/*select_sort(array,len);*/ //选择排序

/*merge_sort(array,0,len-1);*/ //归并排序

heap_sort(array,len); //堆排序
display(array,len);

return 0;
}

分享到:
评论

相关推荐

    数据结构——排序查找等实验

    例如,实验一可能是介绍排序的基本概念,实验二可能是实现简单的排序算法,如冒泡排序,然后随着实验编号的增加,可能会涉及二叉树查找、优化排序算法等更高级的主题。 这些实验将帮助学习者通过实践深入理解数据...

    各种经典排序算法小结---必知必会

    ### 各种经典排序算法小结---必知必会 #### 概述 排序算法是计算机科学中的一个重要组成部分,主要用于将一系列数据按照特定顺序(升序或降序)进行排列。排序算法的学习对于理解算法复杂度、算法设计原理以及提高...

    排序算法——选择排序.docx

    选择排序是一种简单直观的排序算法,它的工作原理可以分为以下几个步骤: 1. **理解选择排序**:选择排序从数组的第一个元素开始,遍历数组寻找当前未排序部分的最小(或最大)元素。找到后,将这个最小(或最大)...

    常用排序算法小结(附Java实现)

    这篇博客“常用排序算法小结(附Java实现)”提供了一种深入理解并掌握常见排序算法的途径,尤其对于Java开发者来说非常实用。文章可能涵盖了如冒泡排序、选择排序、插入排序、快速排序、归并排序、堆排序等多种经典...

    程序员实用算法——源码

     5.7 小结:选择一种排序算法  5.8 资源和参考资料 第6章 树  6.1 二叉树  6.1.1 树查找  6.1.2 节点插入  6.1.3 节点删除  6.1.4 二叉查找树的性能  6.1.5 AVL树  6.2 红黑树  6.3 伸展树  ...

    山东大学数据结构课程设计第一部分代码——外排序

    在“山东大学数据结构课程设计第一部分代码——外排序”这个项目中,学生将学习并实现这种高级排序算法。 外排序主要涉及以下几个核心知识点: 1. **大文件处理**:当数据量超过内存容量时,不能一次性将所有数据...

    C/C++常用算法手册.秦姣华(有详细书签).rar

    4.5.2 Shell排序算法示例 111 4.6 快速排序法 113 4.6.1 快速排序算法 113 4.6.2 快速排序算法示例 114 4.7 堆排序法 116 4.7.1 堆排序算法 116 4.7.2 堆排序算法示例 121 4.8 合并排序法 123 4.8.1 合并...

    数据结构与算法分析

     7.3 一些简单排序算法的下界   7.4 谢尔排序   7.5 堆排序   7.6 归并排序   7.7 快速排序   7.7.1 选取枢纽元   7.7.2 分割策略   7.7.3 小数组   7.7.4 实际的快速排序例程  ...

    严蔚敏 数据结构(C语言版) 代码 23490 书中算法

    8.1.4 排序算法效率的评价指标 209 8.2 插入排序 209 8.2.1 直接插入排序 209 8.2.2 折半插入排序 211 8.2.3 希尔排序 212 8.3 交换排序 214 8.3.1 冒泡排序 215 8.3.2 快速排序 216 8.4 选择排序...

    数据结构与算法分析Java语言描述(第二版)

    堆6.6 左式堆6.6.1 左式堆性质6.6.2 左式堆操作6.7 斜堆6.8 二项队列6.8.1 二项队列结构6.8.2 二项队列操作6.8.3 二项队列的实现6.9 标准库中的优先队列小结练习参考文献第7章 排序7.1 预备知识7.2 插入排序7.2.1...

    数据结构与算法分析_Java语言描述(第2版)]

    堆6.6 左式堆6.6.1 左式堆性质6.6.2 左式堆操作6.7 斜堆6.8 二项队列6.8.1 二项队列结构6.8.2 二项队列操作6.8.3 二项队列的实现6.9 标准库中的优先队列小结练习参考文献第7章 排序7.1 预备知识7.2 插入排序7.2.1 ...

    数据结构与算法分析C描述第三版

     7.3 一些简单排序算法的下界   7.4 谢尔排序   7.5 堆排序   7.6 归并排序   7.7 快速排序   7.7.1 选取枢纽元   7.7.2 分割策略   7.7.3 小数组   7.7.4 实际的快速排序例程   7.7.5...

    数据结构与算法分析 Java语言描述第2版

    堆6.6 左式堆6.6.1 左式堆性质6.6.2 左式堆操作6.7 斜堆6.8 二项队列6.8.1 二项队列结构6.8.2 二项队列操作6.8.3 二项队列的实现6.9 标准库中的优先队列小结练习参考文献第7章 排序7.1 预备知识7.2 插入排序7.2.1 ...

    数据结构与算法分析-Java语言描述(第2版)_2_2

    小结 练习 参考文献第6章 优先队列(堆) 6.1 模型 6.2 一些简单的实现 6.3 二叉堆 6.3.1 结构性质 6.3.2 堆序性质 6.3.3 基本的堆操作 6.3.4 其他的堆操作 6.4 优先队列的应用 6.4.1 选择问题...

    数据结构与算法分析-Java语言描述(第2版)_1_2

    小结 练习 参考文献第6章 优先队列(堆) 6.1 模型 6.2 一些简单的实现 6.3 二叉堆 6.3.1 结构性质 6.3.2 堆序性质 6.3.3 基本的堆操作 6.3.4 其他的堆操作 6.4 优先队列的应用 6.4.1 选择问题...

    谭浩强C语言程序设计,C++程序设计,严蔚敏数据结构,高一凡数据结构算法分析与实现.rar )

    10.8 有关指针的数据类型和指针运算的小结 167 10.8.1 有关指针的数据类型的小结 167 10.8.2 指针运算的小结 167 10.8.3 void 指针类型 168 11 结构体与共用体 11.1 定义一个结构的一般形式 170 11.2 结构类型变量的...

    谭浩强C语言程序设计,C++程序设计,严蔚敏数据结构,高一凡数据结构算法分析与实现.rar

    10.8 有关指针的数据类型和指针运算的小结 167 10.8.1 有关指针的数据类型的小结 167 10.8.2 指针运算的小结 167 10.8.3 void 指针类型 168 11 结构体与共用体 11.1 定义一个结构的一般形式 170 11.2 结构类型变量的...

    数据结构与算法

    - **小结**:总结数据结构的基本原理。 - **算法及性能分析**: - **算法**:阐述算法的基本定义、特征和分类。 - **时间复杂性**:分析算法的时间复杂度,包括大O表示法、Ω表示法和Θ表示法。 - **空间复杂性...

Global site tag (gtag.js) - Google Analytics