速通数据结构与算法第七站 排序
系列文章目录
速通数据结构与算法系列
1 速通数据结构与算法第一站 复杂度 http://t.csdnimg.cn/sxEGF
2 速通数据结构与算法第二站 顺序表 http://t.csdnimg.cn/WVyDb
3 速通数据结构与算法第三站 单链表 http://t.csdnimg.cn/cDpcC
4 速通数据结构与算法第四站 双链表 http://t.csdnimg.cn/0VyDl
5 速通数据结构与算法第五站 栈&&队列 http://t.csdnimg.cn/MRU83
6 速通数据结构与算法第六站 树&&堆 速通数据结构与算法第六站 树&&堆-CSDN博客
感谢佬们支持!
目录
系列文章目录
- 前言
- 一、插入排序
- 复杂度分析
- 二、希尔排序
- 复杂度分析
- 三、选择排序
- 复杂度分析
- 四、堆排序
- 复杂度分析
- 五、冒泡排序
- 复杂度分析
- 六、快排
- 1 hoare法
- 2 挖坑法
- 3 前后指针法(recommend)
- 4 非递归
- 5 优化之三数取中(随机选key)
- 6 优化之小区间优化
- 7 优化之三路划分
- 8 SGI sort设计
- 9 复杂度分析
- 七、归并排序
- 1 递归版本
- 2 非递归但梭哈实现法
- 3 非递归但不梭哈实现法
- 4 复杂度分析
- 八、其他排序的介绍
- 1 计数排序
- 复杂度分析
- 2 桶排序
- 复杂度分析
- 3 基数排序
- 九、排序总结
- 总结
前言
这一节是速通数据结构的最后一节,我们要来学习排序。排序,看起来是个很简单的话题,实则一点也不简单。
举个例子,为了达到极致的效率,STL(SGI)算法库中的排序优化一大堆,首先要用快排,当数据个数小于16为了减少递归层数,改调插排,为了key的大小更适中搞了三数取中
一旦"划分恶化"改调堆排,防止时间复杂度恶化到O(n方)。所以排序还是很值得我们学习滴~
注:这篇文章收稿时达到了2w字!最用心的一集
一、插入排序
插入排序类似于我们整理扑克的过程
学排序首先要从单趟排序开始
假设现在有一个[2,4,7]的有序序列,我们要往其中插入一个数

1、假设我们插入的是1,2、4、7就都要往后移动
2、假设我们插入的是5,那需要挪的就是7
3、假设我们插入的是8,那就不需要挪,直接在最后插入即可
有了思路之后,我们就可以先来搞单趟排序了
单趟排序的逻辑是将tmp插入[0,end]的有序区间时
“我比你小,你挪;我比你大,插你后面”
void InsertSort(int*a,int tmp)
{int end;int tmp;while(end>=0){if(a[end]>tmp){a[end+1]=a[end];} else{break;}//我比你大||我比所有人都小,即end=-1a[end+1]=tmp;}
}
有了单趟排序,就能推出整个排序了,显然,第一个数不用排,最后一个数的位置是i-1
所以每次的end为i-1,tmp为a [i]
最后得出的代码就是这样的。
void InsertSort(int *a,int n)
{assert(a);for (int i = 1; i < n; ++i){int end = i-1;int tmp=a[i];while (end >= 0){if (a[end] >tmp){a[end + 1] = a[end];--end;}else//a[end]<=x{break;}}a[end + 1] = tmp;}
}//SGI°æ²åÈëÅÅÐòint main()
{int arr[] = { 3,5.2,9,8,10,2 };int size = sizeof(arr) / sizeof(arr[0]);InsertSort(arr, size);
}
打印结果:

复杂度分析
按最坏来看,如果是完全逆序的情况
时间复杂度就是1+2+……+n,最后就是O(n^2)
但是如果数据是完全有序/接近有序的情况,我们仅需一次遍历即可
也就是O(n)
空间复杂度是O(1)
二、希尔排序
希尔排序算是对插入的升级
希尔排序又称缩小增量法,希尔排序的基本增量是:先选定一个整数,将待排序中的所有记录分成个组,所有距离为选定整数的记录分在同一组内,并对每一组的记录做好排序
。然后将选定整数除以一个数,重复上述步骤,当这个整数到达1时,进行的最后一次排序
会得到一个有序的结果
看似逻辑很复杂,实际很简单,就是分组进行插入排序而已,利用了插入排序数据越有序
时间复杂度越小这一特性
如何分组?
我们通过选取一个gap来操作,让间隔为gap的数为一组
例:
gap=3
[9,1,2,5,7,4,8,6,3,5]
我们将其分为红、绿、蓝三组

现对红色一组进行插入排序

蓝色一组

绿色一组

我们先来写红色组排队的逻辑
只需一次让i+=gap即可
int gap=3;for (int i = 0; i < n - gap; i+=gap){int end=i;int tmp = a[end + gap];while (end >= 0){if (a[end] > tmp){a[end + gap] = a[end];end -= gap;}else//a[end]<=x{break;}}a[end + gap] = tmp;}
下来如果要排三组,就要再加一层for循环
由于只有3组,所以我们给一个gap次的for循环即可
int gap=3;for(int j=0;j<gap;++j){for (int i = j; i < n - gap; i+=gap){int end=i;int tmp = a[end + gap];while (end >= 0){if (a[end] > tmp){a[end + gap] = a[end];end -= gap;}else//a[end]<=x{break;}}a[end + gap] = tmp;}}
或者用另一种方法,这样就可以在代码层面上减少一层循环(实际循环是没有减少的)
我们不让i一次+=gap了,而是一次加一个
int gap=3;for (int i = 0; i < n - gap; i++){int end=i;int tmp = a[end + gap];while (end >= 0){if (a[end] > tmp){a[end + gap] = a[end];end -= gap;}else//a[end]<=x{break;}}a[end + gap] = tmp;}
这样就从之前的一组一组插排变成了多组同时开始插排
在此基础上,我们要变化gap的值,最终让gap变为1,这样的话,最终gap就会等于1,最后一次排序的时候就会变成插排,实现有序
#include<stdio.h>
#include<assert.h>
//ϣvoid ShellSort(int* a, int n)
{assert(a);int gap = n;while (gap > 1)//gap==1 {gap /= 2;for (int i = 0; i < n - gap; i++){int end = i;int tmp = a[end + gap];while (end >= 0){if (a[end] > tmp){a[end + gap] = a[end];end -= gap;}else//a[end]<=x{break;}}a[end + gap] = tmp;}}
}int main()
{int arr[] = { 3,5,2,9,8,10,2,2,9,8 };int size = sizeof(arr) / sizeof(arr[0]);ShellSort(arr, size);
}
打印结果

复杂度分析
希尔排序按我们的常理来看,其时间复杂度取决于gap的选择
所以其时间复杂度很难计算,这里给到两本权威书上的解释

![]()

由于咱们的gap是按照knuth的方式来提出的,所以时间复杂度就O(n^1.25)~1.6*O(n^1.25)来算
所以这个排序是个很玄学的排序,就像跳表中选取每次插入数据是否提高层数的概率p一样
空间复杂度依旧是O(1)
三、选择排序
先介绍一下选择排序的基本思想
每一次从待排序的数据元素中选出最大/最小的一个元素,存放在序列的起始位置,直到待排序的所有元素全部排完
我们首先容易想到的就是直接选择排序,每次遍历我们可以同时选出最大/最小的数,将其放在序列的最左/最右
单趟就很容易想了,就是每次遍历选出最小/最大即可
int left = 0, right = n - 1;int maxi = left, mini = left;for (int i = left; i <= right; ++i)//ұ{if (a[i] > a[maxi]){maxi = i;}if (a[i] < a[mini]){mini = i;}}swap(&a[left], &a[mini]);swap(&a[right], &a[maxi]);
剩下只要每次维护left,right即可
但是还有一个坑,如果某一波,maxi正好就在left的位置
那更新最小值的时候就把最大值换走了
所以我们加个特判即可
#include<stdio.h.>void swap(int* a, int* b)
{int tmp = *a;*a = *b;*b = tmp;
}void SelectSort(int*a,int n)
{int left = 0, right = n - 1;while (left < right){int maxi = left, mini = left;for (int i = left; i <= right; ++i)//ұ{if (a[i] > a[maxi]){maxi = i;}if (a[i] < a[mini]){mini = i;}}swap(&a[left], &a[mini]);if (maxi == left){maxi = mini;}swap(&a[right], &a[maxi]);++left;--right;}
}
int main()
{int arr[] = { 3,5,2,9,8,10,2,2,9,8 };int size = sizeof(arr) / sizeof(arr[0]);SelectSort(arr, size);for (int i = 0; i < size; ++i){printf("%d ", arr[i]);}
打印结果

复杂度分析
选择排序由于每次都需要遍历找最大/最小值,所以时间复杂度最好最坏情况都是O(n^2),确实是最fvv的排序
空间复杂度是O(1)
四、堆排序
堆排序就是从直接选择排序变成了用堆选数。
关于堆排序的逻辑我们在上篇博客已经介绍过了,这里就不再赘述了
直接上代码
#include<stdio.h>//swap
void swap(int* px, int* py)
{int tmp = *px;*px = *py;*py = tmp;
}//向上调整
void AdjustUp(int* a, int child)
{int parent = (child - 1) / 2;while (child > 0)//最多调到根{if (a[child] > a[parent]){swap(&a[child], &a[parent]);child = parent;parent = (child - 1) / 2;}else{break;}}
}//向下调整
void AdjustDown(int* a, const int n, int parent)
{//找左右孩子大的一个交换int child = parent * 2 + 1;//suppose左孩子大,经典玩法while (child < n)//如果孩子超出了数组范围,说明parent是叶子节点{if (child + 1 < n && a[child + 1] > a[child])//防止越界{child++;}if (a[child] > a[parent]){swap(&a[parent], &a[child]);parent = child;child = parent * 2 + 1;}else{break;}}
}//排升序--建大堆(向上调整建堆)
void HeapSort1(int* a, int n)
{for (int i = 1; i < n; ++i)//O(lgN){AdjustUp(a, i);}int end = n - 1;while(end>0){swap(&a[0], &a[end]);AdjustDown(a, end, 0);end--;}
}//排升序--建大堆(向下调整建堆)
void HeapSort2(int* a, int n)
{for (int i = (n - 1 - 1) / 2; i >= 0; --i){AdjustDown(a, n, i);}int end = n - 1;while (end > 0){swap(&a[0], &a[end]);AdjustDown(a, end, 0);end--;}
}int main()
{int arr[] = { 2,4,5,6,7,2,6 };const int n = sizeof(arr) / sizeof(arr[0]);HeapSort2(arr, n);for (int i = 0; i < n; ++i){printf("%d ", arr[i]);}printf("\n");return 0;
}
打印结果

复杂度分析
关于堆排序的时间复杂度我们也分析过了
如果是向下调整建堆,就是O(n*lgn)
由于是原地建堆,所以空间复杂度是O(1)
五、冒泡排序
冒泡排序属于交换排序
我们来介绍一下交换排序的思想
所谓交换,就是根据序列中的两个记录键值对的比较结果来swap这两个记录在序列的位置
交换排序的特点是:将键值较大的记录先后移动,将键值较小的记录先前移动
冒泡排序也是我们非常熟悉的排序了
这里直接上代码
#include<stdio.h>
#include<stdbool.h>void swap(int* a, int* b)
{int tmp = *a;*a = *b;*b = tmp;
}void BubbleSort(int* a, int n)
{for (int i = 0; i < n; ++i){bool flag = false;for (int j = i + 1; j < n; ++j){if (a[i] > a[j]){flag = true;swap(&a[i], &a[j]);}}if (flag == false){break;}}
}int main()
{int arr[] = { 3,5,2,9,8,10,2,2,9,8 };int size = sizeof(arr) / sizeof(arr[0]);BubbleSort(arr, size);for (int i = 0; i < size; ++i){printf("%d ", arr[i]);}
}
打印结果

复杂度分析
显然,如果序列本身有序,遍历一次即可排好序
所以时间复杂度最好情况为O(n)
如果是最坏情况,则是O(n^2)
需要注意的是,在相对有序的情况下,冒泡排序的时间复杂度还是不如插入排序的
空间复杂度是O(1)
六、快排
1 hoare法
单趟:选出一个基准值key,把他放到正确的位置(最终排好序要蹲的事)
例:

最终就会变成

我们有三种方法,第一种是这样的:
法一:
1、具体操作为左边找严格比key大的,右边找严格比key小的,然后swap
2、最后在最后停下的位置和key所在的位置交换一下
一波单趟排序就做好啦~
//hoare法
int partition1(int* a, int left, int right)
{int mid = GetMidNumi(a, left, right);swap(&a[left], &a[mid]);int keyi = left;while (left < right){//左边是key,右边先走//带上=,表示找严格大/严格小while (left < right && a[right] >= a[keyi]){right--;}while (left < right && a[left] <= a[keyi]){left++;}swap(&a[left], &a[right]);}//最后left和key换一下swap(&a[left], &a[keyi]);return keyi;
}
注:由于是循环套循环,所以做好边界判断
为什么要找严格大于/小于的?
如果某个用例为[2,2,2,2,2,2];那就完了,会一直死循环
2 挖坑法
由于hoare大佬的这个方法太拉拉了,又给出了第二种(感觉更复杂了,意思差不多)
法二:
1、先将key值存在一个变量里,就会形成一个坑位
2、依然是左边找大,右边找小
右边找小,放到坑里,更新坑的位置
左边再找大,放进坑里,更新坑的位置
3、当left和right相遇,将key填入坑中
按图来看,就是这样


//挖坑法
int partition2(int* a, int left, int right)
{int mid = GetMidNumi(a, left, right);swap(&a[left], &a[mid]);int key = a[left];int hole = left;//此时 left为坑while (left < right){//左边是key,右边先走//带上=,表示找严格大/严格小while (left < right && a[right] >= key){right--;}a[hole] = a[right];hole = right;while (left < right && a[left] <= key){left++;}a[hole] = a[left];hole = left;}//最后坑的位置放kea[left] = key;return hole;
}
3 前后指针法(recommend)
这个方法还是很清爽很简洁的
法三
1、初始时,prev指向序列开头,cur指针指向prev指针后的一个位置,就像这样
2、cur所在位置比key小的,
如果找到了,就++prev,和cur交换
交换完以后,++cur
3 如果cur所在位置比key大
就++cur
在排序过程中,大概就是两种情况
1 prev紧跟着cur
2 prev和cur隔着比key大的一段数的区间
按图来看,就是这样


//前后指针法
int partition3(int* a, int left, int right)
{int mid = GetMidNumi(a, left, right);swap(&a[left], &a[mid]);int keyi = left;int cur = left+1;int prev = left;while (cur <= right){if (a[cur] < a[keyi] && (++prev) < cur)//不要自己和自己换~swap(&a[cur], &a[prev]);++cur;}swap(&a[keyi], &a[prev]);return prev;
}
那么问题来了,我们已经学习了3种单趟排序的方法?
如何实现完整的排序呢?
我们知道,第一波单趟排序排好的是最终key的位置
所以下来的操作非常简单
1 我们对key的左右区间递归使用该函数即可
2 当区间只有一个数/区间不存在时,递归调用结束
有了上一节我们学到的递归的经验,这波函数体就会是这样(以第三种方法为例)
void QuickSort(int* a, int left, int right)
{if (left >= right)return;int keyi = PartSort3(a, left, right);QuickSort(a, left, keyi - 1);QuickSort(a, keyi+1, right);
}
需要注意的是,三种单趟排序对同一序列的结果可能不同,如果有数据结构题目问
单趟排序的结果,我们需要考虑3种方式
4 非递归
非递归的思路也很简单,只要将递归的逻辑用栈代替即可
1、每次将left、right入栈;
2、每次取两次栈顶组成begin、end调用单趟排序
3、将单趟排序返回的keyi左右两端区间的左右顶点入栈
4、当栈为空时,循环结束
//前后指针法
int partition3(int* a, int left, int right)
{int keyi = left;int cur = left + 1;int prev = left;while (cur <= right){if (a[cur] < a[keyi] && (++prev) < cur)//不要自己和自己换~swap(&a[cur], &a[prev]);++cur;}swap(&a[keyi], &a[prev]);return prev;
}void QuickSortNonR(int* a, int left, int right)
{stack<int> st;st.push(left);st.push(right);while (!st.empty()){int end = st.top();st.pop();int begin = st.top();st.pop();int keyi = partition3(a, begin, end);//begin,keyi-1 ; keyi+1,endif (begin < keyi - 1){st.push(begin);st.push(keyi-1);}if ( keyi + 1<end){st.push(keyi+1);st.push(end);}}}int main()
{int arr[] = { 3,5,2,9,8,10,2,2,9,8 };int size = sizeof(arr) / sizeof(arr[0]);QuickSortNonR(arr, 0, size - 1);for (int i = 0; i < size; ++i){printf("%d ", arr[i]);}
}
打印结果

5 优化之三数取中(随机选key)
三数取中其实很简单,由于我们固定选最左边的数为key会有不确定性,所以我们选取
左中右三个数中中间的那个为key
//三数取中
int GetMidNumi(int* a, int left, int right)
{int mid = (right + left) / 2;if (a[left] < a[mid]){if (a[mid] < a[right]){return mid;}//说明mid是最大的else if (a[right] > a[left]){return right;}else{return left;}}else{if (a[mid] > a[right]){return mid;}//说明mid是最小的else if (a[right] > a[left]){return left;}else{return right;}}
}
6 优化之小区间优化
我们注意到,当递归到一定深度时,每次的区间长度不长
但仍需要递归,这就会导致递归次数太多,有栈溢出的风险
所以我们限制一个长度,在此长度以下,我们直接调插入排序
由于是接近有序,所以效率不会太拉还可以减少递归层数
void QuickSort(int* a, int left, int right)
{if (left >= right)return;// 小区间优化--小区间直接使用插入排序if ((right - left + 1) > 10){int keyi = PartSort3(a, left, right);QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);}else{InsertSort(a+left, right - left + 1);}
}
7 优化之三路划分
当你有了以上优化之后,你会发现,你依然无法通过力扣的那道排序题
. - 力扣(LeetCode)
因为有个用例是大量重复的数
这对我们的快排是极为不利的
大量的重复意味着大量的递归+遍历,而每次单趟排序都不能做事
所以有人提出了所谓三路划分
之前我们通过选取key找到大于key/小于key的区间,算的上是一种双路划分
所以三路划分是这样的
单独划分出一个等于key的区间

核心思想是这样的
1、 跟key相等的值,往后推
2、比key小的在左边,比key大的在右边,和key相等的在中间
所以我们的双指针法就变成了三指针法了,

1、a[cur]<key,交换left和cur,left++,cur++
2、a[cur]>key,交换right和cur,right--,cur++
3、a[cur]==key,cur++(只动cur!)
搞定了单趟其他就简单了
我们和之前一样递归即可
简单实现一波
void QuickSort(int* a, int begin, int end)
{if (begin >= end)return;if((end-begin+1)<15){InsertSort(a+begin, end - begin + 1);}// 小区间优化--小区间直接使用插入排序else{int mid=GetMidNumi(a,begin,end);swap(a[begin],a[mid]);int left=begin;int right=end;int key=a[begin];int cur=begin+1;while(cur<=right){if(a[cur]<key){left++;cur++;}else if(a[cur]>key){swap(a[cur],a[right]);--right;}else{cur++;}}QuickSort(a,begin,left-1);QuickSort(a,right+1,end);}
}
有了这个优化,我们就可以通过力扣的排序题啦
8 SGI sort设计
(参考侯捷老师的《STL源码剖析》)
学习了上述优化,我们就可以来评鉴一下C++的算法库中的sort了
总述:STL的sort算法,数据量大时采用QuickSort,分段递归排序,一旦分段后的数据量小于某个门槛,为避免QuickSort的递归调用带来过大的额外负荷,就改用InsertSort
如果递归层数过深,还会改用HeapSort
InsertSort
SGI的InsertSort有两个版本,一种是递增,另一种是仿函数
版本二的可以先忽略,我们重点来看版本一的
这个是插入排序的外循环
template<class RandomAccessIterator>
void __insertion_sort(RandomAccessIterator first,RandomAccessIterator last)
{if(first==last)return ;for(RandomAccessIterator i=first+1;i!=last;++i)//外循环{__liner_insert(first,i,value_type(first));}
}
__linear_insert是这么做的
template<class RandomAccessIterator,class T>
inline void __liner_insert(RandomAccessIterator first,RandomAccessIterator last,T*)
{T value =*last;//记录尾元素//和我们写的不同的是,这里分了两种情况,是否尾比头还小(头为最小元素)if(value<*first){copy_backward(first,last,last+1);//整体后移*first=value;}else{__unguarded_linear_insert(last,value); }
}
这里的__unguarded的命名很讲究,表明这波不用判断是否超过边界(也就是我们所写代码中end>=0的逻辑),
因为我们的源码确保了最小值必然在内循环区间的最边缘
void __unguarded_linear_insert( RandomAccessIterator last,T value)
{RandomAccessIterator next=last;while(value<*next)//内循环的逻辑{*last=*next;last=next;--next;}*last=value;
}
细节满满,看似只是省下了一个简单的判断,但是在大数据量下,影响还是很可观的
QuickSort
SGI的QuickSort提供了两种单趟排序(partioning)分别是我们的单趟1和单趟3
别的都差不多,没什么好说的
threshold
SGI sort还认为,对于一个小数据量序列,甚至简单的插入排序可能更快
鉴于这种情况,适度评估序列的大小,再决定选择插排还是快排,是值得采纳的优化措施
侯捷老师认为并无定论,5~20都有可能,因设备而定
final Insert Sort
SGI也采用了当序列小于一个值就调用插入排序的做法
(在源码中,这个值是16)
const int __stl_threshold =16;
原文:如果我们令某个大小以下的序列滞留在"几近排序但尚未成功"的状态,最后再以
一次InsertSort将所有这些"几近排序但尚未成功"的子序列做一次完整的排序,其效率一般认为会比"将所有子序列彻底排好"更好,这是因为InsertSort在面对"几近排序"的序列时,有更好的表现
introsort
不当的轴承(就是key值)选择,可能导致QuickSort退化为O(n^2),所以David大佬提出了一种混合式排序,内省式排序,简称introsort。在分割行为有二次行为的倾向时,能自动检测,转而使用HeapSort,保住O(n*lgn)的下限
自我检测的函数为__lg,其实就是在找2^k<=n的最大值k
template<class Size>
inline Size __lg(Size n)
{Size k;for(k=0;n>1;n>>=1)++k;return k;
}
所以,最终的sort本体是这样的~
template<class RandomAccessIterator>
inline void sort(RandomAccessIterator first,RandomAccessIterator last)
{__introsort_loop(first,last,value_type(first),__lg(last-first)*2);__final_insertion_sort(first,last);
}
template<class RandomAccessIterator,class T,class Size>
void __introsort_loop(RandomAccessIterator first, RandomAccessIterator last,T* ,Size depth_limit)
{while(last-first>__stl_threshold){if(depth_limit==0){//分裂恶化//调用堆排!partial_sort(first,last,last);return ;}--depth_limit;//下来就是单趟排序+三数取中的逻辑了,这里简化一下,就不写了RandomAccessIterator cut;//递归左右区间调用__introsort_loop,这里也简化一下__introsort_loop();__introsort_loop()}
当__introsort_loop()结束后,[first,last)会存在多个元素大于16的元素
此时回到主函数sort,再进行__final_insertion_sort(first,last);
template<class RandomAccessIterator>
void __final_insertion_sort(RandomAccessIterator first,RandomAccessIterator last)
{if(last-first>__stl_threshold){__insertion_sort(first,first+__stl_threshold);__unguarded_insertion_sort(first+__stl_threshold,last);//就是循环调用__unguarded_linear_insert; }else{__insertion_sort(first,last);}
}
该函数首先判断元素个数是否大于16,如果为否,直接调用__insertion_sort
如果为是,就分割为一段长为16的区间和剩下的区间,分别调用__insertion_sort和——__unguarded__insertion_sort处理
这就是SGI STL sort的故事了,设计非常巧妙(最用心的一集)
9 复杂度分析
根据我们在SGI sort分析的那样,如果是普通快排,时间复杂度一般是O(n*lgn)
但是在极端情况下,会退化至O(n^2)。
但是SGI sort,我们可以保证一个O(n*lgn)的下限
空间复杂度的来源是栈帧的建立,为O(lgn)~O(n)
七、归并排序
归并排序是建立在归并操作上的一种有效的排序算法,采用了经典的分治法。
将已有序的子序列合并,得到完全有序的序列

归并的逻辑很简单,就是我们之前的合并两个有序数组的逻辑最后再拷回原数组即可
1 递归版本
void _merge(int* a, int left, int right,int *tmp)
{//一个值时/区间不存在时结束if (left >= right){return;}int mid = (left+right) / 2;//子区间递归排序_merge(a, left, mid, tmp);_merge(a, mid + 1, right, tmp);//归并int begin1 = left;int end1 = mid;int begin2 = mid + 1;int end2 = right;printf("begin1:%d end1:%d\n", begin1, end1);printf("begin2:%d end2:%d\n", begin2, end2);int i = left;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[i++] = a[begin1++];}else{tmp[i++] = a[begin2++];}}if (begin1 > end1){while (begin2 <= end2){tmp[i++] = a[begin2++];}}if (begin2 > end2){while (begin1 <= end1){tmp[i++] = a[begin1++];}}//记住加left!memcpy(a + left, tmp + left, sizeof(int) * (right - left + 1));
}void MergeSort(int* a, int n)
{int* nums = (int*)malloc(sizeof(int) * n);_merge(a,0, n-1,nums);free(nums);
}int main()
{int arr[] = { 3,5,2,9,8,10,2,2};int size = sizeof(arr) / sizeof(arr[0]);MergeSort(arr, size);for (int i = 0; i < size; ++i){printf("%d ", arr[i]);}
}
2 非递归但梭哈实现法
非递归如何模拟模拟归并的过程呢?
归并的逻辑是先一一归并,再二二归并,在此基础上四四归并,他的逻辑更像一个后序遍历
而栈则对应的是前序遍历,所以这波不能用栈,而可以控制一个gap,实现一一、二二、四四……归并
gap==1,此时为一一归并
gap*=2,就变成了二二归并
……
光这样还不够,我们只考虑了元素个数是二的次方的情况
如果是9,20个数据,end2,begin2,begin1会有越界的风险
所以我们要修正边界
光修正边界还不够,我们要考虑拷回原数组的问题,这里分为一把拷(梭哈)和归一部分拷一部分(不梭哈)两种情况
我们可以打印一波边界来看看情况
我们试试9个元素
//一把梭哈
void MergeSortNonRS(int* a, int n)
{int* nums = (int*)malloc(sizeof(int) * n);int gap = 1;while (gap < n){for (int i = 0; i < n; i += 2 * gap){//归并int begin1 = i;int end1 = i+gap-1;int begin2 = i+gap;int end2 = i+2*gap-1;printf("begin1:%d end1:%d\n", begin1, end1);printf("begin2:%d end2:%d\n", begin2, end2);int j = i;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){nums[j++] = a[begin1++];}else{nums[j++] = a[begin2++];}}if (begin1 > end1){while (begin2 <= end2){nums[j++] = a[begin2++];}}if (begin2 > end2){while (begin1 <= end1){nums[j++] = a[begin1++];}}}//记住加left!memcpy(a , nums, sizeof(int) *n);gap *= 2;}free(nums);
}int main()
{int a[] = { 6,1,2,6,9,3,4,6,10};MergeSortNonRS(a, 9);
}

由于begin1为i,所以不可能越界
下来,越界有这么几种情况
1、end1越了,不归并了,但是是要拷贝的(因为只剩一个数了)
【8,11】【12,15】
由于我们要一次梭哈拷贝,所以这波我们要修正边界,才能不被覆盖
2、end1没越界,begin2越界了
同理,不用归并,也要修正边界
【8,8】【9,9】
3、只有end2越界了
他是需要归并的,修正end2
【0,7】【8,15】
不归并的修正边界很简单,我们只需修成一个不存在的区间,就不进循环了
而最后需要归并的end2,我们需要计算一下修正到的值,即n-1
//一把梭哈
void MergeSortNonRS(int* a, int n)
{int* nums = (int*)malloc(sizeof(int) * n);int gap = 1;while (gap < n){for (int i = 0; i < n; i += 2 * gap){//归并int begin1 = i;int end1 = i+gap-1;int begin2 = i+gap;int end2 = i+2*gap-1;printf("begin1:%d end1:%d\n", begin1, end1);printf("begin2:%d end2:%d\n", begin2, end2);//修正if (end1 >= n){end1 = n - 1;begin2 = n;end2 = n - 1;}else if (begin2 >= n){///修成一个不存在的区间begin2 = n;end2 = n - 1;}else if (end2 >= n){end2 = n - 1;}int j = i;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){nums[j++] = a[begin1++];}else{nums[j++] = a[begin2++];}}if (begin1 > end1){while (begin2 <= end2){nums[j++] = a[begin2++];}}if (begin2 > end2){while (begin1 <= end1){nums[j++] = a[begin1++];}}}//记住加left!memcpy(a , nums, sizeof(int) *n);gap *= 2;}free(nums);
}
3 非递归但不梭哈实现法
好消息是,不拷贝,前两个就可以不用修正边界了,直接break出去
只需修正第二个区间的右边界即可
//归一部分拷一部分
void MergeSortNonR(int* a, int n)
{int* nums = (int*)malloc(sizeof(int) * n);int gap = 1;while (gap < n){for (int i = 0; i < n; i += 2 * gap){//归并int begin1 = i;int end1 = i + gap - 1;int begin2 = i + gap;int end2 = i + 2 * gap - 1;printf("begin1:%d end1:%d\n", begin1, end1);printf("begin2:%d end2:%d\n", begin2, end2);//修正if (end1 >= n || begin2 >= n){break;}else if (end2 >= n){end2 = n - 1;}int j = i;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){nums[j++] = a[begin1++];}else{nums[j++] = a[begin2++];}}if (begin1 > end1){while (begin2 <= end2){nums[j++] = a[begin2++];}}if (begin2 > end2){while (begin1 <= end1){nums[j++] = a[begin1++];}}memcpy(a +i , nums+i, sizeof(int) * (end2 - i + 1));}gap *= 2;}
free(nums);
}
复杂度分析
归并排序的时间复杂度也是标准的O(n*lgn)
空间复杂度来源于开辟的新数组,为O(n)
归并排序还可以用作外排序,大家感兴趣的话,可以再了解一下~
八、其他排序的介绍
计数排序
计数排序又称鸽巢原理,是对哈希直接定址法的应用
操作为:
1、统计相同元素个数
2、根据统计结果将序列回收回原来的序列
既然是模仿哈希,那么原理是这样的
思考一下元素的大致范围,开一个大的哈希表(数组),通过定址法将带牌序列的元素映射至
哈希表,最后再从哈希表提取元素即可
例:
{6,1,2,1,9,3,3,2,2,8}
我们直接开一个大小为10的数组就够了

然后遍历数组,直接定址,再对应下标值加1即可
遍历之后是这样的

下来就简单,遍历哈希表排序即可
看似很简单的计数排序,我们要考虑一些别的问题
如果序列是这样
{100,101,101,103,109,120},我们再从0开始定址就有些浪费了
所以我们可以统计最大,最小值,就能最大程度的节省空间了
需要注意的是,如果序列含有负数,我们的排序也可以解决
那代码是这样的~
void CountSort(int* a, int n)
{int max = a[0], min = a[0];for (int i = 1; i < n; ++i){if (a[i] > max){max = a[i];}if (a[i] < min){min = a[i];}}int range = max - min + 1;int* countA = (int*)malloc(sizeof(int) * range);if (countA == NULL){perror("malloc fail\n");return;}memset(countA, 0, sizeof(int) * range);// 计数for (int i = 0; i < n; i++){countA[a[i] - min]++;}// 排序int j = 0;for (int i = 0; i < range; i++){while (countA[i]--){a[j++] = i + min;}}free(countA);
}int main()
{int arr[] = { 2,10,3,90,589,184,505.29,8,83 };for (auto e : arr){std::cout << e << " ";}std::cout << std ::endl;int sz = sizeof(arr) / sizeof(arr[0]);CountSort(arr, 0, sz);for (auto e : arr){std::cout << e << " ";}return 0;
}
复杂度分析
显然,如果序列内的值相差不太大,计数排序的时间复杂度能逼急O(n),非常逆天
其实是O(n+range)
但是由于我们用到了哈希表,那就意味着你得能哈希出一个值来才能排序
所以浮点数肯定是寄了,但是字符串可以(字符串哈希)。
空间复杂度是O(range)
桶排序
桶排序(Bucket Sort)是一种基于分布的排序算法,特别适用于数据分布比较均匀的情况。它的基本思想是将数据分成若干个桶(Bucket),然后分别对每个桶中的数据进行排序,最后再将各个桶中的数据按顺序合并,得到最终的有序数据。
桶排序的基本步骤:
- 创建桶:
- 根据数据范围创建一定数量的桶,每个桶代表一个区间范围。
- 将元素分配到桶中:
- 遍历输入数据,将每个数据根据其值分配到对应的桶中。
- 对每个桶内的数据排序:
- 对每个桶内的数据单独进行排序(可以使用任何排序算法,通常使用快速排序或插入排序)。
- 合并桶中的数据:
- 最后按顺序将各个桶中的数据合并成一个整体,即完成排序
复杂度分析
时间复杂度:
-
最好情况: O(n+k)O(n + k)O(n+k)
- 如果输入数据均匀分布到桶中,且每个桶内的数据可以使用一个高效的排序算法(如插入排序)来进行排序,桶排序的时间复杂度接近线性,等于 O(n)O(n)O(n)。
- 此外,每个桶的排序时间是与桶内元素数量有关的。如果使用插入排序,最好情况下每个桶的排序复杂度是 O(1)O(1)O(1),总的时间复杂度为 O(n+k)O(n + k)O(n+k),其中 nnn 是输入数据的数量,kkk 是桶的数量。
-
平均情况: O(n+k)O(n + k)O(n+k)
- 当数据大致均匀分布到桶中时,桶排序的平均时间复杂度与最好情况类似,也是 O(n+k)O(n + k)O(n+k),因为分配数据到桶中的过程是线性的。
-
最坏情况: O(n2)O(n^2)O(n2)
- 最坏情况下,所有数据都被分配到同一个桶中,此时桶排序退化为在单个桶中对 nnn 个元素进行排序。若该桶内使用插入排序或其他 O(n2)O(n^2)O(n2) 时间复杂度的排序算法,则总的时间复杂度为 O(n2)O(n^2)O(n2)。
空间复杂度:
- 空间复杂度: O(n+k)
这种排序一般没什么用,大家了解一下即可
基数排序
首先我们要知道,基数排序也是一个和之前不同,即不需要比较、移动的排序
是一种借助一种多关键字的排序对单关键字进行排序的方法
例:
我们的扑克牌就有两种关键字进行排序
一种是花色,♠️,♣️,♦️,♥️
另一种是数字大小,1,2,3,4,5,6,7,8,9,10,J,Q,K
再介绍两个概念
最高位优先(MSD)&最低位优先(LSD)

以扑克牌为例
MSD为:每个子序列花色相同但数字不同
LSD为:4个1,4个2,4个3……
给一个例子

我们以低位优先为例
由于是低位优先,所以我们先按个位排,

将其遍历,重组回原数组(注意,这波063先进,所以先出063)

下来再排十位

排好百位,这波就结束啦

原理还是很简单的,建一个挂队列的哈希表
然后进行数位次操作
每次操作先分发,再组合
写好的代码就是这样的~
#define K 3
#define RADIX 10std::queue<int> Q[RADIX];int GetKey(int value,int k)
{int key = 0;while (k >= 0){key = value % 10;value /= 10;k--;}return key;
}void Distribute(int*arr,int left,int right,int k)
{for (int i = left; i < right; ++i){int key = GetKey(arr[i],k);Q[key].push(arr[i]);}
}void Collect(int*arr)
{int k = 0;for (int i = 0; i < RADIX; ++i){while (!Q[i].empty()){arr[k++] = Q[i].front();Q[i].pop();}}
}void RadixSort(int* arr, int left, int right)
{for (int i = 0; i < K; ++i){//分发Distribute(arr,left,right,i);//组合Collect(arr);}
}int main()
{int arr[] = { 278,10,63,930,589,184,505.269,8,83 };for (auto e : arr){std::cout << e << " ";}std::cout << std ::endl;int sz = sizeof(arr) / sizeof(arr[0]);RadixSort(arr, 0, sz);for (auto e : arr){std::cout << e << " ";}return 0;
}
复杂度分析
假设我们搞了r个队列
那我们的空间复杂度就是O(r)
假设我们要进行k次操作
每次分发和组合的时间复杂度就是(n+r)
所以总体就是O(k(n+r))
九、排序总结
这里我们再介绍一个所谓的稳定性的概念
假定在待排序的记录序列中,存在多个具有相同关键字的记录,若经过排序,这些记录的
相对次序保持不变。
即在原序列中,r[i]=r[j],且r[i]在r[j]之前,在排序之后,如果r[i]仍在r[j]之前,就说明这种排序是稳定的,否则就是不稳定的
说人话就是,假设某个序列有相同的数字,经过排序之后,这些相同的数的顺序不变,就是稳定的
有什么用呢?
假定考试时规定分数相同先交卷的人更牛逼,我们就用得着这个稳定性了
根据这些排序的原理,我们便可推出一个排序是否稳定了
| 排序名 | 是否稳定? | 解释 |
| 插入排序 | 稳定 | 显然,如果规定了相等了不往后 挪即可 |
| 希尔排序 | 不稳定 | 相同的数可能被分到不同的组 |
| 选择排序 | 不稳定 | 每次选到的数不固定 |
| 堆排序 | 不稳定 | 每次作为堆顶的元素不确定,而堆顶之下可能会有重复元素 |
| 冒泡排序 | 稳定 | 相同不交换就可以做到稳定 |
| 快速排序 | 不稳定 | 每次的key值不确定 |
| 归并排序 | 稳定 | |
| 计数排序 | 稳定 | |
| 基数排序 | 稳定 | 辅助数组元素是队列,可以通过先进先出保证 |
总结
做总结,这篇博客我们学习了排序,这将是速通数据结构算法初阶的最后一集,接下来我们将迅速的投入沉淀C++的篇章中
水平有限,还请各位大佬指正。如果觉得对你有帮助的话,还请三连关注一波。希望大家都能拿到心仪的offer哦。

每日gitee侠:今天你交gitee了嘛
相关文章:
速通数据结构与算法第七站 排序
系列文章目录 速通数据结构与算法系列 1 速通数据结构与算法第一站 复杂度 http://t.csdnimg.cn/sxEGF 2 速通数据结构与算法第二站 顺序表 http://t.csdnimg.cn/WVyDb 3 速通数据结构与算法第三站 单链表 http://t.csdnimg.cn/cDpcC 4 速通…...
灵当CRM index.php接口SQL注入漏洞复现 [附POC]
文章目录 灵当CRM index.php接口SQL注入漏洞复现 [附POC]0x01 前言0x02 漏洞描述0x03 影响版本0x04 漏洞环境0x05 漏洞复现1.访问漏洞环境2.构造POC3.复现 0x06 修复建议 灵当CRM index.php接口SQL注入漏洞复现 [附POC] 0x01 前言 免责声明:请勿利用文章内的相关技…...
修复: Flux女生脸不再油光满面, 屁股下巴 -- 超实用Comfyui小技巧
ComfyUI上目前最强画图模型公认为Flux. 初次用Flux基础模型画真实的女生时, 和SD比起来, 会觉得画出来细节更多, 更真实. 但是当画多了, 就会觉得画出来的女生总是似曾相识. 仔细观察, 会发现一些共同的特征. 人偏老气, 像30~50的女生. 改了提示词也效果不大. 颧骨凸起, 嘴…...
Actions Speak Louder than Words Meta史诗级的端到端推荐大模型落地
发现好久之前整理的推荐系统被遗忘在了草稿箱,让它出来见见世面。。。后续空了持续更新 文章目录 1.Background2.Related works2.1 典型推荐模型2.1.1 DIN2.1.2 DIEN2.1.3 SIM2.1.4 MMoE2.1.5 其他 2.2. 生成式推荐 3.Method3.1 统一特征空间3.2 重塑召回排序模型3.…...
金智维KRPA之Excel自动化
Excel自动化操作概述 Excel自动化主要用于帮助各种类型的企业用户实现Excel数据处理自动化,Excel自动化是可以从单元格、列、行或范围中读取数据,向其他电子表格或工作簿写入数据等活动。 通过相关命令,还可以对数据进行排序、进行格式…...
哪款宠物空气净化器能有效去除浮毛?希喂、352实测分享
你是否曾经站在家电卖场里,面对琳琅满目的宠物空气净化器产品而感到无所适从?或者在浏览网上商城时,被海量的参数和功能描述搞得头晕眼花?别担心,你不是一个人。在这个科技飞速发展的时代,选择一台既能满足…...
2024.9.28更换启辰R30汽车火花塞
2024.9.28周六汽车跑了11万公里,实在加速肉,起步顿挫,油耗在8个,决定更换火花塞。第一个火花塞要拆掉进气歧管。第二和第三个可以直接换。打开第二个火花塞一看电极都被打成深坑,针电极都被打凸。我有两个旧的火花塞&a…...
2024上海网站建设公司哪家比较好TOP3
判断一家网建公司的好坏,第一是看公司背景,包括成立时间,工商注册信息等,第二可以去看看建站公司做的案例,例如,网站开发、设计、引流等等的以往案例,了解清楚具体的业务流程。 一、公司背景 …...
TDesign组件库+vue3+ts 如何视觉上合并相同内容的table列?(自定义合并table列)
背景 当table的某一列的某些内容相同时,需要在视觉上合并这一部分的内容为同个单元格 如上图所示,比如需要合并当申请人为同个字段的列。 解决代码 <t-table:data"filteredData":columns"columns":rowspan-and-colspan"…...
BACnet协议-(基于ISO 8802-3 UDP)(2)
1、模拟设备的工具界面如下: 2、使用yet another bacnet explorer 用作服务,用于发现设备,界面如下: 3、通过wireshark 抓包如下: (1)、整体包如下: (2)、m…...
android 根据公历日期准确节气计算年月日时天干地支 四柱八字
1 年柱 判断当前日期是否超过本年的立春 未超过年份-1 已超过按当前年份计算 2月柱 当前日期是否超过当月的第一个节气 未超过-1 超过当前月份计算 节气对日柱时柱没影响。 获取某年某月第一个节气的准确日期 private int sTerm(int y, int n) {int[] sTermInfo…...
VMware虚拟机连接公网,和WindTerm
一、项目名称 vmware虚拟机连接公网和windterm 二、项目背景 需求1:windows物理机,安装了vmware虚拟机,需要访问公网资源,比如云服务商的yum仓库,国内镜像加速站的容器镜像,http/https资源。 需求2…...
游戏盾SDK真的能无视攻击吗
游戏盾SDK真的能无视攻击吗?在当今的互联网环境中,游戏行业蓬勃发展,但同时也面临着日益严峻的安全挑战。DDoS攻击、CC攻击、外挂作弊等恶意行为频发,不仅威胁着游戏的稳定性和公平性,也严重影响了玩家的游戏体验。为了…...
【QT】亲测有效:“生成的目标文件包含了过多的段,超出了编译器或链接器允许的最大数量”错误的解决方案
在使用dlib开发人脸对齐功能时,出现了”生成的目标文件包含了过多的段,超出了编译器或链接器允许的最大数量的错误“。 主要功能代码如下: #include <QApplication> #include <QImage> #include <QDebug>#include <dlib…...
什么是 Apache Ingress
Apache Ingress 主要用于管理来自外部的 HTTP 和 HTTPS 流量,并将其路由到合适的 Kubernetes 服务。 容器化与 Kubernetes 是现代云原生应用程序的基础。Kubernetes 的主要职责是管理容器集群,确保它们的高可用性和可扩展性,同时还提供自动化…...
SpringBoot助力墙绘艺术市场创新
3 系统分析 当用户确定开发一款程序时,是需要遵循下面的顺序进行工作,概括为:系统分析–>系统设计–>系统开发–>系统测试,无论这个过程是否有变更或者迭代,都是按照这样的顺序开展工作的。系统分析就是分析系…...
Antlr的使用
概念 ANTLR(ANother Tool for Language Recognition)是一个强大的解析器生成工具,用于读取、处理、执行或翻译结构化文本或二进制文件。ANTLR通过定义文法(grammar)来识别、构建和访问语言中的元素。 ANTLR为包括Jav…...
HealChat心理大语言模型 丨OPENAIGC开发者大赛高校组AI创作力奖
在第二届拯救者杯OPENAIGC开发者大赛中,涌现出一批技术突出、创意卓越的作品。为了让这些优秀项目被更多人看到,我们特意开设了优秀作品报道专栏,旨在展示其独特之处和开发者的精彩故事。 无论您是技术专家还是爱好者,希望能带给…...
PyQt5整合爬虫制作图片爬取器-幽络源
前言 本篇教程适合对Python爬虫和Python软件制作感兴趣的小伙伴阅读,看完本篇教程,你将能更深入了解PyQt5与实际功能的整合方式。 1.设计界面 首先在pycharm中创建一个新目录,这里我建立的目录名为爬图片,然后按如图打开Qt设计…...
DC00023基于jsp+MySQL新生报到管理系统
1、项目功能演示 DC00023基于jsp新生报到管理系统java webMySQL新生管理系统 2、项目功能描述 基于jspMySQL新生报到管理系统项目分为学生、辅导员、财务处和系统管理员四个角色。 2.1 学生功能 1、系统登录 2、校园新闻、报到流程、学校简介、在线留言、校园风光、入校须知…...
web vue 项目 Docker化部署
Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段: 构建阶段(Build Stage):…...
Unity3D中Gfx.WaitForPresent优化方案
前言 在Unity中,Gfx.WaitForPresent占用CPU过高通常表示主线程在等待GPU完成渲染(即CPU被阻塞),这表明存在GPU瓶颈或垂直同步/帧率设置问题。以下是系统的优化方案: 对惹,这里有一个游戏开发交流小组&…...
转转集团旗下首家二手多品类循环仓店“超级转转”开业
6月9日,国内领先的循环经济企业转转集团旗下首家二手多品类循环仓店“超级转转”正式开业。 转转集团创始人兼CEO黄炜、转转循环时尚发起人朱珠、转转集团COO兼红布林CEO胡伟琨、王府井集团副总裁祝捷等出席了开业剪彩仪式。 据「TMT星球」了解,“超级…...
NFT模式:数字资产确权与链游经济系统构建
NFT模式:数字资产确权与链游经济系统构建 ——从技术架构到可持续生态的范式革命 一、确权技术革新:构建可信数字资产基石 1. 区块链底层架构的进化 跨链互操作协议:基于LayerZero协议实现以太坊、Solana等公链资产互通,通过零知…...
在鸿蒙HarmonyOS 5中使用DevEco Studio实现录音机应用
1. 项目配置与权限设置 1.1 配置module.json5 {"module": {"requestPermissions": [{"name": "ohos.permission.MICROPHONE","reason": "录音需要麦克风权限"},{"name": "ohos.permission.WRITE…...
【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分
一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计,提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合:各模块职责清晰,便于独立开发…...
【数据分析】R版IntelliGenes用于生物标志物发现的可解释机器学习
禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍流程步骤1. 输入数据2. 特征选择3. 模型训练4. I-Genes 评分计算5. 输出结果 IntelliGenesR 安装包1. 特征选择2. 模型训练和评估3. I-Genes 评分计…...
安卓基础(aar)
重新设置java21的环境,临时设置 $env:JAVA_HOME "D:\Android Studio\jbr" 查看当前环境变量 JAVA_HOME 的值 echo $env:JAVA_HOME 构建ARR文件 ./gradlew :private-lib:assembleRelease 目录是这样的: MyApp/ ├── app/ …...
算法岗面试经验分享-大模型篇
文章目录 A 基础语言模型A.1 TransformerA.2 Bert B 大语言模型结构B.1 GPTB.2 LLamaB.3 ChatGLMB.4 Qwen C 大语言模型微调C.1 Fine-tuningC.2 Adapter-tuningC.3 Prefix-tuningC.4 P-tuningC.5 LoRA A 基础语言模型 A.1 Transformer (1)资源 论文&a…...
Fabric V2.5 通用溯源系统——增加图片上传与下载功能
fabric-trace项目在发布一年后,部署量已突破1000次,为支持更多场景,现新增支持图片信息上链,本文对图片上传、下载功能代码进行梳理,包含智能合约、后端、前端部分。 一、智能合约修改 为了增加图片信息上链溯源,需要对底层数据结构进行修改,在此对智能合约中的农产品数…...

