计算机 数组

这篇博客对数组的理解很深入,特别转发,原文地址:https://jiang-hao.com/articles/2020/backend-data-struct-array.html 定义 数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。 这个定义里有几个关键词,理解了这几个关键词,我想你就能彻底掌握数组的概念了。 首先是线性表(Linear List)。顾名思义,线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。其实除了数组,链表、队列、栈等也是线性表结构。 而与它相对立的概念是非线性表,比如二叉树、堆、图等。之所以叫非线性,是因为,在非线性表中,数据之间并不是简单的前后关系。 第二个是连续的内存空间和相同类型的数据。正是因为这两个限制,它才有了一个堪称“杀手锏”的特性:“随机访问”。但有利就有弊,这两个限制也让数组的很多操作变得非常低效,比如要想在数组中删除、插入一个数据,为了保证连续性,就需要做大量的数据搬移工作。 随机访问 我们拿一个长度为 10 的 int 类型的数组 int[] a = new int[10]来举例。在下面这个图中,计算机给数组 a[10],分配了一块连续内存空间 1000~1039,其中,内存块的首地址为 base_address = 1000。 我们知道,计算机会给每个内存单元分配一个地址,计算机通过地址来访问内存中的数据。当计算机需要随机访问数组中的某个元素时,它会首先通过下面的寻址公式,计算出该元素存储的内存地址: a[i]_address=base_address+i∗data_type_sizea[i]_address=base_address+i∗data_type_size 其中 data_type_size 表示数组中每个元素的大小。我们举的这个例子里,数组中存储的是 int 类型数据,所以 data_type_size 就为 4 个字节。 这里要特别纠正一个“错误”。在面试的时候,常常会问数组和链表的区别,很多人都回答说,“链表适合插入、删除,时间复杂度 O(1);数组适合查找,查找时间复杂度为 O(1)”。 实际上,这种表述是不准确的。数组是适合查找操作,但是查找的时间复杂度并不为 O(1)。即便是排好序的数组,你用二分查找,时间复杂度也是 O(logn)。所以,正确的表述应该是,数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。 低效的“插入”和“删除” 前面概念部分我们提到,数组为了保持内存数据的连续性,会导致插入、删除这两个操作比较低效。现在我们就来详细说一下,究竟为什么会导致低效?又有哪些改进方法呢? 插入操作 设数组的长度为 n,现在,如果我们需要将一个数据插入到数组中的第 k 个位置。为了把第 k 个位置腾出来,给新来的数据,我们需要将第 k~n 这部分的元素都顺序地往后挪一位。 如果在数组的末尾插入元素,那就不需要移动数据了,这时的时间复杂度为 O(1)。但如果在数组的开头插入元素,那所有的数据都需要依次往后移动一位,所以最坏时间复杂度是 O(n)。 因为我们在每个位置插入元素的概率是一样的,所以平均情况时间复杂度为 (1+2+…n)/n=O(n)。 如果数组中的数据是有序的,我们在某个位置插入一个新的元素时,就必须按照刚才的方法搬移 k 之后的数据。但是,如果数组中存储的数据并没有任何规律,数组只是被当作一个存储数据的集合。在这种情况下,如果要将某个数据插入到第 k 个位置,为了避免大规模的数据搬移,我们还有一个简单的办法就是,直接将第 k 位的数据搬移到数组元素的最后,把新的元素直接放入第 k 个位置。 为了更好地理解,我们举一个例子。假设数组 a[10]中存储了如下 5 个元素:a,b,c,d,e。 我们现在需要将元素 x 插入到第 3 个位置。我们只需要将 c 放入到 a[5],将 a[2]赋值为 x 即可。最后,数组中的元素如下: a,b,x,d,e,c。 利用这种处理技巧,在特定场景下,在第 k 个位置插入一个元素的时间复杂度就会降为 O(1)。这个处理思想在快排中也会用到。 删除操作 跟插入数据类似,如果我们要删除第 k 个位置的数据,为了内存的连续性,也需要搬移数据,不然中间就会出现空洞,内存就不连续了。 和插入类似,如果删除数组末尾的数据,则最好情况时间复杂度为 O(1);如果删除开头的数据,则最坏情况时间复杂度为 O(n);平均情况时间复杂度也为 O(n)。 实际上,在某些特殊场景下,我们并不一定非得追求数组中数据的连续性。如果我们将多次删除操作集中在一起执行,删除的效率是不是会提高很多呢? 我们继续来看例子。数组 a[10]中存储了 8 个元素:a,b,c,d,e,f,g,h。现在,我们要依次删除 a,b,c 三个元素。...

September 14, 2020

golang interface

interface Go语言里面设计最精妙的应该算interface,它让面向对象,内容组织实现非常的方便,当你看完这一章,你就会被interface的巧妙设计所折服。 什么是interface 简单的说,interface是一组method签名的组合,我们通过interface来定义对象的一组行为。 我们前面一章最后一个例子中Student和Employee都能SayHi,虽然他们的内部实现不一样,但是那不重要,重要的是他们都能say hi 让我们来继续做更多的扩展,Student和Employee实现另一个方法Sing,然后Student实现方法BorrowMoney而Employee实现SpendSalary。 这样Student实现了三个方法:SayHi、Sing、BorrowMoney;而Employee实现了SayHi、Sing、SpendSalary。 上面这些方法的组合称为interface(被对象Student和Employee实现)。例如Student和Employee都实现了interface:SayHi和Sing,也就是这两个对象是该interface类型。而Employee没有实现这个interface:SayHi、Sing和BorrowMoney,因为Employee没有实现BorrowMoney这个方法。 interface类型 interface类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口。详细的语法参考下面这个例子 type Human struct { name string age int phone string } type Student struct { Human //匿名字段Human school string loan float32 } type Employee struct { Human //匿名字段Human company string money float32 } //Human对象实现Sayhi方法 func (h *Human) SayHi() { fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) } // Human对象实现Sing方法 func (h *Human) Sing(lyrics string) { fmt.Println("La la, la la la, la la la la la...", lyrics) } //Human对象实现Guzzle方法 func (h *Human) Guzzle(beerStein string) { fmt.Println("Guzzle Guzzle Guzzle...", beerStein) } // Employee重载Human的Sayhi方法 func (e *Employee) SayHi() { fmt....

September 10, 2020

beego 的日志级别

看 beego 的源码,beego 的日志分为7个级别: const ( LevelEmergency = iota LevelAlert LevelCritical LevelError LevelWarning LevelNotice LevelInformational LevelDebug ) 转换为 LevelEmergency = 0 LevelAlert = 1 LevelCritical = 2 LevelError = 3 LevelWarning = 4 LevelNotice = 5 LevelInformational = 6 LevelDebug = 7

September 9, 2020

23中设计模式简介

设计模式主要分为三大类: 创建型模式 创建型模式关注对象的创建过程 工厂模式(Factory Method) 抽象工厂模式 单例模式 建造者模式 原型模式 结构型模式 结构型模式关注对象和类的组织 适配器模式 桥接模式 装饰模式 组合模式 外观模式 享元模式 代理模式 行为型模式 行为型模式关注系统中对象之间的相互交互,研究系统在运行时对象之间相互通信和协作,进一步明确对象的职责 模板方法模式 命令模式 迭代器模式 观察者模式 中介者模式 备忘录模式 解释器模式 状态模式 策略模式 职责链模式 访问者模式 详细说明 创建型模式 创建型模式关注对象的创建过程 1.抽象工厂模式 为一个产品族提供了统一的创建接口。当需要这个产品族的某一系列的时候,可以从抽象工厂中选出相应的系列创建一个具体的工厂类。 2.建造者模式 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。 3.工厂方法模式 定义一个接口用于创建对象,但是让子类决定初始化哪个类。工厂方法把一个类的初始化下放到子类。 4.原型模式 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。 5.单例模式 确保一个类只有一个实例,并提供对该实例的全局访问。 5.多例模式 确保一个类只有命名的实例,并提供对这些实例的全局访问。 对象池模式 通过回收利用对象避免获取和释放资源所需的昂贵成本。 惰性初始模式 推迟对象的创建、数据的计算等需要耗费较多资源的操作,只有在第一次访问的时候才执行。 资源获取为初始化 通过绑定到合适对象的生命周期来确保资源被适当地释放。 结构型模式 结构型模式关注对象和类的组织 6.适配器模式 将某个类的接口转换成客户端期望的另一个接口表示。适配器模式可以消除由于接口不匹配所造成的类兼容性问题。 7.桥接模式 将一个抽象与实现解耦,以便两者可以独立的变化。 8.组合模式 把多个对象组成树状结构来表示局部与整体,这样用户可以一样的对待单个对象和对象的组合。 9.装饰模式 向某个对象动态地添加更多的功能。修饰模式是除类继承外另一种扩展功能的方法。 10.外观模式 为子系统中的一组接口提供一个一致的界面, 外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。 11.享元 通过共享以便有效的支持大量小颗粒对象。 12.代理 为其他对象提供一个代理以控制对这个对象的访问。 行为型模式 行为型模式关注系统中对象之间的相互交互,研究系统在运行时对象之间相互通信和协作,进一步明确对象的职责 13.职责链 为解除请求的发送者和接收者之间耦合,而使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它。 14.命令 将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可取消的操作。 15.解释器 给定一个语言, 定义它的文法的一种表示,并定义一个解释器, 该解释器使用该表示来解释语言中的句子。 16.迭代器 提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。 17.中介者 包装了一系列对象相互作用的方式,使得这些对象不必相互明显作用,从而使它们可以松散偶合。当某些对象之间的作用发生改变时,不会立即影响其他的一些对象之间的作用,保证这些作用可以彼此独立的变化。 18.备忘录 备忘录对象是一个用来存储另外一个对象内部状态的快照的对象。备忘录模式的用意是在不破坏封装的条件下,将一个对象的状态捉住,并外部化,存储起来,从而可以在将来合适的时候把这个对象还原到存储起来的状态。 19.观察者模式 在对象间定义一个一对多的联系性,由此当一个对象改变了状态,所有其他相关的对象会被通知并且自动刷新。 20.状态 让一个对象在其内部状态改变的时候,其行为也随之改变。状态模式需要对每一个系统可能取得的状态创立一个状态类的子类。当系统的状态变化时,系统便改变所选的子类。 21.策略 定义一个算法的系列,将其各个分装,并且使他们有交互性。策略模式使得算法在用户使用的时候能独立的改变。 22.模板方法 模板方法模式准备一个抽象类,将部分逻辑以具体方法及具体构造子类的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。先构建一个顶级逻辑框架,而将逻辑的细节留给具体的子类去实现。 23.访问者 封装一些施加于某种数据结构元素之上的操作。一旦这些操作需要修改,接受这个操作的数据结构可以保持不变。访问者模式适用于数据结构相对未定的系统,它把数据结构和作用于结构上的操作之间的耦合解脱开,使得操作集合可以相对自由的演化。

September 8, 2020