<八大排序>万字详解(Java实现).插入排序、希尔排序、堆排序、快速排序、归并排序、计数排序...

简介: <八大排序>万字详解(Java实现).插入排序、希尔排序、堆排序、快速排序、归并排序、计数排序

一.插入排序

(一).直接插入排序

1.思路:

直接插入排序,先假定前end个是有序的,把第end+1个插入到前end个元素当中,插入完之后end++。那么怎么插入呢?当然是从后往前一个一个比的呀,判断这个数是否小于前面这end个,如果小于,就把前面的数挪到后一个,然后这个数比完了就end--;如果大于,就把这个数放到end+1的位置上。一个数就插完啦

2.代码:

    public static void insertSort(int[] array) {
        for (int i = 0; i < array.length-1; i++) {
            int end = i;   //因为end每次放完一个,要往后走所以要循环++
            int tmp = array[end+1];//先将要比较的这个数字保存起来
            while(end >= 0){       //如果end没走完队头就继续向前比
                if(tmp<=array[end]){//如果tmp小,就把这个元素往后挪
                    array[end+1] = array[end];
                    end--;         //然后继续向前比
                }else{
                    break; //因为end可能是走到头了出循环,也可能
                }          //是tmp找到了自己的位置出循环,无论如何都要
            }              //放在end+1的位置上,所以直接break
            array[end+1] = tmp;//出了循环放tmp
        }
    }

3.时间复杂度

直接插入排序的时间复杂度是多少呢?

最坏的情况下:如果数组本身是逆序,你要排成升序,则次数为1+2+3+...+n-1=(n*(n-1))/2

                        所以时间复杂度为O(N2)

(二)希尔排序

1.思路:

希尔排序,是在直接插入排序基础上的优化。就是先进行分组直接插入排序(预排序),然后随着分组的越来越细,数组也越来越接近于有序。


多组间隔为gap的预排序,gap由大变小。gap越大,大的数越快的到后面,小的数越快的到前面。gap越大,排完越不接近于有序;gap越小,排完越接近于有序。当gap==1时,就变成了直接插入排序。

2.代码:

    public static void shellSort(int[] array){
        int gap = array.length;
        while(gap > 0){
            gap/=2;//间隔每次/2  也可以gap=gap/3+1;
            //把间隔为gap的多组数据同时插入排序(把1换成gap)
            for (int i = 0; i < array.length - gap; i++) {
                int end = i;
                int tmp = array[end+gap];
                while(end >= 0){
                    if(tmp < array[end]){
                        array[end+gap] = array[end];
                        end-=gap;
                    }else{
                        break;
                    }
                }
                array[end+gap] = tmp;
            }
        }
    }

3.时间复杂度

    public static void shellSort(int[] array){
        int gap = array.length;
        while(gap > 0){//这个循环的时间复杂度为logN 因为每次都除2
            gap/=2;
            //如果gap特别大,此循环时间复杂度为O(N)
            //如果gap特别小,此时数组已经块接近有序了,所以时间复杂度还是O(N)
            for (int i = 0; i < array.length - gap; i++) {
                int end = i;
                int tmp = array[end+gap];
                while(end >= 0){
                    if(tmp < array[end]){
                        array[end+gap] = array[end];
                        end-=gap;
                    }else{
                        break;
                    }
                }
                array[end+gap] = tmp;
            }
        }
    }

所以,时间复杂度为:O(N*logN) 或者 O(N*log3N)

平均的时间复杂度是O(N^1.3)

二.选择排序


(一).直接选择排序(优化版)

1.思路:

普通的直接选择排序是遍历一遍选出最大(小)的数放在最后(前)边,然后再遍历一遍,再选出次大的数,依次往下走...

优化版的的直接选择排序是遍历一次选出两个(一个最大值,一个最小值),然后begin++,end--

再次遍历选出次大,次小的数,依次往下走...

2.代码:

    public static void selectSort(int[] array){
        int begin = 0;
        int end = array.length - 1;
        while(begin < end){
            int mini = begin;
            int maxi = begin;
            for (int i = begin; i <= end; i++) {
                if(array[i] < array[mini]){
                    mini = i;
                }
                if(array[i] > array[maxi]){
                    maxi = i;
                }
            }
            //交换最小值和begin,使得最小值在前
            int tmp = array[mini];
            array[mini] = array[begin];
            array[begin] = tmp;
            if(maxi == begin){//如果刚刚被交换的begin刚好是最大值
                maxi = mini;  //那么需要调整一下
            }
            //交换最大的和end,使得最大值在后
            tmp = array[maxi];
            array[maxi] = array[end];
            array[end] = tmp;
            begin++; //找完一遍,begin++ end--
            end--;
        }
    }

3.时间复杂度

N  N-2  N-4  N-6...

所以,时间复杂度为:O(N^2)

由此可见,直接选择排序的效率最低

(二).堆排序

1.堆的铺垫

如图所示,堆的逻辑结构是一颗完全二叉树。堆的物理结构是一个数组。


通过下标访问父子关系的结点,下标关系如下:


leftchild = parent*2 + 1


rightchild = parent*2 + 2


parent = (child - 1) / 2


大堆要求:树中所有的父亲都大于等于孩子 ——>根是最大的


小队要求:树中所有的父亲都小于等于孩子  ——>根是最小的

2.向下调整算法(建小堆为例)

前提:如果小堆的向下调整算法,前提左右子树必须都是小堆才能使用—>最多调整高度次,


                                                                                                            时间复杂度为O(logN)

思路:从根节点开始,选出左右孩子中小的那个,跟父亲比较,如果比父亲小就交换。然后在继续往下调,调到叶子结点或都比父亲大就终止

代码:

    //向下调整算法
    public static void adjustDown(int[] array ,int n ,int root){
        int parent = root;
        int child = parent *2 + 1;//默认是左孩子小
        while(child < n){
            //选出左右孩子中小的那个
            if(child+1 < n && array[child+1]<array[child]){
                child++;//因为是连续空间,所以++child就为右孩子
            }
            //孩子与父亲比较
            if(array[child] < array[parent]){
                //孩子小,交换父子值
                int tmp = array[child];
                array[child] = array[parent];
                array[parent] = tmp;
                //此时的父亲向下走,孩子也跟着走
                parent = child;
                child = parent *2 + 1;
            }else{ //孩子比父亲大,说明排完序了
                break;
            }
        }
    }

3.建堆

思路:用向下调整算法就可以进行排序,但是如果左右子树不是小堆就不能用向下调整算法了。怎么办?


所以,我们想出了对策,倒着从最后一颗子树开始调(即根从下往上走),这样就可以保证,从下往上依次都是小堆。


再分析倒着走,叶子没有孩子不需要调,所以我们从最后一个非叶子子树开始调(即最后一个元素的父亲),下标为 (n-1-1) / 2,然后root依次变小


代码:

    public static void heapSort(int[] array,int n){
        //建堆
        for (int i = (n-1-1)/2; i >= 0; i--) {
            adjustDown(array,n,i);//根从下往上走(变小)
        }
    }

时间复杂度:O(N)


                    建堆的时间复杂度(计算次数)计算是个很复杂的过程,要列出公式错位相减,


                    最后推出t(n)=N-logN  , 所以其时间复杂度为O(N)


注:如果建大堆,就把向下调整算法中,选孩子中小的改为大的,然后孩子大于父亲才交换,就可以了。

4.排升序建大堆还是建小堆?

排升序要建大堆!!!!!

因为每次建大堆的根为最大的数,把他放到最后面,然后n--,把前n-1个数继续向下调整,找出次大的数,在把他放到倒数第二的位置上,以此类推...时间复杂度可以为O(N*logN)


如果要建小堆,那么你第一个根虽然是最小的,但是你除去第一个数,后面的n-1个数怎么找出最小的数呢?还是得重新建堆,那么时间复杂度就是O(N^2)了,效率很低

5.排升序整体代码

    //向下调整算法 建大堆
    public static void adjustDown(int[] array ,int n ,int root){
        int parent = root;
        int child = parent *2 + 1;//默认是左孩子大
        while(child < n){
            //选出左右孩子中大的那个
            if(child+1 < n && array[child+1]>array[child]){
                child++;//因为是连续空间,所以++child就为右孩子
            }
            //孩子与父亲比较
            if(array[child] > array[parent]){
                //孩子大,交换父子值
                int tmp = array[child];
                array[child] = array[parent];
                array[parent] = tmp;
                //此时的父亲向下走,孩子也跟着走
                parent = child;
                child = parent *2 + 1;
            }else{ //孩子比父亲小,说明排完序了
                break;
            }
        }
    }
    public static void heapSort(int[] array,int n){
        //建堆
        for (int i = (n-1-1)/2; i >= 0; i--) {
            adjustDown(array,n,i); //先建一个大堆,选出最大的数
        }
        int end = n - 1; //end表示进入下一次堆排序的元素个数,也表示最后的下标
        while(end > 0){
            int tmp = array[0];//先把最大的数挪到最后
            array[0] = array[end];
            array[end] = tmp;
            adjustDown(array,end,0);//再继续向下调整,元素个数为end个
            --end; //进堆的个数--
        }
    }

6.整体时间复杂度:

堆的时间复杂度主要是有两部分组成:初始化建堆(O(N))+排序重建堆(N*O(logN))

三.交换排序


(一).冒泡排序

1.思路:

冒泡排序的思路非常简单,就是从头把第i个数与第i+1个数比较,如果比他大就交换位置,然后i++。以此类推,就把最大的数放到了最后面。然后再继续从头一个一个比较,到倒数第二个数停,这时次大的数就被交换到这个位置了,依次类推...

2.代码:

    public static void bubbleSort(int[] array){
        for (int i = 0; i < array.length - 1; i++) {//冒泡排序的趟数
            int exchange = 0;//作为某一趟是否交换的标志
            for (int j = 0; j < array.length - 1 - i; j++) {//每趟要比较多少次
                if(array[j] > array[j+1]){
                    int tmp = array[j];
                    array[j] = array[j+1];
                    array[j+1] = tmp;
                    exchange = 1;
                }
            }
            if(exchange == 0){//说明这一趟没交换
                break;
            }
        }
    }

3.时间复杂度:

N-1  N-2  N-3  N-4 ...

所以时间复杂度为:O(N^2)

4.冒泡与直接插入排序相比谁更优?

先说结果,直接插入排序更强

他俩对于一般的都是O(N^2),但是对于有序、接近有序或局部有序,插入排序的适应性更强

例:1 2 3 5 4 6

冒泡:N+1  +  N+2             插入排序:N

(二).快速排序

单趟排序的不同方法

1.挖坑法

a.思路:

先拿一趟说思路,一个数怎么才能找到自己排完序时的位置呢?我们可以想到,当排完序时,一个数的左边都是比他小的数,右边都是比他大的数。所以我们可以让一个数的左边都是小于它的,右边都是大于它的,来找到它应该在的位置。那么应该怎样做到呢?我们可以先排第一个数,把第一个数设为key,此时这个位置就可以被覆盖,即挖了个坑。让end从右边往左找比他小的,找到了放到坑里,end作为新的坑。让begin从左边找比他大的,找到了放到右边的坑里,形成新的坑,以此类推下去...最终当begin==end时,此时的坑就是key应该在的位置,key就找到了自己的位置。


那么这是一个数找到了位置,我们怎么排下去呢?这就用到了分治算法(递归)。我们可以把key左边看成一段新的数组,右边看成一段新的数组。继续找新的key...直到不能再分下去为止...


如图所示:

(借用一张图)

b.代码:

    public static void quickSort(int[] array,int left,int right){
        if(left>=right){//如果只剩1个,就不用再分
            return;
        }
        int begin = left;
        int end = right;
        int pivot = begin;
        int key = array[begin];
        while(begin < end){//如果begin小于end才继续
            //从右往左找比key小的
            while(begin<end && array[end]>=key){//因为循环里面begin与end也会改变,所以需要再次判断
                end--;//如果不比key小end就--
            }
            array[pivot] = array[end];//比key小就放到坑里
            pivot = end; //end形成新的坑
            //从左往右找比key大的
            while(begin<end && array[begin]<=key){
                begin++;
            }
            array[pivot] = array[begin];
            pivot = begin;
        }
        array[pivot] = key;
        //[left,right]——>[left,pivot-1] pivot [pivot+1,right]
        //所以递归下去
        quickSort(array,left,pivot-1);
        quickSort(array,pivot+1,right);
    }

c.时间复杂度:

其单趟排序,虽然是两个while,但是只遍历一遍,所以是O(N)

那么快排要排几趟呢?最理想的情况下就是pivot每次都把数组二分

那么就需要logN次

所以,整体的时间复杂度是:O(N*logN)

d.快排什么情况下最坏?

答:有序的情况下。因为如果数组有序,那么快排每趟排序只将一个数与数组分离,只安排好了一个数。此时的时间复杂度为O(N^2)

e.三数取中

我们刚刚发现,如果快排的key总取最值的话,会使快排退化到O(N^2),所以我们采取一种解决方式:三数取中

思路:我们让key等于下标为left,right和mid这三个数的中间值。这样可以保证key每次都取不到最值

f.优化快排

三数取中快排代码:

    //三数取中
    public static int getMid(int[] array,int left,int right){
        int mid = (left + right)/2;
        if(array[left]>array[mid]){
            if(array[right]>array[left]){
                return left;
            }else if(array[mid]>array[right]){
                return mid;
            }else{
                return right;
            }
        }else{//array[left]<array[mid]
            if(array[mid]<array[right]){
                return mid;
            }else if(array[left]>array[right]){
                return left;
            }else{
                return right;
            }
        }
    }
 
    public static void quickSort(int[] array,int left,int right){
        if(left>=right){//如果只剩1个,就不用再分
            return;
        }
        //得到中数下标
        int mid = getMid(array,left,right);
        //交换一下第一个数和中间值,使得begin取的数是中间值
        if(mid!=left) {
            int tmp = array[mid];
            array[mid] = array[left];
            array[left] = tmp;
        }
        int begin = left;
        int end = right;
        int pivot = begin;
        int key = array[begin];
        while(begin < end){//如果begin小于end才继续
            //从右往左找比key小的
            while(begin<end && array[end]>=key){//因为循环里面begin与end也会改变,所以需要再次判断
                end--;//如果不比key小end就--
            }
            array[pivot] = array[end];//比key小就放到坑里
            pivot = end; //end形成新的坑
            //从左往右找比key大的
            while(begin<end && array[begin]<=key){
                begin++;
            }
            array[pivot] = array[begin];
            pivot = begin;
        }
        array[pivot] = key;
        //[left,right]——>[left,pivot-1] pivot [pivot+1,right]
        //所以递归下去
        quickSort(array,left,pivot-1);
        quickSort(array,pivot+1,right);
    }

g.小区间优化:

我们知道递归调用每次把数据分段,如果调到最后几层,会把1000000个数分成好多份,会产生大量的递归调用。我们主要的递归调用次数就是在最后几层。所以我们采用小区间优化的方法

思路:就是如果区间小于某一值我们就不用递归排序,而是选择直接插入排序排这几个数

代码:

鄙人还没有能力用Java写出小区间优化,所以只能用C顶一下(个_个)

2.左右指针法

a.思路:单趟排序的第二种方法,左右指针法。与挖坑法有一些相同之处,也是先定一个key,然后begin找大,end找小。只不过,当begin和end找到时直接把他俩交换。因为一开始begin这边就少一个数(key),根据一一对应的原则,当end与begin相遇时,这里的值一定小于key。所以直接把key与begin的值交换一下


b.代码:

    //左右指针法    
    public static int partSort2(int[] array,int left,int right){
        //得到中数下标
        int mid = getMid(array,left,right);
        //交换一下第一个数和中间值,使得begin取的数是中间值
        if(mid!=left) {
            int tmp = array[mid];
            array[mid] = array[left];
            array[left] = tmp;
        }
        int begin = left;
        int end = right;
        int keyi = begin;
        int key = array[begin];
        while(begin<end){
            //end找小
            while(begin<end && array[end]>=key){
                end--;
            }
            //begin找大
            while(begin<end && array[begin]<=key){
                begin++;
            }
            //左右一交换
            int tmp = array[end];
            array[end] = array[begin];
            array[begin] = tmp;
        }
        //begin与keyi的值一交换
        int tmp = array[begin];
        array[begin] = array[keyi];
        array[keyi] = tmp;
        return begin;
    }
 
    public static void quickSort(int[] array,int left,int right){
        if(left>=right){//如果只剩1个,就不用再分
            return;
        }
        int keyIndex = partSort2(array,left,right);
        //[left,right]——>[left,keyIndex-1] keyIndex [keyIndex+1,right]
        //所以递归下去
        quickSort(array,left,keyIndex-1);
        quickSort(array,keyIndex+1,right);
    }

3.前后指针法

a.思路:同样的先定一个key。然后定义两个变量prev,cur做下标,cur从头开始找比key小的。在cur找的过程中,cur与prev中间间隔的数字就是比key大的。cur找到了,prev++,然后他俩交换。如果没找到cur就继续++。只到走到头,这时候prev的值是比key小的,所以把keyi与prev的值交换。


b.代码:

    public static int partSort3(int[] array,int left,int right){
        //得到中数下标
        int mid = getMid(array,left,right);
        //交换一下第一个数和中间值,使得begin取的数是中间值
        if(mid!=left) {
            int tmp = array[mid];
            array[mid] = array[left];
            array[left] = tmp;
        }
        int keyi = left;
        int prev = left;
        int cur = left+1;
        while(cur <= right){
            if(array[cur]<array[keyi] && ++prev!=cur){//如果prev++之后还是cur                                
                int tmp = array[prev];       //++肯定要++,但是自己和自己交换没意义
                array[prev] = array[cur];
                array[cur] = tmp;
            }
            cur++;
        }
        int tmp = array[keyi];
        array[keyi] = array[prev];
        array[prev] = tmp;
        return prev;
    }
    public static void quickSort(int[] array,int left,int right){
        if(left>=right){//如果只剩1个,就不用再分
            return;
        }
        int keyIndex = partSort3(array,left,right);
        //[left,right]——>[left,keyIndex-1] keyIndex [keyIndex+1,right]
        //所以递归下去
        quickSort(array,left,keyIndex-1);
        quickSort(array,keyIndex+1,right);
    }

四.归并排序


(一).归并排序

1.思路:

先假设一个前提,把一个数组分成两半区间,如果左半区间有序,右半区间有序。那么我们就可以采用归并算法,依次对比取小的放到新的临时数组中。然后再拷贝回来。


那么再归并之前,左右子区间没有序怎么办?那就先递归分到区间只剩一个元素的时候,再合并,在left和right新的数组下标内排序。最后一起拷贝到array中


逻辑类似于二叉树的后序


2.代码:

#include<stdlib.h>
void _mergeSort(int* a, int left, int right, int* tmp)
{
  if (left >= right)
    return;
  int mid = (left + right) >> 1;//相当于/2
  //把数组二分进行递归
    //把数组分为[left,mid]和[mid+1,right]两个区间
  _mergeSort(a, left, mid, tmp);
  _mergeSort(a, mid + 1, right, tmp);
  //分好 进行归并
  int begin1 = left, end1 = mid;
  int begin2 = mid + 1, end2 = right;
  int index = left;
  //一个个比较在tmp里排序
  while (begin1 <= end1 && begin2 <= end2)
  {
    if (a[begin1] < a[begin2])
    {
      tmp[index++] = a[begin1++];
    }
    else
    {
      tmp[index++] = a[begin2++];
    }
  }
  //有一个走完,把另一个全部拷过去
  while (begin1 <= end1)
  {
    tmp[index++] = a[begin1++];
  }
  while (begin2 <= end2)
  {
    tmp[index++] = a[begin2++];
  }
  //所有数已经在tmp内排完啦,最后把tmp拷贝到a中
  for (int i = left; i <= right; i++)
  {
    a[i] = tmp[i];
  }
}
void mergeSort(int array[], int n)
{
  int* tmp = (int*)malloc(sizeof(int) * n);//动态开辟一个数组,把数拷贝到这里交换
  //因为如果在这个函数里递归,那么就会动态开辟很多个数组,所以要另一个子函数
  _mergeSort(array, 0, n - 1, tmp);//把两个数组给它,还有数组的下标
    free(tmp);
}

3.对文件中的数据排序:

归并排序又叫外排序。可以对文件中的数据进行排序。因为文件中数据的读取不支持随机访问,所以别的排序失效。


问题:假设10G的数据放在硬盘的文件中,要排序,如何排呢?(可能内存不够,假设有1G内存可以用)


思路:把10G的文件切分成10个1G的文件,用归并排序把这些1G的文件排有序。最后再磁盘上将这些1G的文件进行归并


五.非递归的归并排序与快排


此内容有待开发...过段时间会补上的!!

六.计数排序


(一).计数排序

1.思路:

计数排序是一种非比较排序,顾名思义就是先malloc一块新的数组,然后每个数字对应一个位置,这个位置存放数字出现的次数,利用相对映射,再讲array中的数重新输入。

2.代码:

void countSort(int a[], int n)
{
  int min = a[0];
  int max = a[0];
  for (int i = 0; i < n; i++)//找出最大值和最小值
  {
    if (a[i] > max)
    {
      max = a[i];
    }
    if (a[i] < min)
    {
      min = a[i];
    }
  }
  int range = max - min + 1;//算出范围 用于动态申请和相对映射
  int* count = (int*)malloc(sizeof(int) * range);
  memset(count, 0, sizeof(int) * range);//初始化数组为0
  //开始计数
  for (int i = 0; i < n; i++)
  {
    count[a[i] - min]++;//相对映射
  }
  //开始将计数转化成数
  int j = 0;
  for (int i = 0; i < range; i++)
  {
    while (count[i]--)
    {
      a[j++] = i + min;
    }
  }
}

3.时间/空间复杂度:

时间复杂度:O(N+range)  说明它适用于集中一组整形数据排序

空间复杂度:O(range)

七.七大排序的总结对比


注:快排带有三数取中的时候最坏情况基本不会出现

      稳定性: 如果你排序的数据中有相同的数字,当排完序之后这两个相同的数字相对顺序不会变,就是稳定的;否则就是不稳定的。(假设要求成绩相同时,先提交的在前,这时候就得用稳定的排序了)

目录
相关文章
|
26天前
|
存储 搜索推荐 算法
【用Java学习数据结构系列】七大排序要悄咪咪的学(直接插入,希尔,归并,选择,堆排,冒泡,快排)以及计数排序(非比较排序)
【用Java学习数据结构系列】七大排序要悄咪咪的学(直接插入,希尔,归并,选择,堆排,冒泡,快排)以及计数排序(非比较排序)
21 1
|
3月前
|
存储 Java
Java中ArrayList 元素的排序
本文提供了Java中根据`ArrayList`元素的某个属性进行排序的示例代码,包括实现`Comparable`接口和重载`compareTo`方法,然后使用`Collections.sort`方法进行排序。
|
3月前
|
存储 Java
|
3月前
|
存储 Java
|
3月前
|
存储 搜索推荐 Java
|
搜索推荐 Java
希尔排序(简单易懂,图文并貌,插入排序)java代码实现
希尔排序(简单易懂,图文并貌,插入排序)java代码实现
151 0
希尔排序(简单易懂,图文并貌,插入排序)java代码实现
|
9天前
|
监控 安全 Java
在 Java 中使用线程池监控以及动态调整线程池时需要注意什么?
【10月更文挑战第22天】在进行线程池的监控和动态调整时,要综合考虑多方面的因素,谨慎操作,以确保线程池能够高效、稳定地运行,满足业务的需求。
80 38
|
6天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
1天前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
11 4
|
1天前
|
消息中间件 供应链 Java
掌握Java多线程编程的艺术
【10月更文挑战第29天】 在当今软件开发领域,多线程编程已成为提升应用性能和响应速度的关键手段之一。本文旨在深入探讨Java多线程编程的核心技术、常见问题以及最佳实践,通过实际案例分析,帮助读者理解并掌握如何在Java应用中高效地使用多线程。不同于常规的技术总结,本文将结合作者多年的实践经验,以故事化的方式讲述多线程编程的魅力与挑战,旨在为读者提供一种全新的学习视角。
15 3