• [技术干货] NumPy-ndarray 的数据类型用法说明【】
    ndarray 的数据类型数据类型,即 dtype ,也是一个特殊的对象, 它包含了ndarray需要为某一种类型数据所申明的内存块信息(也成为了元数据,即表示数据的数据)dtype是NumPy能够与琪他系统数据灵活交互的原因。通常,其他系统提供一个硬盘或内存与数据的对应关系,使得利用C或Fortran等底层语言读写数据变得十分方便。名称描述bool_布尔型数据类型(True 或者 False)int_默认的整数类型(类似于 C 语言中的 long,int32 或 int64)intc与 C 的 int 类型一样,一般是 int32 或 int 64intp用于索引的整数类型(类似于 C 的 ssize_t,一般情况下仍然是 int32 或 int64)int8字节(-128 to 127)int16整数(-32768 to 32767)int32整数(-2147483648 to 2147483647)int64整数(-9223372036854775808 to 9223372036854775807)uint8无符号整数(0 to 255)uint16无符号整数(0 to 65535)uint32无符号整数(0 to 4294967295)uint64无符号整数(0 to 18446744073709551615)float_float64 类型的简写float16半精度浮点数,包括:1 个符号位,5 个指数位,10 个尾数位float32单精度浮点数,包括:1 个符号位,8 个指数位,23 个尾数位float64双精度浮点数,包括:1 个符号位,11 个指数位,52 个尾数位complex_complex128 类型的简写,即 128 位复数complex64复数,表示双 32 位浮点数(实数部分和虚数部分)complex128复数,表示双 64 位浮点数(实数部分和虚数部分)使用astype方法来显式的转换数组的数据类型123456arr = np.array([1,2,3,4,5])print(arr.dtype)print(arr)float_arr = arr.astype('float32')#也可以写作 arr.astype(np.float32)print(float_arr.dtype)print(float_arr)int32 [1 2 3 4 5] float32 [1. 2. 3. 4. 5.]注意:将内容为数字的字符串数组转为数字是可以的,当内容是浮点型数字的时候只能转成 float,不能 int,只有是整数的时候才可以转成int用其他数组的dtype来转换数据类型:123456int_arr = np.arange(10)calibers = np.array([.22, .270, .357], dtype=np.float64)print(calibers)arr_last = int_arr.astype(calibers.dtype)print(arr_last.dtype)print(arr_last)[0.22 0.27 0.357] float64 [0. 1. 2. 3. 4. 5. 6. 7. 8. 9.]补充:Python3:numpy的简单使用(ndarray的基本属性以及基本生成数组的方法)声明由于本人学习需要,所以开始学习numpy,这个科学计算工具,本文用于复习当前所学习的内容(当前使用numpy的版本为:1.17.4)1.ndarray的基本的属性2.生成数组的方法(主要测试生成0和生成1的方法:ones和zeros方法)1. 输出当前ndarray的基本属性1234567891011121314151617181920212223# 测试当前Numpy中的narray中的属性# 使用的numpy的版本为:1.17.4import numpy as np default_array = [[1, 2, 3, 4, 5, 6],                 [1, 2, 3, 4, 5, 6]]np_array = np.array(default_array)print("当前存储后的数据的dtype类型为:{}".format(np_array.dtype))  # int32print("查看这个对象的实际类型:{}".format(type(np_array)))  #print("查看这个对象的形状:{}".format(np_array.shape))  # (2,6)print("当前这个对象的字节长度:{}".format(np_array.itemsize))  # 4print("当前这个对象的长度(使用python的len方法):{}".format(len(np_array)))  # 2 只迭代了一组数据外层的二维数据print("当前这个对象的长度(使用自己的size方法):{}".format(np_array.size))  # 获取了所有的数据的数量 print(np.array([[1, 2, 3], [1, 2, 3]]).dtype)print(np.array([1.2, 2.2, 3.2]).dtype) # 可以看出当前默认使用的类型为int32# 默认使用的浮点类型为:float64 # 修改和设定当前的使用的初始化类型# print(np.array([[1.1,1.2,1.3]],dtype="int32").dtype)print(np.array([[1.1,1.2,1.3]],dtype=np.int32).dtype)结果:总结:1.创建了二维数据的时候使用原生的python的len方法获取的长度是外层的长度,并不是二维数组实际内容的长度!2.通过np.array(数组)将原来的python中的数组转换为ndarray类型的数据3.每一个ndarray中都会有一个数据类型使用dtype表示,默认使用的整数类型为int32,浮点类型为float644.通过ndarray.size获取当前ndarray中的元素的个数5.通过ndarray.shap获取当前的ndarray的形状6.使用np.array()创建ndarray的时候可以指定当前的ndarray的dtype,其形式可以是字符也可以是np.类型2.使用numpy生成简单的数组(np.zeros(),np.ones(),np.empty(),np.array())123456789101112131415161718192021# 使用numpy中的生成的数组数据的方法import numpy as np # 生成1的操作np_array = np.zeros([2, 2])print("当前生成的数据为:{}".format(np_array))print("输出当前生成的数据的类型为:{}".format(np_array.dtype)) # 说明当前默认产生的数据数据的类型为float64# 现在改变当前的dtype,直接将当前的dtype的数据类型设置为int32np_array.dtype = np.int32print("当前生成的数据为:{}".format(np_array))print("输出当前生成的数据的类型为:{}".format(np_array.dtype)) # 生成1的数据np_array_ones = np.ones([2, 2], dtype=np.int32)print(np_array_ones) # 创建一个未初始化的数据,默认未初始化x = np.empty([3, 2], dtype=int)print(x)结果:总结:1.使用当前的np.zeros(shape)和np.ones(shape)方法生成全是0或者全是1的指定形状的数组2.通过np.empty(shape)生成空的数组3.可以通过ndarray.dtype=dtype方式改变当前的ndarray的类型3.使用生成数组方式二(np.asarray(),np.copy())123456789101112# 从已有的数组中创建数据import numpy as np default_array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]default_tuple = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)print(type(default_tuple))copy_array = np.asarray(default_array)  # 用于浅拷贝copy_tuple = np.asarray(default_tuple)print("asarray数组后的数据:{}".format(copy_array))print("asarray元组后的数据:{}".format(copy_tuple))deep_copy_array = np.copy(default_array)print("copy数组后的数据:{}".format(deep_copy_array))总结:1.这里使用np.asarray()方法生成的数组与原来的数组有关联,是浅拷贝2.这里的np.copy()方法生成的另外一份备份数据,是深拷贝4.生成指定范围的数组(np.range(),np.random.random(),np.random.randint(),np.linspace())123456789101112131415# 通过固定范围生成数组,使用arange方式生成0 到 9之间的数据,默认生成的数据为当前的为范围的值,这里的步长默认就是1,结果中不包含10,这里是按照指定的步长进行迭代range_array = np.arange(0, 10, dtype=np.int32)print("range_array:{}".format(range_array)) # 通过随机方式生成数组random_array = np.random.random((2, 2))print("使用随机方式生成数组:{}".format(random_array))  # 默认生成的数据为0到1之间的数据 # 2 生成随机的整数random_array_int = np.random.randint(1, 10, (2, 2))print("生成随机整数:{}".format(random_array_int)) # 在指定范围中生成15 个 1到 10之间的数,这是一个随机的数据,是等距离的,当要求的数据超过当前的范围的数据的时候默认就会随机生成一些数据listspace_array = np.linspace(1, 10, 15, dtype=np.int32)  # 就是按照一定的等分进行划分为指定个需要的数据,这里的结果中包含10,相当于当前的等差数列一样print("listspace_array:{}".format(listspace_array))结果:总结:1.当前的random方法就是随机生成指定区间的数据,可以指定类型2.range就是相当于当前的python中的range方法,可以指定步长,是一个[a,b)这中数据3.linspace用于在指定的范围中按照指定的方式生成数组,这个是等差数列,如果当前需要的数据大于这个范围就会出现随机生成的情况5.生成等比数列(np.logspace())123# 生成一个等比的数列,这里面的2 表示生成的样本的个数为2 ,起始数据为1,结束数据为4,表示最小为3的1次方到当前的3的4次方equal_ratio_array = np.logspace(1, 4, 2, dtype=np.int32)  # 这里的默认的底数为10 表示当前最小为10的一次方,最大为当前的10的4次方print("当前的等比数列的数据为:{}".format(equal_ratio_array))当前的等比数列的数据为:[ 10 10000]总结1.这个等比具有默认的底数为10,第一个表示10的1次方,第二个为生成数的最大次方为10的4次方,生成的数据2表示当前生成的等比数组的长度为22.可以设定当前的底数值,可以指定当前的类型6.总结1.当前的numpy这个模块可以实现创建当前的数组,可以生成指定类型和指定形状的数组2.通过numpy可以模拟需要的数据,产生数的方式很快!
  • [技术干货] 聊聊Numpy.array中[:]和[::]的区别在哪【转载】
    [:]和[::]的区别蛮大的,用的好可以节省时间,下面以实例进行分析array([:])12345678910111213>>> import numpy as np>>>>>> x=np.array([1,2,3,4,5,6,7,8,9,10,11,12])>>> print(x[1:5])#打印index为1~5的数组,范围是左闭右开[2 3 4 5]>>> print(x[3:])#打印index=3之后的数组,包含index=3[ 4  5  6  7  8  9 10 11 12]>>> print(x[:9])#打印index=9之前的数组,不包含index=9[1 2 3 4 5 6 7 8 9]>>> print(x[1:-2])#打印index=1到倒数第2个index之间的数组[ 2  3  4  5  6  7  8  9 10]>>> print(x[-9:-2])#打印倒数第9个index和倒数第2个index之间的数组,左开右闭[ 4  5  6  7  8  9 10]array([::])123456789101112>>> print(x[1::3])#以index=1为起始位置,间隔3[ 2  5  8 11]>>> print(x[::3])#默认从index=0开始,间隔3[ 1  4  7 10]>>> print(x[3::])#和[3:]一样[ 4  5  6  7  8  9 10 11 12]>>> print(x[::-1])#反向打印数据,从最后一个index开始,间隔为1[12 11 10  9  8  7  6  5  4  3  2  1]>>> print(x[::-3])#反向打印数据,从最后一个index开始,间隔为3[12  9  6  3]>>> print(x[7:2:-1])#反向打印index=2(不包含)到index=7之间的数据[8 7 6 5 4]也是碰到这方面的问题,没搞明白,干脆试了试就清楚了,应该[:]和[::]还有很多有趣的地方。补充:Numpy.array()详解 、np.array与np.asarray辨析、 np.array和np.ndarry的区别记录一下numpy.array()的详细用法,以及与np.asarray()和np.ndarray()的区别。1. Numpy.array()详解该函数的作用一言蔽之就是用来产生数组。1.1 函数形式123456numpy.array(object,     dtype=None,     copy=True,     order='K',     subok=False,     ndmin=0)1.2 参数详解object:必选参数,类型为array_like,可以有四种类型:数组,公开数组接口的任何对象,__array__方法返回数组的对象,或任何(嵌套)序列。np.array()的作用就是按照一定要求将object转换为数组。dtype:可选参数,用来表示数组元素的类型。如果没有给出,那么类型将被确定为保持序列中的对象所需的最小类型。注: This argument can only be used to ‘upcast' the array. For downcasting, use the .astype(t) method.copy:可选参数,类型为bool值。如果为true(默认值),则复制对象。否则的话只有在以下三种情况下才会返回副本:(1).if __array__ returns a copy;(2). if obj is a nested sequence;(3). if a copy is needed to satisfy any of the other requirements (dtype, order, etc.)order:{‘K', ‘A', ‘C', ‘F'},optional 。指定阵列的内存布局。该参数我至今还没有遇到过具体用法,这句话的意思就是我不会,故在此省略。subok:可选参数,类型为bool值。如果为True,则子类将被传递,否则返回的数组将被强制为基类数组(默认)。或者说,True:使用object的内部数据类型,False:使用object数组的数据类型。ndmin:可选参数,类型为int型。指定结果数组应具有的最小维数。返回对象out:输出ndarray,满足指定要求的数组对象。1.3 具体用法简单示例12345678910111213141516171819import numpy as np  arr01 = np.array([1,2,3])print(arr01) #[1 2 3]print(type(arr01))  #<class 'numpy.ndarray'>print(arr01.dtype)  #int32  #Upcastingarr02 = np.array([1.,2.,3.])print(arr02) #[1. 2. 3.]print(arr02.dtype)  #float64  #More than one dimension:arr03 = np.array([[1,2],[3,4]])print(arr03)"""[[1 2] [3 4]]"""dtype参数使用示例1234567891011121314151617181920import numpy as np  #指定数组元素类型为复数类型DYX= np.array([1,2,3],dtype = complex)print(DYX) #[1.+0.j 2.+0.j 3.+0.j]print(DYX.dtype)  #complex128  #由多个元素组成的数据类型:HXH = np.array([(1,2),(3,4)],dtype=[('a','<i4'),('b','<i8')])print(HXH)  #[(1, 2) (3, 4)]#下面的输出有点神奇,我也只能记住规律了。print(HXH["a"]) #[1 3]print(HXH["b"])  #[2 4]print(HXH.dtype)  #[('a', '<i4'), ('b', '<i8')]print(HXH["a"].dtype) #int32print(HXH["b"].dtype) #int64  TSL = np.array([(1,2,3),(4,5,6)],dtype=[("a","i"),("b","i"),("c","i")])print(TSL["a"]) #[1 4]print(TSL["a"].dtype)  #int32上述代码中,numpy的数据类型,可以百度下subok参数使用示例import numpy as np  DYX = np.array(np.mat('1 2; 3 4'))#没有显示的写出subok的值,但是默认为Falseprint(DYX)#数组类型print(type(DYX))  #<class 'numpy.ndarray'>"""[[1 2] [3 4]]"""  HXH = np.array(np.mat('1 2; 3 4'), subok=True)print(HXH)#矩阵类型print(type(HXH))  #<class 'numpy.matrixlib.defmatrix.matrix'>"""[[1 2] [3 4]]"""前文对subok的描述是这样的:“如果为True,则子类将被传递,否则返回的数组将被强制为基类数组(默认)”。在上文的代码中“np.mat('1 2; 3 4')”,就是子类,是矩阵类型。DYX = np.array(np.mat('1 2; 3 4'))中subok为False,返回的数组类型被强制为基类数组,所以DYX的类型是<class 'numpy.ndarray'>,是数组;HXH = np.array(np.mat('1 2; 3 4'), subok=True)中subok为True,子类被传递,所以HXH的类型是矩阵<class 'numpy.matrixlib.defmatrix.matrix'>。这就是区别所在。ndmin参数使用示例12345678910import numpy as np  DYX = np.array([1,2,3],ndmin=0)print(DYX,DYX.shape) #[1 2 3] (3,)  HXH = np.array([1,2,3],ndmin=1)print(HXH,HXH.shape) #[1 2 3] (3,)  TSL = np.array([1,2,3],ndmin=2)print(TSL,TSL.shape) #[[1 2 3]] (1, 3)其他两个参数copy和order,我至今还没有遇到过,所以暂且不表。谁有介绍这两个参数用法的博客吗?2. Asarray和Array辨析Numpy.asaray的用法不再赘述,主要介绍一下二者的区别。2.1 object对象是普通迭代序列时12345678910111213141516171819202122import numpy as np  data = [1,1,1]print(type(data)) #<class 'list'> 列表类型arr_ar = np.array(data)arr_as = np.asarray(data)  #输出上没有区别print(arr_ar) #[1 1 1]print(arr_as) #[1 1 1]  data[1]=2#改变原序列对arr_ar和arr_as没影响print(arr_ar) #[1 1 1]print(arr_as) #[1 1 1]  #此时data是[1, 2, 1]#改变arr_ar和arr_as对原序列没有影响arr_ar[1]=3print(data) #[1, 2, 1]arr_as[1]=3print(data)  #[1, 2, 1]可见在参数对象是普通迭代序列时,asarray和array没有区别(在我的理解范围内)。2.2 object对象是ndarray对象时123456789101112131415161718192021222324252627import numpy as np data = np.ones((3,))#print(type(data)) #<class 'numpy.ndarray'> 数组类型arr_ar = np.array(data)arr_as = np.asarray(data)  print(arr_ar) #[1. 1. 1.]print(arr_as) #[1. 1. 1.]  """这边区别就出来了。修改原始序列后,np.array()产生的数组不变,但是np.asarray()产生的数组发生了变化"""data[1]=2print(arr_ar) #[1. 1. 1.]print(arr_as) #[1. 2. 1.]  !!!  """这边也有区别,修改array产生的数组,不影响原始序列修改asarray产生的数组,会影响原始序列"""#此时data=[1. 2. 1.]arr_ar[2]=3print(data)  #[1. 2. 1.]arr_as[2]=3print(data)  #[1. 2. 3.]我们总结一下:相同点:array和asarray都可以将数组转化为ndarray对象。区别:当参数为一般数组时,两个函数结果相同;当参数本身就是ndarray类型时,array会新建一个ndarray对象,作为参数的副本,但是asarray不会新建,而是与参数共享同一个内存。重点就是这个共享内存。3.Numpy.ndarray()这是最近在一个项目里看到的用法,搜索了一下用法,只在stackoverflow看到了一个问题:“What is the difference between ndarray and array in numpy?”。地址如下:https://stackoverflow.com/questions/15879315/what-is-the-difference-between-ndarray-and-array-in-numpynumpy.array只是一个创建ndarray的便利函数;它本身不是一个类。他讲到也可以使用numpy.ndarray创建一个数组,但这不是推荐的方法。 numpy.ndarray() 是一个类,而numpy.array() 是一个创建ndarray的方法/函数。在numpy docs中,如果你想从ndarray类创建一个数组,你可以用引用的2种方式来做:(1).using array(), zeros() or empty() methods: Arrays should be constructed using array, zeros or empty (refer to the See Also section below). The parameters given here refer to a low-level method (ndarray(…)) for instantiating an array.【1-使用array(), zeros()或empty()方法:数组应该使用array, zeros()或empty()构造。这里给出的参数引用用于实例化数组的低级方法(ndarray(…))。】(2).from ndarray class directly: There are two modes of creating an array using new: If buffer is None, then only shape, dtype, and order are used. If buffer is an object exposing the buffer interface, then all keywords are interpreted.【2-来自ndarray类:使用new创建数组有两种模式:如果buffer是None,则只使用shape,dtype和order。 如果buffer是公开buffer接口的对象,则解释所有关键字。】所以说老老实实用numpy.array()吧。
  • [技术干货] 聊聊prod()与cumprod()区别cumsum()【转载】
    pandas.Series.cumprod 官方文档cumprod()累积连乘1234Series.cumprod(axis=None, skipna=True, *args, **kwargs)#实现功能:Return cumulative product over a DataFrame or Series axis.#实现功能:Returns a DataFrame or Series of the same size containing the cumulative product.#return:scalar or Seriescumsum()累积连加pandas.Series.prod官方文档123Series.prod(axis=None, skipna=None, level=None, numeric_only=None, min_count=0, **kwargs)# 实现功能:Return the product of the values for the requested axis.# return:scalar or Series优点没看明白,因为常规情况下,所用的.prod()并非pandas下的函数,而是numpy下的函数。numpy.prod官方文档123numpy.prod(a, axis=None, dtype=None, out=None, keepdims=<class numpy._globals._NoValue>)# 实现功能:Return the product of array elements over a given axis.# return:product_along_axis : ndarray返回给定轴上数组元素的乘积。跟cumprod不同,cumprod是计算当前一个累积乘上前面所有的数据,更多是一个list;prod返回的是给定这个轴上最终一个值。补充:【python初学者】简单易懂的图解:np.cumsum和np.cumprod函数到底在干嘛?1.np.cumsum本人是一名python小白,最近过完了python的基本知识后,在看《利用python进行数据分析》这本书,书中cumsum函数一笔带过留下本小白“懵逼树下你和我”,当然是我自己的问题不是书的问题,经过画图理解后渐渐明白了这个函数到底在干么。1.1np.cumsum-轴的概念首先,在学习cumsum函数之前我们应该先明白什么是轴,以下面代码来进行说明:12arr=np.arange(1,17,1).reshape((2,2,4))arr12345array([[[ 1,  2,  3,  4],        [ 5,  6,  7,  8]],        [[ 9, 10, 11, 12],        [13, 14, 15, 16]]])其实数组的轴(axis)就是数组的维度,上面的代码生成了一个224的数组,所以1、这个数组的0轴为2 ,axis=02、这个数组的1轴为2 ,axis=13、这个数组的2轴为4 ,axis=2该数组如图所示(蓝,橙,黄,绿都是2轴,橙和绿上的“2轴”画图时忘了标注):这里还要补充说一下:红色的数字只是因为我用的iPad画图很不方便所以没改成黑色,忽略就好1.2cumsum(axis=0)cumsum作用:计算轴向元素累加和,返回由中间结果组成的数组这句概念中我认为大家理解起来比较难受的地方应该是轴向元素累加。首先,通过前文对轴概念的理解我们可以知道axis=0代表着最外层的维度也就是0轴(这里可能说法不太正确,主要为了配合上节图片),所以就是0轴的累加计算,我们以前文用到的数组为例(红色虚线表示按照0轴进行累加):step1:沿着0轴进行累加代码:12345arr=np.array([[[ 1,  2,  3,  4],               [ 5,  6,  7,  8]],              [[ 9, 10, 11, 12],               [13, 14, 15, 16]]])arr.cumsum(axis=0)结果为:12345array([[[ 1,  2,  3,  4],        [ 5,  6,  7,  8]],        [[10, 12, 14, 16],        [18, 20, 22, 24]]])1.3cumsum(axis=1)这里我们还是以之前举例的数组为例,沿着1轴进行累加(也就是2 * 2 * 4中的第二个2),这里为了方便讲解我将数组的摆放位置换了一下,不影响哈~step1:红色虚线代表我们现在应该沿着1轴进行累加啦!step2:既然沿着1轴进行累加,我们是不是就应该在1轴内部进行累加呢?所以就应该[1,2,3,4]和[5,6,7,8]进行累加,[9,10,11,12]和[13,14,15,16]进行累加代码结果:1234567arr.cumsum(axis=1)#运行结果array([[[ 1,  2,  3,  4],        [ 6,  8, 10, 12]],        [[ 9, 10, 11, 12],        [22, 24, 26, 28]]])1.4cumsum(axis=2)都已经讲到沿着轴2进行累加了,废话就不多说了直接放图,大家看看有没有做对吧step1:老规矩:红色虚线表示沿着2轴进行累加,所以应该是1,2,3,4进行累加,5,6,7,8进行累加,依次类推step2我们以蓝色这一项为例:第一项:1第二项:1+2=3第三项:1+2+3=6第四项:1+2+3+4=10代码结果:1234567arr.cumsum(axis=2)#运行结果array([[[ 1,  3,  6, 10],        [ 5, 11, 18, 26]],        [[ 9, 19, 30, 42],        [13, 27, 42, 58]]])
  • [技术干货] js 常用方法
    1、输入一个值,返回其数据类型function type(para) { return Object.prototype.toString.call(para) }2、数组去重function unique1(arr) { return [...new Set(arr)] } function unique2(arr) { var obj = {}; return arr.filter(ele => { if (!obj[ele]) { obj[ele] = true; return true; } }) } function unique3(arr) { var result = []; arr.forEach(ele => { if (result.indexOf(ele) == -1) { result.push(ele) } }) return result; }3、字符串去重String.prototype.unique = function () { var obj = {}, str = '', len = this.length; for (var i = 0; i < len; i++) { if (!obj[this[i]]) { str += this[i]; obj[this[i]] = true; } } return str; } ###### //去除连续的字符串 function uniq(str) { return str.replace(/(\w)\1+/g, '$1') }4、深拷贝 浅拷贝//深克隆(深克隆不考虑函数) function deepClone(obj, result) { var result = result || {}; for (var prop in obj) { if (obj.hasOwnProperty(prop)) { if (typeof obj[prop] == 'object' && obj[prop] !== null) { // 引用值(obj/array)且不为null if (Object.prototype.toString.call(obj[prop]) == '[object Object]') { // 对象 result[prop] = {}; } else { // 数组 result[prop] = []; } deepClone(obj[prop], result[prop]) } else { // 原始值或func result[prop] = obj[prop] } } } return result; } // 深浅克隆是针对引用值 function deepClone(target) { if (typeof (target) !== 'object') { return target; } var result; if (Object.prototype.toString.call(target) == '[object Array]') { // 数组 result = [] } else { // 对象 result = {}; } for (var prop in target) { if (target.hasOwnProperty(prop)) { result[prop] = deepClone(target[prop]) } } return result; } // 无法复制函数 var o1 = jsON.parse(jsON.stringify(obj1));
  • [交流吐槽] java基本数据类型
    package base;public class demo02 {    public static void main(String[] args) {        //八大基本数据类型        //整型        int num1 = 10;  //最常用        //范围在100多//        byte num2 = 200;        //short        short num3 = 20;        //long        long num4 = 40L;    //Long类型在数字后面加个L表示是long类型        //float 浮点数:小数        float num5 = 12.3F; //float类型加F,否则就报错        double num6 = 3.14159;        //字符        char name1 = 'a';//        char name2 = 'as';  //字符是一个        //字符串        //String 不是关键字,是一个类        String num7 = "asd";        //布尔值        boolean flag = true;    //真        boolean fla = false;    //假    }}
  • [使用说明] CodeArts IDE代码重构
    重构是通过改变现有程序结构而不改变其功能和用途来提高代码的可重用性和可维护性。CodeArts IDE支持重构操作,提供了多种重要的重构类型,如下:重命名提取重构(方法、常数、接口、字段、变量、参数、超类)引入重构 (字段、常量、变量、参数、参数对象、函数变量、功能参数)复制重构移动和更改重构查找和替换重构内联重构 (方法、字段、变量、匿名类、超类、参数)设计模式与重构补全功能运用CodeArts IDE支持的重构操作重构类型支持操作快捷键重命名Rename SymbolShift+F6提取重构Extract Superclass Extract Method Object Extract MethodCtrl+Alt+Shift+MExtract Interface Extract Delegate... Encapsulate Fields 引入重构Introduce Variable Ctrl+Alt+VIntroduce ParameterCtrl+Alt+Shift+PIntroduce Parameter Object Introduce Functional Variable Introduce Functional Parameter Introduce FieldCtrl+Alt+Shift+FIntroduce Constant Ctrl+Alt+C复制重构Copy Class Shift+F5移动和更改重构Push members down Pull members up Move inner class to Upper level Make Static Change Method SignatureCtrl+F6Change Class SignatureCtrl+F6Type Migration 查找和替换重构Replace Inheritance with Delegation Replace Constructor with Factory Method Replace Constructor with Builder 内联重构Inline Variable Inline to Anonymous Class Inline Super Class Inline Parameter Ctrl+Alt+Shift+PInline Method Ctrl+Alt+Shift+LInline Field 其它重构Invert Boolean Use Interface Where Possible Convert Raw Types to GenericsConvert To Instance MethodRemove MiddlemanSafe DeleteAlt+DelMove Static Members
  • [使用说明] CodeArts智能代码补全
    代码补全可以有效的提升开发效率、减少拼写错误和输入代码量。CodeArts依赖于codearts.smartassist-java-ls插件实现代码补全功能。代码补全类型主要有:关键字基础补全名字补全类型补全方法补全片段补全缩写补全智能类型匹配补全标签属性补全CodeArts的代码补全具有能使用字段名称的驼峰字母作为关键字母快速搜索的特点。关键字基础补全关键字(Reserved Words)是指在Java、Javascript等计算机语言中有特定含义,用来表示一种数据类型,或者表示程序的结构等。CodeArts支持计算机语言的关键字基础补全。如图所示:输入关键字首字母,代码补全列表可优先推荐关键字public、private、protected。名字补全名字是指用户自定义的变量名、参数名、方法名、类名、接口名、包名等名称。CodeArts可根据上下文场景,推荐当前变量命名的模板。定义类的变量,代码推荐变量命名最优模板。当您定义好方法参数后,输入首字母后,CodeArts可优先在代码列表中推荐参数名称。输入名字首字母,代码补全列表可展示建议的名字。类型补全类型包括基础数据类型(整数类型、字符类型、浮点类型、布尔类型)和引用类型(类、接口类型、数组类型、null类型)。定义的每一个变量都必须声明其数据类型,因其在编译时进行严格的语法检查,如果变量值的数据类型与定义的类型不同,则会报错。因此,CodeArts对数据类型进行补全,便于减少拼写错误,加快变量的定义。如图所示:输入数据类型首字母,代码补全列表可优先推荐。方法补全方法是指定义在类中的具有特定功能的一段独立小程序。CodeArts方法补全时可补全方法所需的元素:方法名、返回值类型、参数表、方法体。CodeArts可根据类中的变量,补全类变量相关方法。类中已定义变量homeBrandMapper,CodeArts搜索推荐关于变量的常用的模板set和get方法。选择setHomeBrandMapper()方法上屏后,自动补全变量的set方法包含方法名、参数表、方法体。在项目主类中,可快速进行main方法声明补全。在类中输入main,选择main() method declaration上屏后,补全主类main方法。片段补全CodeArts为常用的代码片段提供了标准的模板,这些代码片段具有基于源代码语言的各种构造。这包括条件语句和循环、折叠区域和其它构造。        动画演示:缩写补全CodeArts常用缩写补全,可自动补全代码语句及符号。常用缩写:sout、souf、soutm、soutp、soutv打印方法for循环简写foriprsf、psf、psfi、psfs、psvm变量定义语句智能类型匹配补全智能类型匹配代码能够过滤代码建议列表并仅显示适用于当前上下文的类型。在可以确定类型的情况下使用:在赋值语句的右侧部分在变量初始值定义中在return返回语句中在方法调用的参数列表中在对象声明中new关键字之后在链式表达式中默认情况下,CodeArts会在您键入时自动显示代码推荐列表窗口。当您完成语句上屏,希望转换当前代码时,按Ctrl+Shift+Space键可触发CodeArts搜索与当前的代码相关内容,选择可进行转换。实例如下:return返回语句。CodeArts 扫描return语句相关的方法内容,并建议适合当前上下文的返回值。鼠标在return上,操作快捷键Ctrl+Shift+Space,推荐列表展示可转换的代码。标签属性补全CodeArts能够自动补全许多文件类型中标签和属性的名称和值:HTML,包括CSS 类和JSX 中的 HTML 标签的补全。按<可以开始输入标签名称。CodeArts扫描文件显示适合当前上下文的标记名称列表。按Enter键,CodeArts可添加所选的标签。驼峰搜索变量、参数、类、方法均可使用驼峰字母作为关键字母快速搜索,驼峰字母不区分大小写。直接输入SmsHomeBrandMapper的驼峰字母“Shbm”作为关键字;CodeArts搜索项目中的相关类名展示在代码推荐列表,Enter或Tab键可上屏SmsHomeBrandMapper。
  • [使用说明] CodeArts智能搜索
    智能搜索是CodeArts的核心功能之一,依赖于Language Server,能够进行项目级别的搜索,比如整个项目全局搜索、文件搜索、类名搜索以及自定义范围的搜索等,通过快捷键Double Ctrl,或Ctrl + N,或Ctrl + Shift + L可以打开搜索框。智能搜索的类型主要有6大类:AllTypeMemberTextFileCommand各个类型的搜索可以互相切换,搜索内容也随之切换。一、All搜索CodeArts的All搜索可进行全局搜索或自定义范围搜索。1.全局搜索:可以通过名称找到项目中或项目之外的任何内容,可以搜索文件、类、方法、命令等。搜索文本内容包含':'等符号,如图:当前编辑器有打开java文件,搜索面板则会显示当前文件的类、方法等。双击搜索结果项,可在编辑器打开并跳转到相应位置。2.自定义范围的搜索:方法搜索类搜索接口搜索内容搜索当前文件搜索枚举类型搜索注解类型搜索包搜索反射类搜索父类接口搜索子类接口搜索组合搜索    搜索规则:如搜索方法,输入“method:***”或“m:***”即可搜索;同时支持反向搜索,即输入“***:method”或“***:m”(*代表输入的内容)。    2.1 方法搜索    如果想直接搜索方法,可通过“method:***”或“m:***”来进行搜索(*代表输入的方法名),支持反向搜索。         2.2 类搜索    搜索和关键词相关的类,可通过“class:***”或“c:***”来进行搜索(*代表输入的类名),支持反向搜索。        2.3 接口搜索    搜索和关键词相关的接口,可通过“interface:***”或“i:***”来进行搜索(*代表输入的接口名),支持反向搜索。        2.4 内容搜索    搜索和关键词相关的文本内容,可通过“text:***”来进行搜索(*代表输入的文本)。        2.5 当前文件搜索    搜索当前文件的内容,可通过“local:***”或“l:***”来进行搜索(*代表输入的文件名),支持反向搜索。        2.6 枚举类型搜索    搜索和关键词相关的枚举类型,可通过“enum:***”或“e:***”来进行搜索(*代表输入的枚举名称),支持反向搜索。        2.7 注解类型搜索    搜索和关键词相关的注解类型,可通过“annotation:***”或“a:***”来进行搜索(*代表输入的注解名称),支持反向搜索。        2.8 包搜索    搜索和关键词相关的包,可通过“package:***”或“p:***”来进行搜索(*代表输入的包名),支持反向搜索。        2.9 反射类搜索    搜索和关键词相关的反射类,可通过“field:***”或“f:***”来进行搜索(*代表输入的反射类名称),支持反向搜索。        2.10 父类接口搜索    搜索和关键词相关的父类,可通过“super:***”来进行搜索(*代表输入的父类名)。        2.11 子类接口搜索    搜索和关键词相关的子类,可通过“sub:***”来进行搜索(*代表输入的子类名)。        2.12 组合搜索    “或”搜索:OR、|、||    如搜索类或方法,可通过“class:*** OR method:***”,“class:*** | method:***”,“class:*** || method:***”来进行搜索,支持反向搜索。        “与”搜索:AND、&、&&    如搜索类与方法,可通过“class:*** AND method:***”,“class:*** & method:***”,“class:*** && method:***”来进行搜索,支持反向搜索。        “或”搜索和“与”搜索组合        更多组合搜索,请自行尝试使用。二、Type搜索可搜索类(class)、接口(interface)、枚举类(enum)、注解(annotation)。三、Member搜索可搜索方法(method)和字段(field)。四、Text搜索可对任意文本内容进行搜索。五、File搜索不仅可以搜索类名,还可以搜索其他文件比如properties文件、xml文件,或者范围更大,可以找到对应名称的文件夹。搜索范围:类文件文件夹六、Command搜索在输入框里输入命令关键词,就会展示相应的命令,比如有文件(File)、视图(View)、终端(Terminal)、调试(Debug)、运行(Run)、Git等等,双击可以执行命令,或根据命令快捷键进行相应的操作。
  • [技术干货] 数据结构进阶—红黑树[转载]
    1、R-B Tree的概念红黑树(R-B Tree),是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。AVL树是高度平衡的。C++ STL中,很多部分(包括set, multiset, map, multimap)的底层都是红黑树。对于任意一棵红黑树,它可以在O(l o g 2 N log_2Nlog 2​ N)时间内做查找,插入和删除,这里的N是树中元素的数目。2、红黑树的性质红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树还增加了如下的额外要求:每个结点不是红色就是黑色根节点是黑色的每个叶子结点都是黑色的(此处的叶子结点指的是空结点(NIL))如果一个节点是红色的,则它的两个孩子结点是黑色的对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点2.1 红黑树性质分析性质1和性质2:这个很好理解,红黑树嘛,结点不是红色就是黑色,至于为什么根结点是黑色的,因为这也是规定。性质3:这里的叶子结点不是我们平常认知的叶子结点(没有子结点的结点),而是NIL(可以认为是nullptr)性质4:在一棵红黑树中,不可能存在连续的两个红结点(就是父结点是红色的并且子结点也是红色的)性质5:在上图中,例如黑结点13到黑结点1左边的NUL,有2个黑结点,到黑结点11的左右两边也有2个黑结点。对于红结点17到红结点22左右两边和到27的左右两边的黑结点数也是相同的,都为2。因为上图满足这5点性质,所以该树是一棵红黑树在R-B Tree的概念中提到红黑树是接近平衡的,因为红黑树确保没有一条路径会比其他路径长出俩倍。为什么是这样呢?由于每个结点非红即黑(性质1),加上不存在两个连续的红结点(性质4),那么红黑树的最长路径一定是红黑交替的,而最短路径一定全都是黑结点。而任意结点其所有后代叶结点的简单路径上,均包含相同数目的黑色结点,则说明最长路径和最短路径中的黑色结点数目一定相等。最后加上根节点为黑(性质2)和叶子结点为黑(性质3),那么一定可以得到一个结论:最长路径<=2*最短路径最长路径 = 最短路径最长路径 = 2*最短路径3、红黑树的插入当我们在对红黑树进行插入操作时,对树做了修改,那么可能会违背红黑树的性质。为了保持红黑树的性质,我们可以通过对树进行旋转,即修改树种某些结点的颜色及指针结构,以达到对红黑树进行插入、删除结点等操作时,红黑树依然能保持它特有的性质(五点性质)。红黑树的插入分为5种情况,在了解之前,我们还要必须知道插入的结点是红色的,如果插入结点为黑色,总是会破坏性质5,而插入红结点不一定会破坏性质5。因为在插入之前,该树已经是红黑树,插入了黑结点肯定会影响该结点到根结点的这条路径。比如:在上图中黑结点11的左边插入黑结点,除了这条路径的黑结点数为4之外,其他的都为3。那为什么插入红色结点不一定会破坏性质5呢?假如还是在上图中黑结点11的左边插入红结点,在插入之后,每条路径的黑结点还是3。除此之外,我们将需要插入的结点标为N,父结点为P,祖父结点为G,叔结点为U(父亲的亲兄弟)。下面将逐一介绍这5种情况:3.1 情况1该树为空树,直接插入根结点的位置,违反性质1,把节点颜色有红改为黑即可3.2 情况2插入节点N的父节点P为黑色,不违反任何性质,无需做任何修改 3.3 情况3N为红,P为红,(祖节点一定存在,且为黑,下边同理)U也为红,这里不论P是G的左孩子,还是右孩子;不论N是P的左孩子,还是右孩子为什么在这个情况下,祖父结点一定存在?因为P结点为红,假设祖父结点不存在,那么P将成为整颗树的根,但P为红色,已经违反了性质2(根结点为黑色)在此情况下,只需要将P和U变色即可3.4 情况4N为红,P为红,U为黑(存在为黑或者不存在),P为G的左孩子,N为P的左孩子(或者P为G的右孩子,N为P的左孩子;反正就是同向的)在此情况下,需要将P、G变色,P、G变换即左单旋(或者右单旋)此处为左单旋,右单旋为相反操作3.5 情况5N为红,P为红,U为黑(存在为黑或者不存在),P为G的左孩子,N为P的右孩子(或者P为G的右孩子,N为P的左孩子;反正两方向相反)在此情况下,需要进行两次旋旋转,P、N右单旋,G、N左单旋此处为右左单旋,左右单旋为相反操作4、树的旋转红黑树的旋转和AVL树的旋转一样,只是红黑树没有平衡因子,不需要控制平衡因子4.1 左单旋void RotateL(Node* parent){    Node* subR = parent->_right;//parent的的右边一定不为nullptr    Node* subRL = subR->_left;//subR的左边可能为nullptr    Node* pparent = parent->_parent;//保存parent的根(父节点)    //将subRL链接到parent的右侧    parent->_right = subRL;    if (subRL != nullptr)        subRL->_parent = parent;    //将parent链接到subR左侧    subR->_left = parent;    parent->_parent = subR;    //将subR和pparent链接起来    //pparent为nullptr,subR要成为新的根    if (pparent == nullptr)//_parent == _root    {        subR->_parent = pparent;        _root = subR;    }    //pparent不为nullptr,就链接pparent和subR    else    {        if (pparent->_right == parent)            pparent->_right = subR;        else            pparent->_left = subR;        subR->_parent = pparent;    }}4.2 右单旋void RotateR(Node* parent){    Node* subL = parent->_left;//parent的的左边一定不为nullptr    Node* subLR = subL->_right;//subL的右边可能为nullptr    Node* pparent = parent->_parent;//保存parent的根(父节点)    //将subLR链接到parent的左侧    parent->_left = subLR;    if (subLR != nullptr)        subLR->_parent = parent;    //将parent链接到subL右侧    subL->_right = parent;    parent->_parent = subL;    //将subL和pparent链接起来    //pparent为nullptr,subL要成为新的根    if (pparent == nullptr)    {        subL->_parent = pparent;        _root = subL;    }    //pparent不为nullptr,就链接pparent和subL    else    {        if (pparent->_left == parent)            pparent->_left = subL;        else            pparent->_right = subL;        subL->_parent = pparent;    }}4.3 左右单旋和右左单旋//右旋左旋void RotateRL(Node* parent){    RotateR(parent->_right);    RotateL(parent);}//左旋右旋void RotateLR(Node* parent){    RotateL(parent->_left);    RotateR(parent);}123456789101112135、旋转总结父亲节点是·黑色,插入的节点为红色,不需要进行操作。父亲节点和叔叔节点是红色,插入一个红色的节点后,父亲节点和叔叔节点变为黑色,祖父节点变为红色父亲节点是红色、叔叔节点不存在或者存在且为黑色:1.parent在grandfather左侧:cur在parent左侧 -> 以祖父为中心,进行右旋,并且祖父变为红色,父亲变为黑色 (右旋)cur在parent右侧 -> 先以父亲为中心进行左旋,再以祖父为中心进行右旋 (左右双旋)2.parent在grandfather右侧:cur在parent右侧 -> 以祖父为中心,进行左旋,并且祖父变为红色,父亲变为黑色 (左旋)cur在parent左侧 -> 先以父亲为中心进行右旋,再以祖父为中心进行左旋 (右左双旋)6、整体代码#pragma once#include<iostream>using namespace std;enum Color{    RED,    BLACK};template<class K, class V>struct RBTreeNode{    RBTreeNode<K, V>* _parent;    RBTreeNode<K, V>* _left;    RBTreeNode<K, V>* _right;    pair<K, V> _kv;    Color _cor;    RBTreeNode(const pair<K, V>& kv)        :_parent(nullptr)        , _left(nullptr)        , _right(nullptr)        , _kv(kv)        , _cor(RED)    {}};template<class K, class V>class RBTree{    typedef RBTreeNode<K, V> Node;public:    RBTree()        :_root(nullptr)    {}        bool Insert(const pair<K, V>& kv)    {        //_root为nullptr,说明是插入的第一个值        if (_root == nullptr)        {            _root = new Node(kv);            _root->_cor = BLACK;            return true;        }        Node* parent = nullptr;        Node* cur = _root;        while (cur)        {            //插入的值比cur大,说明要在右边插入            if (kv.first > cur->_kv.first)            {                parent = cur;                cur = cur->_right;            }            //插入的值比cur小,说明要在左边插入            else if (kv.first < cur->_kv.first)            {                parent = cur;                cur = cur->_left;            }            else                return false;        }        //链接父子结点        cur = new Node(kv);        if (kv.first > parent->_kv.first)        {            parent->_right = cur;            cur->_parent = parent;        }        else        {            parent->_left = cur;            cur->_parent = parent;        }        //控制平衡        while (parent != nullptr && parent->_cor == RED)        {            //不需要判断grandfather是否为nullptr,因为parent如果是红色,不可能是根            Node* grandfather = parent->_parent;            if (parent == grandfather->_left)            {                Node* uncle = grandfather->_right;                //情况1、uncle存在且为红,不需要旋转处理                if (uncle != nullptr && uncle->_cor == RED)                {                    //变色+向上继续调整                    parent->_cor = uncle->_cor = BLACK;                    grandfather->_cor = RED;                    cur = grandfather;                    parent = cur->_parent;                }                //情况2、uncle存在且为黑/不存在                else                {                    //cur为parent的左孩子,需要进右单旋                    //       g      p(黑)                    //     p     c    g(红)                    //   c                    if (cur == parent->_left)                    {                        RotateR(grandfather);                        parent->_cor = BLACK;                        grandfather->_cor = RED;                    }                    //cur为parent的右孩子,需要左右双旋(先左旋,再右旋)                    //      g            c(黑)                    //   p(红)         p   g(红)                    //      c(红)                    else                    {                        RotateLR(grandfather);                        //RorateL(parent);                        //RorateR(grandfather);                        cur->_cor = BLACK;                        grandfather->_cor = RED;                    }                    break;                }            }            //parent为grandfather的右孩子,和parent为grandfather的左孩子的情况一样,只是方向不同            else            {                Node* uncle = grandfather->_left;                //情况1、uncle存在且为红,不需要旋转处理                if (uncle != nullptr && uncle->_cor == RED)                {                    //变色+向上继续调整                    parent->_cor = uncle->_cor = BLACK;                    grandfather->_cor = RED;                    cur = grandfather;                    parent = cur->_parent;                }                //情况2、uncle存在且为黑/不存在                else                {                    //cur为parent的右孩子,需要进左单旋                    if (cur == parent->_right)                    {                        RotateL(grandfather);                        parent->_cor = BLACK;                        grandfather->_cor = RED;                    }                    //cur为parent的左孩子,需要进右左单旋                    else                    {                        RotateRL(grandfather);                        //RorateR(parent);                        //RorateL(grandfather);                        cur->_cor = BLACK;                        grandfather->_cor = RED;                    }                    break;                }            }        }        //让根变黑        _root->_cor = BLACK;        return true;    }private:    Node* _root;    //右单旋    void RotateR(Node* parent)    {        Node* subL = parent->_left;//parent的的左边一定不为nullptr        Node* subLR = subL->_right;//subL的右边可能为nullptr        Node* pparent = parent->_parent;//保存parent的根(父节点)        //将subLR链接到parent的左侧        parent->_left = subLR;        if (subLR != nullptr)            subLR->_parent = parent;        //将parent链接到subL右侧        subL->_right = parent;        parent->_parent = subL;        //将subL和pparent链接起来        //pparent为nullptr,subL要成为新的根        if (pparent == nullptr)        {            subL->_parent = pparent;            _root = subL;        }        //pparent不为nullptr,就链接pparent和subL        else        {            if (pparent->_left == parent)                pparent->_left = subL;            else                pparent->_right = subL;            subL->_parent = pparent;        }    }    //左单旋    void RotateL(Node* parent)    {        Node* subR = parent->_right;//parent的的右边一定不为nullptr        Node* subRL = subR->_left;//subR的左边可能为nullptr        Node* pparent = parent->_parent;//保存parent的根(父节点)        //将subRL链接到parent的右侧        parent->_right = subRL;        if (subRL != nullptr)            subRL->_parent = parent;        //将parent链接到subR左侧        subR->_left = parent;        parent->_parent = subR;        //将subR和pparent链接起来        //pparent为nullptr,subR要成为新的根        if (pparent == nullptr)//_parent == _root        {            subR->_parent = pparent;            _root = subR;        }        //pparent不为nullptr,就链接pparent和subR        else        {            if (pparent->_right == parent)                pparent->_right = subR;            else                pparent->_left = subR;            subR->_parent = pparent;        }    }    //右旋左旋    void RotateRL(Node* parent)    {        RotateR(parent->_right);        RotateL(parent);    }    //左旋右旋    void RotateLR(Node* parent)    {        RotateL(parent->_left);        RotateR(parent);    }};原文链接:https://blog.csdn.net/qq_56044032/article/details/124823622
  • [技术干货] 了解什么是哈希表[转载]
    前言本次将从概念上理解什么是哈希表,理论知识较多,满满干货1、什么是哈希表1.1 哈希表的整体概念在我们之前学过的数据结构中,例如线性表(顺序表、链表等)和树结构(AVL树、红黑树),在这些结构中存储的记录(数据)的相对位置是随机的,和记录的关键字key没有任何关联。在这些结构中进行查找,因为不知道key(关键字)和记录在结构中存储位置的关系,所以只能让key和结构中的记录进行一一比较,但是不一定是每个记录都要与key进行比较。在顺序查找时,比较的结果有"=“和"≠"两种可能。在二分查找或者平衡搜索树查找,比较的结果有”>“,”<“和”="三种可能。所以对于这种比较型查找,效率和比较的次数强关联,比较次数少,效率就高,比较次数多,效率就低。对于上诉所说的情况,有没有一种可能,我们不需要通过任何比较就能找到我们想要查询的记录,当然是有可能的。只需要在记录的存储位置和它的key建立一个确定的对应关系f(也可以称之为映射关系),使key与结构中唯一一个存储位置相对应。说到这里,初学者可能看得不是很明白,那就举个栗子:例如我在给别人自我介绍的时候,我说我来自重庆,此时在其他人脑海里就会联想到重庆火锅,小面等许多美食或者洪崖洞等地标性建筑。这就是一种映射关系,通过重庆这个关键字就能得到所对应的信息。因此在查找时,只需要通过这个对应关系,就能找到key所对应的值。如果结构中存在key和我们所想要查找的K相等的记录,则必定在f(K)的存储位置上,所以我们并不需要任何比较就能找到所查记录。对于这种映射关系,我们将其称之为哈希函数,按照这个思想建立的表,我们称之为哈希表或者散列表。1.2 举例说明1.2.1 例子1通过上面的概念和"判定字符是否唯一"这个题,我们就能从大体方向了解哈希表究竟是一个什么样的数据结构。至少我认为这道题直接明了的运用到了哈希思想。有兴趣的伙伴也可以尝试一下。对于初学者(没学过哈希)来说,做这个题基本都是用暴力解法:定义52个变量,初始值为0,用于分别代表字母a~ z 和A ~ Z ,然后对字符串s进行52次遍历,统计字母出现的次数,再计算这些变量值是否相同。虽然这样做没什么问题,但是效率太低。假设字符串长度为n,那么就会对字符串进行n次遍历,时间复杂度为O(N²)。此时就需要用到哈希思想,建立映射关系了。那如何建立映射关系呢?每个字符(字母)是有ASCII码的,ASCLL码也是一个整数。哈希思想就是在记录的存储位置和它的key建立一个确定的对应关系,使key与结构中唯一一个存储位置相对应,通过这个特征,我们可以用数组,因为数组有下标,下标是一个整数,而且通过下标我们就可以直接确定数组中的位置,从而取得该位置中的数据。通过这种映射,时间复杂度为O(N),因为我们只需要遍历字符串一次就能得到所有字符出现的次数,它们分别放在了数组中对应下标的位置中。1.2.2 例子2假设我们建立一张全国34个地区的各名族人口统计表,每个地区为一个记录,记录的各数据项为:显然,可以用一个一维数组C(1…30)来存放这张表,其中C[i]是编号为i的地区的人口情况。编号i便为记录的关键字,由它唯一确定记录的存储位置C[i]。例如:假设北京市的编号为1,若要查看北京市的各名族人口,只需要取出C[1]的记录即可。假如把这个数组看成是哈希表,则哈希函数f(key) = key。然而,很多情况下的哈希函数并不是如此简单。为了查看方便,应以地区作为关键字。假设地区名以汉语拼音的字符表示,则不能简单的取哈希函数f(key) = key,而是要将这些字符转换为数字,再进行其他相关处理。例如我们可以有这样的哈希函数:取关键字中的第一个字母在字母表中的序号作为哈希函数。例如:BEIJING的哈希函数值为字母’B’在字母表中的序号等于02。先求关键字的第一个和最后一个字母在表中的序号之和,然后判别这个值,若比30(表长)大,则减去30。例如:TIANJING的首尾字母’T’和’N’之和为34,故取04为它的哈希函数值。除了这两种哈希函数之外,还有其他很多的哈希函数,这得看自己怎么实现。从这个例子来看:哈希函数是一个映射,怎么映射,如何建立映射关系,100个人有100种想法,但是建立出来的映射关系也有好也有坏,说明哈希函数的建立是很灵活的,只要关键字通过哈希函数所得的位置在哈希表内即可。对于不同的关键字通过哈希函数可能得到同一个地址,即key1 ≠ key2,而f(key1) = f(key2),这种现象称为哈希冲突或者哈希碰撞。例如在第一种哈希函数情况下,山西、上海、山东和四川的首字母都是’H’,哈希地址均为19,但C[19]只能存放一个记录,如果存放了山西,那么其他三个记录放在何处呢?对于这种情况,需要仔细分析关键字的特性,构造出合适的哈希函数,从而避免哈希冲突的发生。该例子来源于严尉敏老师的《数据结构与算法》一书。1.3 小总结一般情况下,哈希冲突只能可能的减少,无法达到完全避免的效果。因为,哈希函数是从关键字集合到地址集合的映像。表越长,数据量越少,冲突的概率就越低,表越短,数据量越大,冲突的概率就越高。在建立哈希表时不仅要设定一个好的哈希函数,也要设定一种好的处理冲突的方法。综上所述,可如下描述哈希表:根据哈希函数和处理哈希冲突的方法,将一组记录的关键字映射到一个有限且连续的一段区间内,并以关键字处理得到地址作为记录在表中的存储位置,这种表就成为哈希表,这个过程称为哈希造表或散列,所得的存储位置称为哈希地址或者散列地址。2、哈希函数的构造方法构造哈希函数的方法有很多,但能尽可能的避免哈希冲突才能称得上好的哈希函数。换言之,就是关键字通过哈希函数的处理得到一个随机的地址,以便使一组关键字的哈希地址均匀的分布在整个地址区间,从而减少哈希冲突。常见的哈希函数的构造方法有以下几种:2.1 直接定址法取关键字或关键字的某个线性函数值为哈希地址,即:H(key) = key或者H(key) = a·key+b其中a和b是常数(这种哈希函数叫做自身函数)。对于上述例子1中提到的过的"判定字符是否唯一"这道题,就是采用的直接定址法。又例如:有一个从0到100分的期末成绩数字统计表,其中,分数作为关键字,哈希函数取关键字自身这样的话,如果要询问分数为88的有几人,则只需要查表的第88项即可。由于直接定址法所得的地址集合和关键字集合的大小相同。因此,对于不同的关键字不会发生冲突。但实际上用这种哈希函数的情况比较少。2.2 数字分析法假设 关键字是一个N进制数,并且哈希表中的可能出现的关键字都是事先知道的,则可以去关键字的若干数位组合成哈希地址。例如有9个记录,其关键字为8位十进制数。对于关键字全体的分析中,发现第①位和第②位都是‘1’和‘2’。第②位只能取‘’、‘6’和‘7’。第③位只能取‘6’和‘9’。第⑧位只能取‘2’、‘4’和‘6’。第⑨位只能取‘2’、‘3’和6。因此这些位不能取。但是对于⑤⑥⑦三位出现的数都是随机的,所以我们可以任取两位组合成为一个哈希地址。2.3 除留余数法取关键字被某个不大于哈希表表长m的数p除后所得的余数为哈希地址。即:H(key) = key % p,p ≤ m这是一种简单,也是最常用的构造哈希函数的方法。它不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。2.4 随机数法选择一个随机函数,取关键字的随机函数值为它的哈希地址,即:H(key) = random(key),其中random为随机函数。通常,当关键字长度不等时采用此方法构造哈希函数比较恰当。2.5 小总结除了上述所提到的方法之外,还有其他的一些哈希函数构造方法,例如平方取中法和折叠法等。在构造哈希函数时,需要根据实际情况来考虑采用不同的哈希函数。通常需要考虑一下因素:计算哈希函数所需要的时间(包括硬件指令的因素)关键字的长度哈希表的大小关键字的分布情况记录的查找频率3、处理哈希冲突的方法在前面哈希表的概念中提及到好的哈希函数能够减少哈希冲突。那究竟什么是哈希冲突呢?例如下图:因此,如何处理哈希冲突也是哈希表不可缺少的一部分。3.1 开放定址法Hi = H(key)+di)%m i = 1, 2, 3…,k(k ≤ m-1)其中:H(key)为哈希函数,m为哈希表表长,d为增量序列,可以有下列3种取法:di = 1, 2, 3, …, m - 1,称为线性探测再散列di = 1², 2², 3², …, k² (k ≤ m/2),称为二次线性探测再散列di = 伪随机数序列,称为随机探测再散列例如,在长度为10的哈希表中已经填入有关键字分别为35、56和97的记录(哈希函数H(key) = key % 10),现在有第四个记录,其关键字为65,有哈希函数得到哈希地址为5,产生冲突。若采取线性探测再散列,得到下一个地址6,让然冲突,再求下一个地址,还是冲突的,知道哈希地址为8,不冲突后将该记录放入,处理哈希冲突的过程也就结束。若采用二次线性探测再散列也类似从上述线性探测再散列的过程中我们可以发现一个现象:当表中i,i+1,i+2位置已经被填上的时候,当下次插入数据的哈希地址为i,i+1,i+2和i+3时都会填入到i+3的位置,·这种在处理冲突过程中发生的两个第一个哈希地址不同的记录去争夺同一个后继哈希地址的现象称为“二次聚集”即在处理同义词(哈希地址相同的记录)的冲突中有添加了非同义词的冲突,显然这种对查找不利。举个栗子:比如在一张表中,表的大小为150,如果前面100个地址所对应的空间都填入了记录,现在需要插入一个哈希地址为1的记录,经过线性探测再散列的方式,最终会将这个记录填入到哈希地址为101的空间。如果在查找这个记录时,就需要从表的起始位置开始查找,直到哈希地址为101为止。本来我们是想通过哈希表进行1次查找就能找到记录,但实际上却查找了101次,效率大大得降低了。相对于线性探测再散列的方式,二次线性探测再散列能降低二次聚集,大家可以思考一下为什么。但在另一方面,用线性探测再散列处理冲突可以保证做到:只要哈希表未被填满,总能找到一个不发生哈希冲突的地址。而二次探测再散列只有早哈希表长为m形如4j+3(j为整数)的素数是才有可能,随机探测再散列,取决于伪随机数列。3.2 再哈希法Hi = RHi(key) i = 1,2,…,kRHi为不同的哈希函数。当发生哈希冲突时,需要使用其他哈希函数计算哈希地址,如果再冲突,再重复上述操作,直到不在发生哈希冲突位置。这种做法降低了"二次聚集",但增加了计算哈希地址的时间。3.3 链地址法链地址法又被称为拉链法。它将所有关键字为同义词的记录存储在一张线性表中,这个线性表一般为链表。同时还需要一张表(暂时称之为A表)来存放这些线性表的指针。初始状态时,A表存放的都是空指针。凡是哈希地址为i的记录都插入到头指针为A[i]的链表中,插入的方式也是根据实际情况定,例如头插、尾插,也可在中间插入,从而保证链表中的记录有序。例如:已知一组关键字为{99,77,57,37,21,11,3}。按照哈希函数H(key) = key%10和链地址法处理哈希冲突:3.4 建立公共溢出区我们不仅要构造基本表,还有构造溢出表。基本表每个空间都存放一个有效记录。溢出表存放与基本表中关键字为同义词的记录,不管它们由哈希函数得到的哈希地址是什么,一旦发生哈希冲突,都将其填入溢出表。4、哈希表的装填因子虽然哈希表在关键字和记录的存储位置建立的映射关系,但是由于哈希冲突,使哈希表的查找任然需要给定值和关键字的比较。而比较次数取决于三个因素:哈希函数、处理哈希冲突的方法和哈希表的装填因子哈希表的装填因子定义:α = 表中填入的记录数/哈希的长度,α标志哈希表的装填程度。直直观的看α越大,表明填入的记录越多,再次插入记录时,发生哈希冲突的概率越大。α越小,表明填入的记录就越少,再次插入记录时,发生哈希冲突的概率越小。所以我们应该使α保持在某个特定的值附近,即不浪费太多的空间,也降低了哈希冲突的概率。一般而言,α最好在0.7~0.8之间,当α大于这个区间,就应该对哈希表进行扩容。原文链接:https://blog.csdn.net/qq_56044032/article/details/124939637
  • [技术干货] 大数据时代的“冷热数据”管理
    一、为“数据”降本的背景 信息爆炸的时代数据极速膨胀,数据存储与计算消耗的IT资源、能源日益增长。为了节省能源,例如我国推出了东数西算,腾讯把数据中心装进了贵州山里,微软把数据中心建在海底,“脸书”在犹他州雪山旁建立新数据中心。海底的数据中心建设从硬件、技术角度进行“数据成本”控制。从业务角度对膨胀的数据本身进行“冷热”分级管理,不仅有利于节约“计算成本”,也可以提高业务数据化运营效率。 二、冷热数据定义及意义 冷热数据主要从数据访问频度、更新频度进行划分。冷数据,即实际生产中被访问、更新频度比较低、概率比较低的数据。热数据,访问、更新频度较高,未来被调用的概率较高的数据。冷数据在业务场景中计算时效要求慢,可以做集中化部署,可以对数据进行压缩、去重等降低成本的方法。热数据因为访问频次需求大,效率要求高,可以高性能存储与就近计算部署;数据冷热管理最核心目标提高算力利用率,所谓算力通常包含CPU、GPU、内存、带宽等能力,算力瓶颈在于单位时间内处理数据能力。视频、人工智能等领域的算力消耗集中在对大规模数据及参数的“算法”的计算处理。在传统行业领域以结构化数据为主,算力消耗集中在“订单、客户、事件”三大类数据的搬运、数据排序、数据关联、数据合并、数据算术运算、数据的查询等。希望通过对数据冷热区分,精准识别出“热”数据,减少对“冷数据”的搬运、关联、排序、计算等,把算力集中在刀刃上,实现数据处理“提速、降本”。三、系统架构设计时对数据的“冷热”管理 数据规模控制目前有“冷热分离异构系统”和“冷热分离同构系统”两类架构。“冷热分离异构系统”:将冷热数据根据被访问的频度及概率,一般来说将“时间序列较早,访问频度较低于一定比例”归档转移至另一个系统的进行存储。两套系统拥有不同的存储特性、访问方式等,优先热数据访问性能的同时,降低冷数据的运维成本“冷热分离同构系统”:冷热数据应用同一套规则,同一个数据集群中部署不同配置的机器,不同服务器进HOT/COLD属性标志。高配置服务器管理管理热数据,低配置服务器用于管理冷数据。当创建一个新的Index时,指定其数据分配到Hot属性的机器上;一段时间后,再将其配置修改为分配到Cold属性机器上,Elasticsearch便会自动完成数据迁移。系统级数据的冷热分级管理可以有效提高算力使用效率。图:冷热存储策略全冷存储指数据全部存储在HDD盘,是一种较为经济的存储策略。全热存储指数据全部存储在SSD盘,满足高性能访问的需求。冷热混合存储指一定数量的分区存储在SSD盘,其余数据存储在HDD盘。四、数据结构设计时进行“冷热”管理 传统行业的数据处理不需要像阿尔法狗即时计算出围棋的落子位置,更多的是固化的计算逻辑。因此可以通过“数据分区、计算分时”等策略优化算力利用率数据分区,数据结构设计时从动态与静态维度对数据进行“冷热”分区,减少对“冷数据”的搬运、关联、排序、计算等,降低参与计算的数据规模。计算分时,很多传统领域数据计算步骤是相对固化的、非实时的,可以通过对计算步骤分解在多个时段,平滑并发计算量。1、所谓静态数据主要指事件类数据,描述发生一个事件的数据记录,如保险领域理赔,报案事件、理算记录、结案事件,每个事件包含了对象、时间、事件内容等。静态数据参与的计算主要在于“被搬运、被查询、被关联、被计算”,静态数据本身几乎不进行合并更新计算。对于静态数据中被关联、被计算关键字段可以进行热度标识,参与计算的高频字段可以分配至临时表独立存储,减少统计类计算时加载的数据规模。如:保险领域对理赔事件原始数据字段超过20个,数据“入湖共享”时对高频度报表计算的“案件类型、报案时间、结案时间、金额”4个“热”数据字段拆出一个独立表进行共享,并增加“机构属性标记、客户号、手机号、保单号”关联关键字段(数据规模比原始数据降低3/4)。这样不同机构在开展个性化理赔统计报表分析时(不同分公司报表分析频度、统计样式可以个性化),仅需要加载对应机构的数据,快速完成“客户-理赔”与“保单-理赔”关联计算,减少“客户-保单-理赔”跨表数据搬运及复杂关联。2、动态数据指会时序更新的数据,如客户类的数据“收入、偏好、最近一次交易等”涉及持续更新合并。动态数据消耗的算力集中在“数据更新合并、数据排序、查询、关联”,其中数据的Update涉及较多校验规则。针对动态数据中各字段更新频度进行冷热标识,对于高频度update字段进行独立表管理,避免高频对大宽表的读写操作。如在保险领域,客户高频度更新信息字段主要是“职业、出险次数、最近投保”等和交易关联性强字段,客户数据中台数据结构设计时,对高频update字段独立表写入管理,减少对客户大宽表加载与读写。结语目前在IT行业系统架构设计重视度比较高,在数据结构设计有很大提升空间。如我所在在保险企业业务核心系统为外资产品,运行10多年后进行升级重构时,最大的难题就是数据结构设计,招投标时国内厂商可以在系统结构上给出较为完善的解决方案,但在数据结构上、数据规则上面临很大挑战。
  • [交流吐槽] Flink SQL 知其所以然:SQL 数据类型大全!
    SQL 数据类型在介绍完一些基本概念之后,我们来认识一下,Flink SQL 中的数据类型。Flink SQL 内置了很多常见的数据类型,并且也为用户提供了自定义数据类型的能力。总共包含 3 部分:原子数据类型。复合数据类型。用户自定义数据类型。一、原子数据类型1、字符串类型:CHAR、CHAR(n):定长字符串,就和 Java 中的 Char 一样,n 代表字符的定长,取值范围 [1, 2,147,483,647]。如果不指定 n,则默认为 1。VARCHAR、VARCHAR(n)、STRING:可变长字符串,就和 Java 中的 String 一样,n 代表字符的最大长度,取值范围 [1, 2,147,483,647]。如果不指定 n,则默认为 1。STRING 等同于 VARCHAR(2147483647)。2、二进制字符串类型:BINARY、BINARY(n):定长二进制字符串,n 代表定长,取值范围 [1, 2,147,483,647]。如果不指定 n,则默认为 1。VARBINARY、VARBINARY(n)、BYTES:可变长二进制字符串,n 代表字符的最大长度,取值范围 [1, 2,147,483,647]。如果不指定 n,则默认为 1。BYTES 等同于 VARBINARY(2147483647)。3、 精确数值类型:DECIMAL、DECIMAL(p)、DECIMAL(p, s)、DEC、DEC(p)、DEC(p, s)、NUMERIC、NUMERIC(p)、NUMERIC(p, s):固定长度和精度的数值类型,就和 Java 中的 BigDecima一样,p 代表数值位数(长度),取值范围 [1, 38];s 代表小数点后的位数(精度),取值范围 [0, p]。如果不指定,p 默认为 10,s 默认为 0。TINYINT:-128 到 127 的 1 字节大小的有符号整数,就和 Java 中的 byte 一样。SMALLINT:-32,768 to 32,767 的 2 字节大小的有符号整数,就和 Java 中的 short 一样。INT、INTEGER:-2,147,483,648 to 2,147,483,647 的 4 字节大小的有符号整数,就和 Java 中的 int 一样。BIGINT:-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 的 8 字节大小的有符号整数,就和 Java 中的 long 一样。4、有损精度数值类型:FLOAT:4 字节大小的单精度浮点数值,就和 Java 中的 float 一样。DOUBLE、DOUBLE PRECISION:8 字节大小的双精度浮点数值,就和 Java 中的 double 一样。关于 FLOAT 和 DOUBLE 的区别可见 https://www.runoob.com/w3cnote/float-and-double-different.html。5、布尔类型:BOOLEAN。6、NULL 类型:NULL。7、Raw 类型:RAW('class', 'snapshot') 。只会在数据发生网络传输时进行序列化,反序列化操作,可以保留其原始数据。以 Java 举例,class 参数代表具体对应的 Java 类型,snapshot 代表类型在发生网络传输时的序列化器。8、日期、时间类型:DATE:由 年-月-日 组成的 不带时区含义 的日期类型,取值范围 [0000-01-01, 9999-12-31]TIME、TIME(p):由 小时:分钟:秒[.小数秒] 组成的 不带时区含义 的的时间的数据类型,精度高达纳秒,取值范围 [00:00:00.000000000到23:59:59.9999999]。其中 p 代表小数秒的位数,取值范围 [0, 9],如果不指定 p,默认为 0。TIMESTAMP、TIMESTAMP(p)、TIMESTAMP WITHOUT TIME ZONE、TIMESTAMP(p) WITHOUT TIME ZONE:由 年-月-日 小时:分钟:秒[.小数秒] 组成的 不带时区含义 的时间类型,取值范围 [0000-01-01 00:00:00.000000000, 9999-12-31 23:59:59.999999999]。其中 p 代表小数秒的位数,取值范围 [0, 9],如果不指定 p,默认为 6。TIMESTAMP WITH TIME ZONE、TIMESTAMP(p) WITH TIME ZONE:由 年-月-日 小时:分钟:秒[.小数秒] 时区 组成的 带时区含义 的时间类型,取值范围 [0000-01-01 00:00:00.000000000 +14:59, 9999-12-31 23:59:59.999999999 -14:59]。其中 p 代表小数秒的位数,取值范围 [0, 9],如果不指定 p,默认为 6。TIMESTAMP_LTZ、TIMESTAMP_LTZ(p):由 年-月-日 小时:分钟:秒[.小数秒] 时区 组成的 带时区含义 的时间类型,取值范围 [0000-01-01 00:00:00.000000000 +14:59, 9999-12-31 23:59:59.999999999 -14:59]。其中 p 代表小数秒的位数,取值范围 [0, 9],如果不指定 p,默认为 6。TIMESTAMP_LTZ 与 TIMESTAMP WITH TIME ZONE 的区别在于:TIMESTAMP WITH TIME ZONE 的时区信息是携带在数据中的,举例:其输入数据应该是 2022-01-01 00:00:00.000000000 +08:00;TIMESTAMP_LTZ 的时区信息不是携带在数据中的,而是由 Flink SQL 任务的全局配置决定的,我们可以由 table.local-time-zone 参数来设置时区。INTERVAL YEAR TO MONTH、 INTERVAL DAY TO SECOND:interval 的涉及到的种类比较多。INTERVAL 主要是用于给 TIMESTAMP、TIMESTAMP_LTZ 添加偏移量的。举例,比如给 TIMESTAMP 加、减几天、几个月、几年。二、复合数据类型数组类型:ARRAY、t ARRAY。数组最大长度为 2,147,483,647。t 代表数组内的数据类型。举例 ARRAY、ARRAY,其等同于 INT ARRAY、STRING ARRAY。Map 类型:MAP。Map 类型就和 Java 中的 Map 类型一样,key 是没有重复的。举例 Map、Map。集合类型:MULTISET、t MULTISET。就和 Java 中的 List 类型,一样,运行重复的数据。举例 MULTISET,其等同于 INT MULTISET。对象类型:ROW、ROW、ROW(n0 t0, n1 t1, ...>、ROW(n0 t0 'd0', n1 t1 'd1', ...)。就和 Java 中的自定义对象一样。举例:ROW(myField INT, myOtherField BOOLEAN),其等同于 ROW。三、用户自定义数据类型用户自定义类型就是运行用户使用 Java 等语言自定义一个数据类型出来。但是目前数据类型不支持使用 CREATE TABLE 的 DDL 进行定义,只支持作为函数的输入输出参数。
  • [交流吐槽] Redis消息队列发展历程
    作者 | 丕天Redis是目前最受欢迎的kv类数据库,当然它的功能越来越多,早已不限定在kv场景,消息队列就是Redis中一个重要的功能。Redis从2010年发布1.0版本就具备一个消息队列的雏形,随着10多年的迭代,其消息队列的功能也越来越完善,作为一个全内存的消息队列,适合应用与要求高吞吐、低延时的场景。我们来盘一下Redis消息队列功能的发展历程,历史版本有哪些不足,后续版本是如何来解决这些问题的。一、Redis 1.0 list从广义上来讲消息队列就是一个队列的数据结构,生产者从队列一端放入消息,消费者从另一端读取消息,消息保证先入先出的顺序,一个本地的list数据结构就是一个进程维度的消息队列,它可以让模块A写入消息,模块B消费消息,做到模块A/B的解耦与异步化。但想要做到应用级别的解耦和异步还需要一个消息队列的服务。1.list的特性Redis 1.0发布时就具备了list数据结构,应用A可以通过lpush写入消息,应用B通过rpop从队列中读取消息,每个消息只会被读取一次,而且是按照lpush写入的顺序读到。同时Redis的接口是并发安全的,可以同时有多个生产者向一个list中生产消息,多个消费者从list中读取消息。这里还有个问题,消费者要如何知道list中有消息了,需要不断轮询去查询吗。轮询无法保证消息被及时的处理,会增加延时,而且当list为空时,大部分轮询的请求都是无效请求,这种方式大量浪费了系统资源。好在Redis有brpop接口,该接口有一个参数是超时时间,如果list为空,那么Redis服务端不会立刻返回结果,它会等待list中有新数据后在返回或是等待最多一个超时时间后返回空。通过brpop接口实现了长轮询,该效果等同于服务端推送,消费者能立刻感知到新的消息,而且通过设置合理的超时时间,使系统资源的消耗降到很低。#基于list完成消息的生产和消费 #生产者生产消息msg1 lpush listA msg1 (integer) 1 #消费者读取到消息msg1 rpop listA "msg1" #消费者阻塞式读取listA,如果有数据立刻返回,否则最多等待10秒 brpop listA 10 1) "listA" 2) "msg1"使用rpop或brpop这样接口消费消息会先从队列中删除消息,然后再由应用消费,如果应用应用在处理消息前异常宕机了,消息就丢失了。但如果使用lindex这样的只读命令先读取消息处理完毕后在删除,又需要额外的机制来保证一条消息不会被其他消费者重复读到。好在list有rpoplpush或brpoplpush这样的接口,可以原子性的从一个list中移除一个消息并加入另一个list。应用程序可以通过2个list组和来完成消息的消费和确认功能,使用rpoplpush从list A中消费消息并移入list B,等消息处理完毕后在从list B中删除消息,如果在处理消息过程中应用异常宕机,恢复后应用可以重新从list B中读取未处理的消息并处理。这种方式为消息的消费增加了ack机制。#基于2个list完成消息消费和确认 #从listA中读取消息并写入listB rpoplpush listA listB "msg1" #业务逻辑处理msg1完毕后,从listB中删除msg1,完成消息的确认 lrem listB 1 msg1 (integer) 12.list的不足之处通过Redis 1.0就引入的list结构我们就能实现一个分布式的消息队列,满足一些简单的业务需求。但list结构作为消息队列服务有一个很致命的问题,它没有广播功能,一个消息只能被消费一次。而在大型系统中,通常一个消息会被下游多个应用同时订阅和消费,例如当用户完成一个订单的支付操作时,需要通知商家发货,要更新物流状态,可能还会提高用户的积分和等级,这些都是不同的下游子系统,他们全部会订阅支付完成的操作,而list一个消息只能被消费一次在这样复杂的大型系统面前就捉襟见肘了。可能你会说那弄多个list,生产者向每个list中都投递消息,每个消费者处理自己的list不就行了吗。这样第一是性能不会太好,因为同一个消息需要被重复的投递,第二是这样的设计违反了生产者和消费者解耦的原则,这个设计下生产者需要知道下游有哪些消费者,如果业务发生变化,需要额外增加一个消费者,生产者的代码也需要修改。3.总结优势模型简单,和使用本地list基本相同,适配容易通过brpop做到消息处理的实时性通过rpoplpush来联动2个list,可以做到消息先消费后确认,避免消费者应用异常情况下消息丢失不足消息只能被消费一次,缺乏广播机制二、Redis 2.0 pubsublist作为消息队列应用场景受到限制很重要的原因在于没有广播,所以Redis 2.0中引入了一个新的数据结构pubsub。pubsub虽然不能算作是list的替代品,但它确实能解决一些list不能解决的问题。1.pubsub特性pubsub引入一个概念叫channel,生产者通过publish接口投递消息时会指定channel,消费者通过subscribe接口订阅它关心的channel,调用subscribe后这条连接会进入一个特殊的状态,通常不能在发送其他请求,当有消息投递到这个channel时Redis服务端会立刻通过该连接将消息推送到消费者。这里一个channel可以被多个应用订阅,消息会同时投递到每个订阅者,做到了消息的广播。另一方面,消费者可以会订阅一批channel,例如一个用户订阅了浙江的新闻的推送,但浙江新闻还会进行细分,例如“浙江杭州xx”、“浙江温州xx”,这里订阅者不需要获取浙江的所有子类在挨个订阅,只需要调用psubscribe“浙江*”就能订阅所有以浙江开头的新闻推送了,这里psubscribe传入一个通配符表达的channel,Redis服务端按照规则推送所有匹配channel的消息给对应的客户端。#基于pubsub完成channel的匹配和消息的广播 #消费者1订阅channel1 subscribe channel1 1) "subscribe" 2) "channel1" 3) (integer) 1 #收到消息推送 1) "message" 2) "channel1" 3) "msg1" #消费者2订阅channel* psubscribe channel* 1) "psubscribe" 2) "channel*" 3) (integer) 1 #收到消息推送 1) "pmessage" 2) "channel*" 3) "channel1" 4) "msg1" 1) "pmessage" 2) "channel*" 3) "channel2" 4) "msg2" #生产者发布消息msg1和msg2 publish channel1 msg1 (integer) 2 publish channel2 msg2 (integer) 1在Redfis 2.8时加入了keyspace notifications功能,此时pubsub除了通知用户自定义消息,也可以通知系统内部消息。keyspace notifications引入了2个特殊的channel分别是__keyevent@__:和__keyspace@__:,通过订阅__keyevent客户端可以收到某个具体命令调用的回调通知,通过订阅__keyspace客户端可以收到目标key的增删改操作以及过期事件。使用这个功能还需要开启配置notify-keyspace-events。#通过keyspace notifications功能获取系统事件 #写入请求 set testkey v EX 1 #订阅key级别的事件 psubscribe __keyspace@0__:testkey 1) "psubscribe" 2) "__keyspace@0__:testkey" 3) (integer) 1 #收到通知 1) "pmessage" 2) "__keyspace@0__:testkey" 3) "__keyspace@0__:testkey" 4) "set" 1) "pmessage" 2) "__keyspace@0__:testkey" 3) "__keyspace@0__:testkey" 4) "expire" 1) "pmessage" 2) "__keyspace@0__:testkey" 3) "__keyspace@0__:testkey" 4) "expired" #订阅所有的命令事件 psubscribe __keyevent@0__:* 1) "psubscribe" 2) "__keyevent@0__:*" 3) (integer) 1 #收到通知 1) "pmessage" 2) "__keyevent@0__:*" 3) "__keyevent@0__:set" 4) "testkey" 1) "pmessage" 2) "__keyevent@0__:*" 3) "__keyevent@0__:expire" 4) "testkey" 1) "pmessage" 2) "__keyevent@0__:*" 3) "__keyevent@0__:expired" 4) "testkey"2.pubsub的不足之处pubsub既能单播又能广播,还支持channel的简单正则匹配,功能上已经能满足大部分业务的需求,而且这个接口发布的时间很早,在2011年Redis 2.0发布时就已经具备,用户基础很广泛,所以现在很多业务都有用到这个功能。但你要深入了解pubsub的原理后,是肯定不敢把它作为一个一致性要求较高,数据量较大系统的消息服务的。首先,pubsub的消息数据是瞬时的,它在Redis服务端不做保存,publish发送到Redis的消息会立刻推送到所有当时subscribe连接的客户端,如果当时客户端因为网络问题断连,那么就会错过这条消息,当客户端重连后,它没法重新获取之前那条消息,甚至无法判断是否有消息丢失。其次,pubsub中消费者获取消息是一个推送模型,这意味着Redis会按消息生产的速度给所有的消费者推送消息,不管消费者处理能力如何,如果消费者应用处理能力不足,消息就会在Redis的client buf中堆积,当堆积数据超过一个阈值后会断开这条连接,这意味着这些消息全部丢失了,在也找不回来了。如果同时有多个消费者的client buf堆积数据但又还没达到断开连接的阈值,那么Redis服务端的内存会膨胀,进程可能因为oom而被杀掉,这导致了整个服务中断。3.总结优势消息具备广播能力psubscribe能按字符串通配符匹配,给予了业务逻辑的灵活性能订阅特定key或特定命令的系统消息不足Redis异常、客户端断连都会导致消息丢失消息缺乏堆积能力,不能削峰填谷。推送的方式缺乏背压机制,没有考虑消费者处理能力,推送的消息超过消费者处理能力后可能导致消息丢失或服务异常三、Redis 5.0 stream消息丢失、消息服务不稳定的问题严重限制了pubsub的应用场景,所以Redis需要重新设计一套机制,来解决这些问题,这就有了后来的stream结构。1.stream特性一个稳定的消息服务需要具备几个要点,要保证消息不会丢失,至少被消费一次,要具备削峰填谷的能力,来匹配生产者和消费者吞吐的差异。在2018年Redis 5.0加入了stream结构,这次考虑了list、pubsub在应用场景下的缺陷,对标kafka的模型重新设计全内存消息队列结构,从这时开始Redis消息队列功能算是能和主流消息队列产品pk一把了。stream的改进分为多个方面成本:存储message数据使用了listpack结构,这是一个紧凑型的数据结构,不同于list的双向链表每个节点都要额外占用2个指针的存储空间,这使得小msg情况下stream的空间利用率更高。功能:stream引入了消费者组的概念,一个消费者组内可以有多个消费者,同一个组内的消费者共享一个消息位点(last_delivered_id),这使得消费者能够水平的扩容,可以在一个组内加入多个消费者来线性的提升吞吐,对于一个消费者组,每条msg只会被其中一个消费者获取和处理,这是pubsub的广播模型不具备的。不同消费者组之前是相互隔离的,他们各自维护自己的位点,这使得一条msg能被多个不同的消费者组重复消费,做到了消息广播的能力。stream中消费者采用拉取的方式,并能设置timeout在没有消息时阻塞,通过这种长轮询机制保证了消息的实时性,而且消费速率是和消费者自身吞吐相匹配。消息不丢失:stream的数据会存储在aof和rdb文件中,这使Redis重启后能够恢复stream的数据。而pubsub的数据是瞬时的,Redis重启意味着消息全部丢失。stream中每个消费者组会存储一个last_delivered_id来标识已经读取到的位点,客户端连接断开后重连还是能从该位点继续读取,消息不会丢失。stream引入了ack机制保证消息至少被处理一次。考虑一种场景,如果消费者应用已经读取了消息,但还没来得及处理应用就宕机了,对于这种已经读取但没有ack的消息,stream会标示这条消息的状态为pending,等客户端重连后通过xpending命令可以重新读取到pengind状态的消息,继续处理。如果这个应用永久宕机了,那么该消费者组内的其他消费者应用也能读取到这条消息,并通过xclaim命令将它归属到自己下面继续处理。#基于stream完成消息的生产和消费,并确保异常状态下消息至少被消费一次 #创建mystream,并且创建一个consumergroup为mygroup XGROUP CREATE mystream mygroup $ MKSTREAM OK #写入一条消息,由redis自动生成消息id,消息的内容是一个kv数组,这里包含field1 value1 field2 value2 XADD mystream * field1 value1 field2 value2 "1645517760385-0" #消费者组mygroup中的消费者consumer1从mystream读取一条消息,>表示读取一条该消费者组从未读取过的消息 XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream > 1) 1) "mystream" 2) 1) 1) "1645517760385-0" 2) 1) "field1" 2) "value1" 3) "field2" 4) "value2" #消费完成后ack确认消息 xack mystream mygroup 1645517760385-0 (integer) 1 #如果消费者应用在ack前异常宕机,恢复后重新获取未处理的消息id。 XPENDING mystream mygroup - + 10 1) 1) "1645517760385-0" 2) "consumer1" 3) (integer) 305356 4) (integer) 1 #如果consumer1永远宕机,其他消费者可以把pending状态的消息移动到自己名下后继续消费 #将消息id 1645517760385-0移动到consumer2下 XCLAIM mystream mygroup consumer2 0 1645517760385-0 1) 1) "1645517760385-0" 2) 1) "field1" 2) "value1" 3) "field2" 4) "value2"Redis stream保证了消息至少被处理一次,但如果想做到每条消息仅被处理一次还需要应用逻辑的介入。消息被重复处理要么是生产者重复投递,要么是消费者重复消费。对于生产者重复投递问题,Redis stream为每个消息都设置了一个唯一递增的id,通过参数可以让Redis自动生成id或者应用自己指定id,应用可以根据业务逻辑为每个msg生成id,当xadd超时后应用并不能确定消息是否投递成功,可以通过xread查询该id的消息是否存在,存在就说明已经投递成功,不存在则重新投递,而且stream限制了id必须递增,这意味了已经存在的消息重复投递会被拒绝。这套机制保证了每个消息可以仅被投递一次。对于消费者重复消费的问题,考虑一个场景,消费者读取消息后业务处理完毕,但还没来得及ack就发生了异常,应用恢复后对于这条没有ack的消息进行了重复消费。这个问题因为ack和消费消息的业务逻辑发生在2个系统,没法做到事务性,需要业务来改造,保证消息处理的幂等性。2.stream的不足stream的模型做到了消息的高效分发,而且保证了消息至少被处理一次,通过应用逻辑的改造能做到消息仅被处理一次,它的能力对标kafka,但吞吐高于kafka,在高吞吐场景下成本比kafka低,那它又有哪些不足了。首先消息队列很重要的一个功能就是削峰填谷,来匹配生产者和消费者吞吐的差异,生产者和消费者吞吐差异越大,持续时间越长,就意味着steam中需要堆积更多的消息,而Redis作为一个全内存的产品,数据堆积的成本比磁盘高。其次stream通过ack机制保证了消息至少被消费一次,但这有个前提就是存储在Redis中的消息本身不会丢失。Redis数据的持久化依赖aof和rdb文件,aof落盘方式有几种,通过配置appendfsync决定,通常我们不会配置为always来让每条命令执行完后都做一次fsync,线上配置一般为everysec,每秒做一次fsync,而rdb是全量备份时生成,这意味了宕机恢复可能会丢掉最近一秒的数据。另一方面线上生产环境的Redis都是高可用架构,当主节点宕机后通常不会走恢复逻辑,而是直接切换到备节点继续提供服务,而Redis的同步方式是异步同步,这意味着主节点上新写入的数据可能还没同步到备节点,在切换后这部分数据就丢失了。所以在故障恢复中Redis中的数据可能会丢失一部分,在这样的背景下无论stream的接口设计的多么完善,都不能保证消息至少被消费一次。3.总结优势在成本、功能上做了很多改进,支持了紧凑的存储小消息、具备广播能力、消费者能水平扩容、具备背压机制通过ack机制保证了Redis服务端正常情况下消息至少被处理一次的能力不足内存型消息队列,数据堆积成本高Redis本身rpo>0,故障恢复可能会丢数据,所以stream在Redis发生故障恢复后也不能保证消息至少被消费一次。四、Tair持久内存版 streamRedis stream的不足也是内存型数据库特性带来的,它拥有高吞吐、低延时,但大容量下成本会比较高,而应用的场景也不完全是绝对的大容量低吞吐或小容量高吞吐,有时应用的场景会介于二者之间,需要平衡容量和吞吐的关系,所以需要一个产品它的存储成本低于Redis stream,但它的性能又高于磁盘型消息队列。另一方面Redis stream在Redis故障场景下不能保证消息的不丢失,这导致业务需要自己实现一些复杂的机制来回补这段数据,同时也限制了它应用在一些对一致性要求较高的场景。为了让业务逻辑更简单,stream应用范围更广,需要保证故障场景下的消息持久化。兼顾成本、性能、持久化,这就有了Tair持久内存版。1.Tair持久内存版特性更大空间,更低成本Tair持久内存版引入了Intel傲腾持久内存(下面称作AEP),它的性能略低于内存,但相同容量下成本低于内存。Tair持久内存版将主要数据存储在AEP上,使得相同容量下,成本更低,这使同样单价下stream能堆积更多的消息。兼容社区版Tair持久内存版兼容原生Redis绝大部分的数据结构和接口,对于stream相关接口做到了100%兼容,如果你之前使用了社区版stream,那么不需要修改任何代码,只需要换一个连接地址就能切换到持久内存版。并且通过工具完成社区版和持久内存版数据的双向迁移。数据的实时持久化Tair持久内存版并不是简单将Redis中的数据换了一个介质存储,因为这样仅能通过AEP降低成本,但没用到AEP断电数据不丢失的特性,对持久化能力没有任何提升。开源Redis通过在磁盘上记录AppendOnlyLog来持久化数据,AppendOnlyLog记录了所有的写操作,相当于redolog,在宕机恢复时通过回放这些log恢复数据。但受限于磁盘介质的高延时和Redis内存数据库使用场景下对低延时的要求,并不能在每次写操作后fsync持久化log,最新写入的数据可能并没有持久化到磁盘,这也是数据可能丢失的根因。Tair持久内存版的数据恢复没有使用AppendOnlyLog来完成, 而是将将redis数据结构存储在AEP上,这样宕机后这些数据结构并不会丢失,并且对这些数据结构增加了一些额外的描述信息,宕机后在recovery时能够读到这些额外的描述信息,让这些redis数据结构重新被识别和索引,将状态恢复到宕机前的样子。Tair通过将redis数据结构和描述信息实时写入AEP,保证了写入数据的实时持久化。HA数据不丢失Tair持久内存版保证了数据的持久化,但生产环境中都是高可用架构,多数情况下当主节点异常宕机后并不会等主节点重启恢复,而是切换到备节点继续提供服务,然后给新的主节点添加一个新的备节点。所以在故障发生时如果有数据还没从主节点同步到备节点,这部分数据就会丢失。Redis采用的异步同步,当客户端写入数据并返回成功时对Redis的修改可能还没同步到备节点,如果此时主节点宕机数据就会丢失。为了避免在HA过程中数据丢失,Tair持久内存版引入了半同步机制,确保写入请求返回成功前相关的修改已经同步到备节点。可以发现开启半同步功能后写入请求的RT会变高,多出主备同步的耗时,这部分耗时大概在几十微秒。但通过一些异步化的技术,虽然写请求的RT会变高,但对实例的最大写吞吐影响很小。当开启半同步后生成者通过xadd投递消息,如果返回成功,消息一定同步到备节点,此时发生HA,消费者也能在备节点上读到这条消息。如果xadd请求超时,此时消息可能同步到备节点也可能没有,生产者没法确定,此时通过再次投递消息,可以保证该消息至少被消费一次。如果要严格保证消息仅被消费一次,那么生产者可以通过xread接口查询消息是否存在,对于不存在的场景重新投递。2.总结优势引入了AEP作为存储介质,目前Tair持久内存版价格是社区版的70%。保证了数据的实时持久化,并且通过半同步技术保证了HA不丢数据,大多数情况下做到消息不丢失(备库故障或主备网络异常时会降级为异步同步,优先保障可用性),消息至少被消费一次或仅被消费一次。五、未来消息队列主要是为了解决3类问题,应用模块的解耦、消息的异步化、削峰填谷。目前主流的消息队列都能满足这些需求,所以在实际选型时还会考虑一些特殊的功能是否满足,产品的性能如何,具体业务场景下的成本怎么样,开发的复杂度等。Redis的消息队列功能并不是最全面的,它不希望做成一个大而全的产品,而是做一个小而美的产品,服务好一部分用户在某些场景下的需求。目前用户选型Redis作为消息队列服务的原因,主要有Redis在相同成本下吞吐更高、Redis的延时更低、应用需要一个消息服务但又不想额外引入一堆依赖等。未来Tair持久内存版会针对这些述求,把这些优势继续放大。吞吐通过优化持久内存版的持久化流程,让吞吐接近内存版甚至超过内存版吞吐。延时通过rdma在多副本间同步数据,降低半同步下写入数据的延时。
  • [问题求助] 【abc产品】【设备指令配置】指令定义中参数配置为数组时如何配置
    【功能模块】Device  BO【操作步骤&问题现象】1、指令定义中入参如果为数组对象类型,如何配置?2、【截图信息】【日志信息】(可选,上传日志内容或者附件)
  • [交流讨论] 【4.28直播疑问解答】8.和nso很像,这类工具最致命的问题还是太复杂了,建模能力需要网络知识和数据结构知识,还要熟知yang
    问:和nso很像,这类工具最**的问题还是太复杂了,建模能力需要网络知识和数据结构知识,还要熟知yang,实际操作太难了。疑问来源:2022.4.28日直播  华为&南天:防火墙一键**,对星夜驰骋说NO直播回顾链接:https://devzone.huawei.com/cn/enterprise/aoc/videos/aoc-2022-05-262.html?id=134&number=1&from=allVideos回复:       yang 其实后面就是网络运维人员必备了,不过我们可以考虑集成些图形化界面帮助用户写yang模型。任何it 的低码自动化都是有模型的。比如terraform或者k8s,都是有自己的语言的。主要看是不是高效解决网络问题。