数据结构 排序

目录

  • 第八章 排序
  • 8.1排序的基本概念
    • 1. 概念
    • 2. 排序算法的分类
  • 8.2 插入排序
    • 8.2.1 直接插入排序
    • 8.2.2 算法效率分析
    • 8.2.2 折半插入排序
    • 总结
    • 8.2.3 希尔排序
  • 8.3 交换排序
    • 8.3.1冒泡排序
    • 8.3.2快速排序(了解栈的过程)
  • 8.4 选择排序
    • 8.4.1 简单选择排序
    • 8.4.2 堆排序
    • 8.4.3 堆的插入删除
  • 8.5 归并排序(Merge Sort)内外部排序
  • 8.6 基数排序(Radix Sort)
      • 排序方式
      • 递减
      • 递增
      • 算法效率分析
      • 基数排序的应用
  • 各种内部排序算法的比较及应用
  • 内部排序算法的应用
  • 8.7 外部排序
  • 8.7.1 归并排序的实现
    • 内存、外存之间的数据交换
    • 外部排序原理
    • 时间开销分析
    • 优化
      • 多路归并
      • 减少初始归并段数量
    • 什么是多路平衡归并?
  • 8.7.2 败者树
    • 败者树的构造
    • 败者树的使用
    • 败者树在多路平衡归并中的应用
    • 败者树的实现思路
  • 8.7.3 置换-选择排序
  • 8.7.4 最佳归并树
    • 归并树的性质
    • 构造2路归并的最佳归并树
    • 多路归并的情况
    • 添加虚段的数量
  • 数据结构结束

第八章 排序

8.1排序的基本概念

1. 概念

在这里插入图片描述

  • 排序:重新排列表中的元素,使表中元素满足按关键字有序的过程(关键字可以相同)
  • 排序算法的评价指标:时间复杂度、空间复杂度;
  • 排序算法的稳定性:关键字相同的元素在排序之后相对位置不变,称为稳定的;(选择题考查)

Q: 稳定的排序算法一定比不稳定的好?
A: 不一定,要看实际需求;

2. 排序算法的分类

在这里插入图片描述

  • 内部排序: 数据都在内存——关注如何使时间、空间复杂度更低;
  • 外部排序: 数据太多,无法全部放入内存——关注如何使时间、空间复杂度更低,如何使读/写磁盘次数更少;

之所以有这种分类是因为磁盘的容量一般远大于内存,而运算速度又远不如内存。因此排序算法不仅要关注时间和空间复杂度,有时考虑到内存容量的问题还要关注如何使读/写磁盘次数更少

8.2 插入排序

8.2.1 直接插入排序

  • 算法思想: 每次将一个待排序的记录按其关键字大小,插入(依次对比、移动)到前面已经排好序的子序列中,直到全部记录插入完成

  • 代码实现:
    在这里插入图片描述

不带“哨兵”

void InsertSort(int A[], int n){    //A中共n个数据元素int i,j,temp;for(i=1; i<n; i++)if(A[i]<A[i-1]){    //A[i]关键字小于前驱temp = A[i];  for(j=i-1; j>=0 && A[j]>temp; --j)A[j+1] = A[j];     //所有大于temp的元素都向后挪A[j+1] = temp;         //复制到插入位置}
}

带“哨兵” ,优点:不用每轮循环都判断j>=0
在这里插入图片描述

void InsertSort(int A[], int n){    //A中从1开始存储,0放哨兵int i,j;for(i=1; i<n; i++)if(A[i]<A[i-1]){    A[0] = A[i];     //复制为哨兵for(j=i-1; A[0] < A[j]; --j)  //从后往前查找待插入位置A[j+1] = A[j];     //向后挪动A[j+1] = A[0];          //复制到插入位置}
}

8.2.2 算法效率分析

  • 空间复杂度:O(1)

  • 时间复杂度:主要来自于对比关键字、移动关键字,若有n个元素,则需要n-1次处理

  • 最好情况: 原本为有序,共n-1趟处理,每一趟都只需要对比1次关键字,不需要移动元素,共对比n-1次 —— O(n)

  • 最差情况: 原本为逆序 —— O(n²)

  • 平均情况:O(n²)

  • 算法稳定性:稳定

8.2.2 折半插入排序

思路: 先用折半查找找到应该插入的位置,再移动元素;

  • 为了保证稳定性,当查找到和插入元素关键字一样的元素时,应该继续在这个元素的右半部分继续查找以确认位置; 即当 A[mid] == A[0] 时,应继续在mid所指位置右边寻找插入位置

  • 当low>high时,折半查找停止,应将[low,i-1]or[high+1,i-1]内的元素全部右移,并将A[0]复制到low所指的位置

代码实现

void InsertSort(int A[], int n){ int i,j,low,high,mid;for(i=2;i<=n;i++){A[0] = A[i];                    //将A[i]暂存到A[0]low = 1; high = i-1;            //折半查找的范围while(low<=high){               //折半查找mid = (low + high)/2;       //取中间点if(A[mid]>A[0])             //查找左半子表high = mid - 1;else                        //查找右半子表low = mid + 1;}for(j=i-1; j>high+1;--j)       //统一后移元素,空出插入位置A[j+1] = A[j];A[high+1] = A[0]}
}
  • 与直接插入排序相比,比较关键字的次数减少了,但是移动元素的次数没有变,时间复杂度仍然是O(n²)

  • 对链表进行插入排序
    移动元素的次数变少了,因为只需要修改指针,不需要依次右移
    但是关键字对比的次数依然是O(n²)数量级,因此整体看来时间复杂度仍然是O(n²)

总结

在这里插入图片描述

8.2.3 希尔排序

在这里插入图片描述
在这里插入图片描述

思路: 先追求表中元素的部分有序,再逐渐逼近全局有序;

更适用于基本有序的排序表和数据量不大的排序表,仅适用于线性表为顺序存储的情况

代码实现

void ShellSort(ElemType A[], int n){//A[0]为暂存单元for(dk=n/2; dk>=1; dk=dk/2)   //步长递减(看题目要求,一般是1/2for(i=dk+1; i<=n; ++i)if(A[i]<A[i-dk]){A[0]=A[i];for(j=i-dk; j>0&&A[0]<A[j];j-=dk)A[j+dk]=A[j];         //记录后移,查找插入的位置A[j+dk]=A[0;]             //插入}
}i++会切换着处理每个子表

算法效率分析

  • 空间效率:空间复杂度=O(1)
  • 时间效率: 最坏情况下时间复杂度=O(n²)
  • 稳定性:希尔排序是一种不稳定的排序方法
  • 在这里插入图片描述

8.3 交换排序

基于“交换”的排序根据序列中两个元素关键字的比较结果来对换这两个记录再序列中的位置;

8.3.1冒泡排序

在这里插入图片描述

第一趟排序使关键字值最小的一个元素“冒”到最前面(其最终位置)—— 每趟冒泡的结果是把序列中最小元素放到序列的最终位置,这样最多做n-1趟冒泡就能把所有元素排好序;

为保证稳定性,关键字相同的元素不交换;
在这里插入图片描述

代码实现

//交换
void swap(int &a,int&b){int temp=a;a=b;b=temp;
}//冒泡排序
void BubbleSort(int A[],int n){for(int i=0;i<n-1;i++){            bool flag=false;               //表示本趟冒泡是否发生交换的标志for(int j=n-1;j>i;j--)         //一趟冒泡过程if(A[j-1]>A[j]){           //若为逆序swap(A[j-1],A[j]);     //交换flag=true;}if(flag==false)return;                    //本趟遍历后没有发生交换,说明表已经有序}
}

算法效率分析

  • 空间复杂度:O(1)
  • 时间复杂度
  • 最好情况 (有序) :只需要一趟排序,比较次数=n-1,交换次数=0,最好时间复杂度=O(n)
  • 最坏情况 (逆序) :比较次数 = (n-1)+(n-2)+…+1 = n(n-1)/2 = 交换次数,最坏时间复杂度 = O(n²),平均时间复杂度 = O(n²)
  • 交换次数和移动次数不是一个概念在这里插入图片描述在这里插入图片描述
    冒泡排序可以用于链表、顺序表

8.3.2快速排序(了解栈的过程)

每一趟排序都可使一个中间元素确定其最终位置
用一个元素(不一定是第一个)把待排序序列“划分”为两个部分,左边更小,右边更大,该元素的最终位置已确认

在这里插入图片描述
在这里插入图片描述

算法实现(重点)

//快速排序
void QuickSort(int A[],int low,int high){if(low<high){   //递归跳出的条件int pivotpos=Partition(A,low,high); //划分QuickSort(A,low,pivotpos-1);        //划分左子表QuickSort(A,pivotpos+1,high);       //划分右子表}
}//用第一个元素将待排序序列划分为左右两个部分
int Partition(int A[],int low,int high){int pivot=A[low];   //第一个元素作为枢轴while(low<high){while(low<high&&A[high]>=pivot) --high;         A[low]=A[high];     //比枢轴小的元素移动到左端while(low<high&&A[low]<=pivot)  ++low;         A[high]=A[low];     //比枢轴大的元素移动到右端}   A[low]=pivot;           //枢轴元素存放到最终位置return low;             //返回存放枢轴的最终位置
}

算法效率分析

每一层的QuickSort只需要处理剩余的待排序元素,时间复杂度不超过O(n);
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 若每次选中的“枢轴”将待排序序列划分为均匀的两个部分,则递归深度最小,算法效率最高

  • 若每次选中的“枢轴”将待排序序列划分为很不均匀的两个部分,则会导致递归深度增加,算法效率变低

  • 若初始序列有序或逆序,则快速排序的性能最差(因为每次选择的都是最靠边的元素)

在这里插入图片描述
在这里插入图片描述

8.4 选择排序

每一趟结束就会有一个元素的左边是全小于等于右边是全大于等于
在这里插入图片描述

8.4.1 简单选择排序

在这里插入图片描述

n个元素的简单选择排序需要n − 1趟处理

最后剩一个不用再处理(最后一个一定是最值)

算法实现

//交换
void swap(int &a,int&b){int temp=a;a=b;b=temp;
}//简单选择排序
void SelectSort(int A[],int n){for(int i=0;i<n-1;i++){            //一共进行n-1趟int min=i;                     //记录最小元素位置for(int j=i+1;j<n;j++)         //在A[i...n-1]中选择最小的元素if(A[j]<A[min]) min=j;     //更新最小元素位置if(min!=i) swap(A[i],A[min]);  //封装的swap()函数共移动元素3次}
}

算法性能分析
在这里插入图片描述
在这里插入图片描述

8.4.2 堆排序

在这里插入图片描述

堆的定义
在这里插入图片描述
在这里插入图片描述
建立大根堆
大根堆:根≥ 左 右

  • 思路:把所有非终端结点都检查一遍,是否满足大根堆的要求,如果不满足,则进行调整

  • 检查当前结点是否满足根≥ \ge≥左,右。若不满足,将当前结点与更大的一个孩子互换

  • 若元素互换破坏了下一级的堆,则采用相同的方法继续往下调整(小元素不断“下坠”)

在这里插入图片描述
在这里插入图片描述


在这里插入图片描述
在这里插入图片描述


代码实现

//建立大根堆
void BuildMaxHeap(int A[],int len){for(int i=len/2;i>0;i--){      //从后往前调整所有非终端结点HeadAdjust(A,i,len);}
}//将以 k 为根的子树调整为大根堆
void HeadAdjust(int A[],int k,int len){A[0]=A[k];                     //A[0]暂存子树的根结点for(int i=2*k;i<=len;i*=2){    //沿key较大的子结点向下筛选if(i<len&&A[i]<A[i+1])i++;                   //取key较大的子结点的下标if(A[0]>=A[i]) break;      //筛选结束else{A[k]=A[i];             //将A[i]调整到双亲结点上k=i;                   //修改k值,以便继续向下筛选}}A[k]=A[0]                      //将被筛选结点的值放入最终位置
}

堆排序的完整逻辑
在这里插入图片描述

//建立大根堆
void BuildMaxHeap(int A[],int len)//将以k为根的子树调整为大根堆
void HeadAdjust(int A[],int k,int len)//堆排序的完整逻辑
void HeapSort(int A[],int len){BuildMaxHeap(A,len);         //初始建堆for(int i=len;i>1;i--){      //n-1趟的交换和建堆过程     swap(A[i],A[1]);         //堆顶元素和堆底元素交换HeadAdjust(A,1,i-1);     //把剩余的待排序元素整理成堆}
}

算法效率分析
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
堆排序是不稳定的

8.4.3 堆的插入删除

在这里插入图片描述

  • 在堆中插入新元素
    对于小根堆,新元素放到表尾,与父节点对比,若新元素比父节点更小,则将二者互换。新元素就这样一路“上升”,直到无法继续上升为止

  • 在堆中删除元素
    被删除的元素用堆顶元素替代,然后让该元素不断“下坠”,直到无法下坠为止

8.5 归并排序(Merge Sort)内外部排序

在这里插入图片描述

在这里插入图片描述

Merge(归并/合并)

  • 归并:把两个或多个已经有序的序列合并成一个

“2路”归并 —— 每选出一个小元素就需对比关键字1次。“4路”归并 —— 每选出一个小元素就需对比关键字3次。故m路归并,每选出一个元素需要对比关键字m − 1次

归并排序(手算模拟)
在内部排序中一般采用2路归并

核心操作:把数组内的两个有序序列归并为一个
在这里插入图片描述

代码实现

int *B=(int *)malloc(n*sizeof(int)); //辅助数组B//A[low...mid]和A[mid+1...high]各自有序,将两个部分归并
void Merge(int A[],int low,int mid,int high){int i,j,k;for(k=low,k<=high;k++)B[k]=A[k];        //把A中所有元素复制到B中for(i=low,j=mid+1,k=i;i<=mid&&j<=high,k++){if(B[i]<=B[j])    A[k]=B[i++];  //将较小值复制到A中elseA[k]=B[j++];}//forwhile(i<=mid)   A[k++]=B[i++];  while(j<=high)  A[k++]=B[j++];
}void MergeSort(int A[],int low,int high){if(low<high){int mid=(low+high)/2;     //从中间划分MergeSort(A,low,mid);     //对左半部分归并排序MergeSort(A,mid+1,high); //对右半部分归并排序Merge(A,low,mid,high);    //归并}//if
}

在这里插入图片描述
算法效率分析
在这里插入图片描述
而递归栈的空间复杂度是 l o g 2 n log_2^n log2n,选取最大的就是辅助数组带来的空间复杂度
两个元素相等时,优先使用靠前的那个。归并排序是稳定的

8.6 基数排序(Radix Sort)

在这里插入图片描述

基数排序
可见基数排序不是基于“比较”的排序算法

排序方式

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

递减

在这里插入图片描述

递增

在这里插入图片描述

算法效率分析

基数排序通常基于链式存储实现
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

基数排序的应用

在这里插入图片描述
在这里插入图片描述

各种内部排序算法的比较及应用

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

内部排序算法的应用

在这里插入图片描述
在这里插入图片描述

8.7 外部排序

8.7.1 归并排序的实现

在这里插入图片描述

内存、外存之间的数据交换

在这里插入图片描述

外部排序原理

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

时间开销分析

读写都是一块一块
在这里插入图片描述

优化

在这里插入图片描述

多路归并

在这里插入图片描述

减少初始归并段数量

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

什么是多路平衡归并?

在这里插入图片描述
图没错,定义错了

图错的例子,如下
在这里插入图片描述

8.7.2 败者树

在这里插入图片描述

败者树的构造

在这里插入图片描述
在这里插入图片描述

败者树的使用

在这里插入图片描述
在这里插入图片描述

败者树在多路平衡归并中的应用

在这里插入图片描述
在这里插入图片描述

败者树的实现思路

从不同位置插入元素,会经过不同层数,也就是不同的对比次数
在这里插入图片描述

8.7.3 置换-选择排序

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

8.7.4 最佳归并树

在这里插入图片描述

归并树的性质

在这里插入图片描述

构造2路归并的最佳归并树

在这里插入图片描述

多路归并的情况

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

添加虚段的数量

在这里插入图片描述

数据结构结束

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/130784.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

小工具之视频抽帧

视频抽帧工具&#xff0c;所有视频所在目录以及抽帧图片保存路径 单个视频抽帧操作步骤&#xff1a; 选择文件路径->选择保存路径->拖动跳帧间隔->点击抽取帧 批量视频抽帧操作步骤&#xff1a; 选择文件夹路径->选择保存路径->拖动跳帧间隔->点击抽取帧 imp…

iview label-in-value 和 @on-change 的使用

在select加上label-in-value 之后&#xff0c;就可以调用通过on-change默认的方法&#xff0c;获取到value和label的值了 <Select v-model"params.area" placeholder"选择区县" label-in-value clearable style"width: 102px"><Option…

pip和conda的环境管理,二者到底应该如何使用

关于pip与conda是否能混用的问题&#xff0c;Anaconda官方早就给出了回答 先说结论&#xff0c;如果conda和pip在相同环境下掺杂使用&#xff0c;尤其是频繁使用这两个工具进行包的安装&#xff0c;可能会导致环境状态混乱 就像其他包管理器一样&#xff0c;大部分这些问题均…

mysql在ubuntu上命令行登陆密码不正确

1.登陆提示如下 2.使用mysql -u root -p登录也是类似的 3.打开宝塔面板 点击root密码&#xff0c;更改密码后即可在命令行界面登录 4.登录效果如下

深度学习环境搭建——之Anaconda3安装配置

序言&#xff1a; 工作中一直从事的是FPGA嵌入式开发&#xff0c;图像处理相关的工作。目前随着AI的浪潮&#xff0c;也被动卷入到深度学习的漩涡中&#xff0c;为了不被漩涡卷入深渊&#xff0c;只能自学些深度学习相关的知识&#xff0c;俗话说“好记性不如烂笔头”何况已经…

profinet是什么?

profinet是什么&#xff1f; 参考&#xff1a;一文读懂Profibus、Profinet、Ethernet的区别 PROFINETPROFIbusetherNET&#xff0c;把Profibus的主从结构移植到以太网上&#xff0c;所以profinet会有Controller和Device&#xff0c;他们的关系可以简单的对应于profibus的Maste…

【C++】构造函数意义 ( 构造函数显式调用与隐式调用 | 构造函数替代方案 - 初始化函数 | 初始化函数缺陷 | 默认构造函数 )

文章目录 一、构造函数意义1、类的构造函数2、构造函数显式调用与隐式调用3、构造函数替代方案 - 初始化函数4、初始化函数缺陷5、默认构造函数6、代码示例 - 初始化函数无法及时调用 一、构造函数意义 1、类的构造函数 C 提供的 构造函数 和 析构函数 作为 类实例对象的 初始化…

长短期记忆网络(LSTM)

概念 三个门&#xff1a;遗忘门、输入门、输出门 候选记忆单元 记忆单元 隐状态 ot 控制是否让输出&#xff0c;是否要进行重置。 总结 代码实现 import torch from torch import nn from d2l import torch as d2lbatch_size,num_steps 32,35 train_iter,vocab d2l.load_…

Linux中安装MySQL_图解_2023新

1.卸载 为了避免不必要的错误发生,先将原有的文件包进行查询并卸载 // 查询 rpm -qa | grep mysql rpm -qa | grep mari// 卸载 rpm -e 文件名 --nodeps2.将安装包上传到指定文件夹中 这里采用的是Xftp 3.将安装包进行解压 tar -zxvf 文件名 -C 解压路径4.获取解压的全路…

day55:C++ day5,运算符重载剩余部分、静态成员、继承

#include <iostream> #include <cstring> #define pi 3.14 using namespace std;class Shape { protected:double round;double area; public://无参构造Shape():round(40),area(100){cout<<"Shape::无参构造函数&#xff0c;默认周长为40&#xff0c;面…

sql 时间函数

1&#xff0c;前提 今天看同事写的sql里面出现了时间类的函数&#xff0c;平时自己也经常用到&#xff0c;每次都要百度&#xff0c;还不如自己整理记录在一起&#xff0c;方便后续使用。 2&#xff0c;sql时间函数 2.1 获取当前时间&#xff1a; selectNOW() as 当前日期时…

docker 部署 node.js(express) 服务

1、在 express 项目根目录下新增 Dockerfile 文件&#xff0c;内容如下&#xff1a; 创建服务容器的方法&#xff0c;可以根据自己的情况选择&#xff1a; 1、以下示例为宿主机没有安装 node 环境的写法&#xff1b; 2、先在本地构建包含 node 和 express 的基础镜像&#xff0…

JavaScript中的事件捕获(event capturing)和事件冒泡(event bubbling)

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 事件捕获和事件冒泡⭐ 事件捕获&#xff08;Event Capturing&#xff09;示例&#xff1a; ⭐ 事件冒泡&#xff08;Event Bubbling&#xff09;示例&#xff1a; ⭐ 应用场景⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开…

计算机组成与设计硬件软件接口学习1

计算机的算术运算 子字并行 &#xff08;大致浏览&#xff09;pdf 170页左右 浮点加法不满足结合律&#xff1a; 适用于整型数据类型的并行执行策略并不适用于浮点数据类型 &#xff0c;原因如上↑ 处理器 流水线 流水线是一种能使多条指令重叠执行的实现技术 流水线技术通…

【C++】类和对象核心总结

类和对象核心知识目录&#xff1a; 一、面向过程和面向对象初步认识 二、类的引入定义&#xff08;struct > class&#xff09; 2.1自定义类型 struct 和 class 的区别 2.2类放在内存中的什么存储区&#xff1f; 2.3类中函数定义的方式 2.3.1声明和定义分离&#xff0…

C++项目:在线五子棋对战(网页版)

文章目录 一、项目介绍&#xff08;一&#xff09;用户管理&#xff08;二&#xff09;匹配对战&#xff08;三&#xff09;聊天功能 二、开发环境三、核心技术四、项目大流程 一、项目介绍 本项目主要实现⼀个网页版的五⼦棋对战游戏&#xff0c;其主要支持以下核心功能&…

【Redis】Bitmap 使用及应用场景

前言&#xff1a;bitmap 占用空间小&#xff0c;查询效率高&#xff0c;在一些场景中使用 bitmap 是一个很好的选择。 一、bitmap 相关命令 SETBIT - 设置指定位置的比特值&#xff0c;可以设为 1 或 0 例如 SETBIT key 10 1&#xff0c;将在 key 对应的 bitmap 中第10位设置为…

Springboot后端跨域处理

跨域 当一台服务器资源从另一台服务器&#xff08;不同的域名或者端口&#xff09;请求一个资源或者接口&#xff0c;就会发起一个跨域HTTP请求。 同源&#xff1a;协议、域名、端口都相同 只要一个不同&#xff0c;就是跨域。 例子 请求方响应方是否跨域原因http://www.ba…

Docker认识即安装

Docker及相关概念 Docker和虚拟机方式的区别&#xff1a;虚拟机技术是虚拟出一套硬件后&#xff0c;在其上运行一个完整的操作系统&#xff0c;在该系统上在运行所需应用进程&#xff1b;而容器内的应用进程是直接运行于宿主的内核&#xff0c;容器内没有自己的内核&#xff0…

GO语言网络编程(并发编程)Channel

GO语言网络编程&#xff08;并发编程&#xff09;Channel 1、Channel 1.1.1 Channel 单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。 虽然可以使用共享内存进行数据交换&#xff0c;但是共享内存在不同的goroutine中容易发生竞态…