分析任何工程,小到一把小刀:knife:,大到一座大桥:bridge_at_night:。无一都是先从整体结构进行剖析,逐个击破来认识。今天,我们就从数组合约的解读启航吧!!!
启航前的一些前提条件:
- 具备高级语言的相关基础知识,C/C++,java,python,go,c#等;
- 需要了解一些简单的数据结构算法;
- 需要有一些基本的solidity语法知识。
我们就像读一篇优秀的文章一样,先来认识一下它的结构:
总共三个4个文件,演示用例2个,核心实现文件2个。依赖关系如下:
从文件名上区分出:一个是一维数组的相关实现,一个是二维数组的相关实现。
pragma solidity ^0.4.25;
import "./LibSafeMathForUint256Utils.sol";
library LibArrayForUint256Utils {
//在array数组里,二分搜索出key(查询)。
function binarySearch(uint256[] storage array, uint256 key) internal view returns (bool, uint) {
if(array.length == 0){
return (false, 0);
}
uint256 low = 0;
uint256 high = array.length-1;
while(low <= high){
uint256 mid = LibSafeMathForUint256Utils.average(low, high);
if(array[mid] == key){
return (true, mid);
}else if (array[mid] > key) {
high = mid - 1;
} else {
low = mid + 1;
}
}
return (false, 0);
}
//在array数组里,寻找出key出现的第一个位置【从前往后】
function firstIndexOf(uint256[] storage array, uint256 key) internal view returns (bool, uint256) {
if(array.length == 0){
return (false, 0);
}
for(uint256 i = 0; i < array.length; i++){
if(array[i] == key){
return (true, i);
}
}
return (false, 0);
}
//将array数组进行反转。
function reverse(uint256[] storage array) internal {
uint256 temp;
for (uint i = 0; i < array.length / 2; i++) {
temp = array[i];
array[i] = array[array.length - 1 - i];
array[array.length - 1 - i] = temp;
}
}
//数组a与数组b是否相等。
function equals(uint256[] storage a, uint256[] storage b) internal view returns (bool){
if(a.length != b.length){
return false;
}
for(uint256 i = 0; i < a.length; i++){
if(a[i] != b[i]){
return false;
}
}
return true;
}
//在数组array中,移除掉index上对应的元素【硬删除】
function removeByIndex(uint256[] storage array, uint index) internal{
require(index < array.length, "ArrayForUint256: index out of bounds");
while (index < array.length - 1) {
array[index] = array[index + 1];
index++;
}
array.length--;
}
//在数组array中,移除掉元素为value的【硬删除】
function removeByValue(uint256[] storage array, uint256 value) internal{
uint index;
bool isIn;
(isIn, index) = firstIndexOf(array, value);
if(isIn){
removeByIndex(array, index);
}
}
//在数组的末尾追加元素value。
function addValue(uint256[] storage array, uint256 value) internal{
uint index;
bool isIn;
(isIn, index) = firstIndexOf(array, value);
if(!isIn){
array.push(value);
}
}
//复制数组b里面的所有元素到a数组里边。
function extend(uint256[] storage a, uint256[] storage b) internal {
if(b.length != 0){
for(uint i = 0; i < b.length; i++){
a.push(b[i]);
}
}
}
//去除array数组里边的重复元素
function distinct(uint256[] storage array) internal returns (uint256 length) {
bool contains;
uint index;
for (uint i = 0; i < array.length; i++) {
contains = false;
index = 0;
uint j = i+1;
for(;j < array.length; j++){
if(array[j] == array[i]){
contains =true;
index = i;
break;
}
}
if (contains) {
for (j = index; j < array.length - 1; j++){
array[j] = array[j + 1];
}
array.length--;
i--;
}
}
length = array.length;
}
//对数组进行快排。
function qsort(uint256[] storage array) internal {
qsort(array, 0, array.length-1);
}
function qsort(uint256[] storage array, uint256 begin, uint256 end) private {
if(begin >= end || end == uint256(-1)) return;
uint256 pivot = array[end];
uint256 store = begin;
uint256 i = begin;
for(;i<end;i++){
if(array[i] < pivot){
uint256 tmp = array[i];
array[i] = array[store];
array[store] = tmp;
store++;
}
}
array[end] = array[store];
array[store] = pivot;
qsort(array, begin, store-1);
qsort(array, store+1, end);
}
//找出数组中最大的元素
function max(uint256[] storage array) internal view returns (uint256 maxValue, uint256 maxIndex) {
maxValue = array[0];
maxIndex = 0;
for(uint256 i = 0;i < array.length;i++){
if(array[i] > maxValue){
maxValue = array[i];
maxIndex = i;
}
}
}
//找出数组中最小的元素
function min(uint256[] storage array) internal view returns (uint256 minValue, uint256 minIndex) {
minValue = array[0];
minIndex = 0;
for(uint256 i = 0;i < array.length;i++){
if(array[i] < minValue){
minValue = array[i];
minIndex = i;
}
}
}
}
我们先将这些函数列举出来:
- binarySearch(uint256[] storage array, uint256 key) //在array数组里,二分搜索出key(查询)。
- firstIndexOf(uint256[] storage array, uint256 key) //在array数组里,寻找出key出现的第一个位置【从前往后】
- reverse(uint256[] storage array)//将array数组进行反转。
- equals(uint256[] storage a, uint256[] storage b)//数组a与数组b是否相等。
- removeByIndex(uint256[] storage array, uint index)
- addValue(uint256[] storage array, uint256 value)
- extend(uint256[] storage a, uint256[] storage b)
- distinct(uint256[] storage array)
- qsort(uint256[] storage array)
- qsort(uint256[] storage array, uint256 begin, uint256 end) private
- max(uint256[] storage array)
- min(uint256[] storage array)
首先,讲解一下这个binarySearch(uint256[] storage array, uint256 key)
,参数需要一个数组和一个目标值key,数组类型属性被storage修饰,它的意思是接受一个存储状态变量(链上变量)。大白话就是,这个变量是一个指针(引用),可以对他进行修改。
关于二分法,分为两种。一种是上述代码所描述的查找,另一种是二分出来的边界。对于后者,这个属于算法方面的,在这里不去对它进行论述,我们讲解前者即二分查找。
二分查找一个最基本的前提是,要求数组里面的元素是按照升序,降序的规则已经预处理好的。然后不断的求其中间下标的元素与目标key值进行比较,来看之间的大小关系,按照升序/降序的规则,来逐个砍掉另一半(不满足要求的,换言之,在这里永远找不到目标key),直到找到目标key为止。
给一张图片,帮助理解下:
红色的方块,就是我们要找的key值,我们趁着茶还没凉,对这一块的代码进行讲解。
function binarySearch(uint256[] storage array, uint256 key) internal view returns (bool, uint) {
if(array.length == 0){
return (false, 0);
}
uint256 low = 0;
uint256 high = array.length-1;
while(low <= high){
uint256 mid = LibSafeMathForUint256Utils.average(low, high);
if(array[mid] == key){
return (true, mid);
}else if (array[mid] > key) {
high = mid - 1;
} else {
low = mid + 1;
}
}
return (false, 0);
}
- 长度是否为0的判断,这个表明了数组array里边是否存在元素;
- 定义了2个变量(哨兵)low和high,两者都从数组的两端开始;
- 在while循环中,我们要保证两个哨兵不能错位。
- 求出两个哨兵的中间值,作为low或high的下一个位置;
- 判断它们与key之间的关系,这个决定了low和high的位置,mid+1,mid-1就是说array[mid]这个值不符合要求了,需要将它从里边剔除掉,还有一个因素是,由于数组是从0开始的,mid-1的操作,并不会把array[mid-1]这个位置上符合key的元素值剔除。
- 直到找到为止。
时间复杂度和空间复杂度这方面,做个简要的分析吧,因为对智能合约来说也相当重要
时间复杂度:由于每次都是折半操作,均摊下来的话,就是一个二叉搜索树,时间复杂度上是O(log n)【n=array.length】;
空间复杂度:这里没有涉及到额外空间(memory创建的数组等)即O(1)。
function firstIndexOf(uint256[] storage array, uint256 key) internal view returns (bool, uint256) {
if(array.length == 0){
return (false, 0);
}
for(uint256 i = 0; i < array.length; i++){
if(array[i] == key){
return (true, i);
}
}
return (false, 0);
}
对于这块代码,我想都不陌生。
- 检测数组的长度是否!=0;
- 循环比较,找到与key相等的,返回目标索引。
//将array数组进行反转。
function reverse(uint256[] storage array) internal {
uint256 temp;
for (uint i = 0; i < array.length / 2; i++) {
temp = array[i];
array[i] = array[array.length - 1 - i];
array[array.length - 1 - i] = temp;
}
}
这段代码就是把数组做了个反转,[1,3,2,6]反转为[6,2,3,1]
-
一个临时变量temp,用于做交换的备份变量。
-
从arrary.length/2中间的位置开始,这个其实就是数组中间位置反转之后是不会变化的。那么/2操作,在数组长度为偶数的情况下,就会有疑惑,这个疑惑可以不必在意,且听我说。
-
奇数情况:/2的情况下,由于长度为uint,那么它就会下取整,比如长度为5的数组[1,3,2,6,0],下取整的下标就是2,刚好是中间位置。在这里,array.length/2刚好到[5]也就停止了,并且完成了交换的操作。
-
偶数情况:/2后,刚好会落到数组中间位置右边的元素,比如[1,3,2,6],那就是[2],在数组的中间线后边。对于这种情况,还没完成呢,就把[x,3,2,x]中的[3,2]交换为[2,3],然后i到达array.length/2停止了,这是刚刚好的。
我们设mid=array.length/2,存在一个i和j(array.length - 1 - i)在mid的两边,交换需要保证i≤mid≤j
在进行交换时,有i=array.length-1-i,mid-i = (array.length-1-i)-mid = 0,且(mid-i>=0,j-mid>=0)这个前提,对于数组长度为偶数的情况,有mid-i<0,j-mid<0这个情况。做个不等式的转换有:mid<i ,j>mid,与i≤mid≤j矛盾,因此,不存在它俩错位交换的情况。
-
时间复杂度:O(n/2)
空间复杂度:O(1)
function equals(uint256[] storage a, uint256[] storage b) internal view returns (bool){
if(a.length != b.length){
return false;
}
for(uint256 i = 0; i < a.length; i++){
if(a[i] != b[i]){
return false;
}
}
return true;
}
- 两个数组长度不同,必然不相等;
- 两个数组在长度相同的条件下,存在一个元素不同,不然不同。
function removeByIndex(uint256[] storage array, uint index) internal{
require(index < array.length, "ArrayForUint256: index out of bounds");
while (index < array.length - 1) {
array[index] = array[index + 1];
index++;
}
array.length--;
}
- index确保在数组长度的范围之内;
- 将index后边的元素逐个覆盖前边的,最后长度-1,达到移除;
function removeByValue(uint256[] storage array, uint256 value) internal{
uint index;
bool isIn;
(isIn, index) = firstIndexOf(array, value);
if(isIn){
removeByIndex(array, index);
}
}
- 调用了
firstIndexOf()
函数查找value的位置,最后调用removeByIndex()
传入下标移除。
function addValue(uint256[] storage array, uint256 value) internal{
uint index;
bool isIn;
(isIn, index) = firstIndexOf(array, value);
if(!isIn){
array.push(value);
}
}
- 调用了
firstIndexOf()
查找出元素出现的位置,以及是否出现isIn; - 根据
isIn
是否为假(value不存在)来决定往array中push新的元素。
function extend(uint256[] storage a, uint256[] storage b) internal {
if(b.length != 0){
for(uint i = 0; i < b.length; i++){
a.push(b[i]);
}
}
}
- 确保数组b在存在元素的情况下,将b数组中的每一个都放入到a中。
function distinct(uint256[] storage array) internal returns (uint256 length) {
bool contains;
uint index;
for (uint i = 0; i < array.length; i++) {
contains = false;
index = 0;
uint j = i+1;
for(;j < array.length; j++){
if(array[j] == array[i]){
contains =true;
index = i;
break;
}
}
if (contains) {
for (j = index; j < array.length - 1; j++){
array[j] = array[j + 1];
}
array.length--;
i--;
}
}
length = array.length;
}
-
i与j错开,即j在i的后边,然后判断array[i]与array[j]的元素是否相同;
-
将contains标记设为true,并将这个array[j]与array[i]相同的元素,从j+1开始往前覆盖,之后长度-1,i-1因为当前array[i]可能是一个新的元素;
-
最后返回去重后的数组长度。
function qsort(uint256[] storage array, uint256 begin, uint256 end) private {
if(begin >= end || end == uint256(-1)) return;
uint256 pivot = array[end];
uint256 store = begin;
uint256 i = begin;
for(;i<end;i++){
if(array[i] < pivot){
uint256 tmp = array[i];
array[i] = array[store];
array[store] = tmp;
store++;
}
}
array[end] = array[store];
array[store] = pivot;
qsort(array, begin, store-1);
qsort(array, store+1, end);
}
快排是排序算法的一种,思想就是利用了递归的思想。
从图片上看到key=2作为轴值,不断的与low(begin)和high(end)进行对比,然后按照规则把它放到对应的位置,并移动该位置上的下标,重合时,就是key=2的合适位置。
- 首先确保给定的begin和end是否满足条件,如果不满足,这个数组默认是不合法的,因为我们后续都要在这两个变量上做手脚;
- pivot备份了最后一个元素的值,i和store两个从begin开始的变量;
- 在for循环中确保没有达到end边界处的条件下,判断array[i]值是否小于当前阶段的目标值pivot。如果小于就对其进行交换,大于的话则不交换,在经过一轮循环之后,
store
会找到一个轴值pivot在当前阶段所在的合适位置,并把array[store]之前的值放在pivot所在的位置; - 之后对它们的两边进行递归操作,直到把所有的array[store]找到合适的位置即完成排序。
function max(uint256[] storage array) internal view returns (uint256 maxValue, uint256 maxIndex) {
maxValue = array[0];
maxIndex = 0;
for(uint256 i = 0;i < array.length;i++){
if(array[i] > maxValue){
maxValue = array[i];
maxIndex = i;
}
}
}
- 经过一轮循环,将找到一个最大值,并返回对应的下标。
function min(uint256[] storage array) internal view returns (uint256 minValue, uint256 minIndex) {
minValue = array[0];
minIndex = 0;
for(uint256 i = 0;i < array.length;i++){
if(array[i] < minValue){
minValue = array[i];
minIndex = i;
}
}
}
- 经过一轮循环,将找到一个最小值,并返回对应的下标。
到这里就结束了,关于参数为storage,就是避免了在传参过程中solidity的虚拟机EVM对数组进行复制,产生不必要的时间消耗和空间占用。
趁热打铁,我们将难度提上一提,讲解下二维数组库合约的实现。
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.4.25;
/**
* @author wpzczbyqy <[email protected]>
* @title uint256类型 二维数组操作
* 提供二维数组的操作,增加新元素,删除元素,修改值,查找值,合并扩展数组等
**/
library Lib2DArrayForUint256 {
/**
*@dev 二维数组中增加一个数组元素
*@param array uint256类型二维数组
*@param value uint256类型一维数组
**/
function addValue(uint256[][] storage array,uint256[] memory value) internal{
require(value.length > 0, "Empty Array: can not add empty array");
array.push(value);
}
/**
*@dev 查找二维数组中指定位置的值
*@param array uint256类型二维数组
*@param row 值所在的行
*@param col 值所在的列
*@return uint256 返回查找的值
**/
function getValue(uint256[][] storage array, uint256 row, uint256 col) internal view returns (uint256) {
if(array.length == 0){
return 0;
}
require(row < array.length,"Row: index out of bounds");
require(col < array[row].length, "Col: index out of bounds");
return array[row][col];
}
/**
*@dev 修改二维数组中指定位置的值
*@param array uint256类型二维数组
*@param row 值所在的行
*@param col 值所在的列
*@param val 修改指定位置的值为val
*@return uint256[][] 返回修改后的数组
**/
function setValue(uint256[][] storage array, uint256 row, uint256 col, uint256 val) internal returns (uint256[][] memory) {
require(array.length > 0, "Array is empty");
require(row < array.length, "Row: index out of bounds");
require(col < array[row].length, "Col: index out of bounds");
array[row][col] = val;
return array;
}
/**
*@dev 查找二维数组array第一个值为val的元素索引
*@param array uint256类型二维数组
*@param val 要查找的值
*@return bool 是否查找得到
*@return uint256 找到的元素一维下标
*@return uint256 找到的元素二维下标
**/
function firstIndexOf(uint256[][] storage array, uint256 val) internal view returns (bool, uint256, uint256) {
uint256 row;
uint256 col;
if (array.length == 0) {
return (false, 0, 0);
}
for(uint256 i = 0; i < array.length; i++) {
for(uint256 j = 0; j < array[i].length; j++) {
if(array[i][j] == val){
row = i;
col = j;
return (true, row, col);
}
}
}
return (false, 0, 0);
}
/**
*@dev 删除二维数组中的某个数组元素
*@param array uint256类型二维数组
*@param index 删除第index个数组元素
*@return uint256[][] 返回修改后的数组
**/
function removeByIndex(uint256[][] storage array, uint256 index) internal returns(uint256[][] memory) {
require(index < array.length, "Index: index out of bounds");
uint256 lastIndex = array.length - 1;
for (uint256 i = index; i < lastIndex; i++) {
array[i] = array[i + 1];
}
// 删除数组的最后一个元素
// solidity 0.6.0以下
// delete array[lastIndex];
// array.length --;
// solidity 0.6.0及以上(0.6.0时动态数组的长度是只读的,并且数组引入了pop()方法)
array.pop();
return array;
}
/**
*@dev 合并两个二维数组
*@param array1 uint256类型二维数组
*@param array2 uint256类型二维数组
*@return uint256[][] 返回合并后的数组
**/
function extend(uint256[][] storage array1, uint256[][] storage array2) internal returns(uint256[][] memory){
require(array2.length > 0, "Extend: can not extend empty array");
for(uint256 i = 0; i < array2.length; i++){
array1.push(array2[i]);
}
return array1;
}
}
还是同样的,我们先把库函数的合约函数列举一下:
- addValue(uint256[][] storage array,uint256[] memory value)
- getValue(uint256[][] storage array, uint256 row, uint256 col)
- setValue(uint256[][] storage array, uint256 row, uint256 col, uint256 val)
- firstIndexOf(uint256[][] storage array, uint256 val)
- removeByIndex(uint256[][] storage array, uint256 index)
- extend(uint256[][] storage array1, uint256[][] storage array2)
function addValue(uint256[][] storage array,uint256[] memory value) internal{
require(value.length > 0, "Empty Array: can not add empty array");
array.push(value);
}
- 确保“新盖的一层楼是实实在在的”;
- 将其动态押入到array里边去。
function getValue(uint256[][] storage array, uint256 row, uint256 col) internal view returns (uint256) {
if(array.length == 0){
return 0;
}
require(row < array.length,"Row: index out of bounds");
require(col < array[row].length, "Col: index out of bounds");
return array[row][col];
}
- 做好相关的安全性检查,返回对应的行列值。
function setValue(uint256[][] storage array, uint256 row, uint256 col, uint256 val) internal returns (uint256[][] memory) {
require(array.length > 0, "Array is empty");
require(row < array.length, "Row: index out of bounds");
require(col < array[row].length, "Col: index out of bounds");
array[row][col] = val;
return array;
}
- 做好相关的安全性检查,设置对应的行列值。
function firstIndexOf(uint256[][] storage array, uint256 val) internal view returns (bool, uint256, uint256) {
uint256 row;
uint256 col;
if (array.length == 0) {
return (false, 0, 0);
}
for(uint256 i = 0; i < array.length; i++) {
for(uint256 j = 0; j < array[i].length; j++) {
if(array[i][j] == val){
row = i;
col = j;
return (true, row, col);
}
}
}
return (false, 0, 0);
}
- array.length==0的判断是确保数组array里边非空;
- 逐行逐列的对二维数组进行值搜索;
- 返回目标。
function removeByIndex(uint256[][] storage array, uint256 index) internal returns(uint256[][] memory) {
require(index < array.length, "Index: index out of bounds");
uint256 lastIndex = array.length - 1;
for (uint256 i = index; i < lastIndex; i++) {
array[i] = array[i + 1];
}
array.pop();
return array;
}
- 确保删除的子数组下标在array.length范围内,之后后者覆盖前者,达到删除目的。
function extend(uint256[][] storage array1, uint256[][] storage array2) internal returns(uint256[][] memory){
require(array2.length > 0, "Extend: can not extend empty array");
for(uint256 i = 0; i < array2.length; i++){
array1.push(array2[i]);
}
return array1;
}
- 在检查待合并的数组不是空数组合法后,将array2合并到array1中。