二叉树的相关算法实现

定义树的节点 typedef struct TreeNode { int data; struct TreeNode *leftChild; struct TreeNode *RightChild; } TreeNode;创建树 /** * 1 * / \ * 2 3 * /\ / * 4 5 6 */ TreeNode *root = malloc(sizeof(TreeNode)); root->data = 1; TreeNode *node1 = malloc(sizeof(TreeNode)); node1->data = 2; TreeNode *node2 = malloc(sizeof(TreeNode)); node2->data = 3; TreeNode *node3 = malloc(sizeof(TreeNode)); node3->data = 4; node3->leftChild = NULL; node3->RightChild = NULL; TreeNode *node4 = malloc(sizeof(TreeNode)); node4->data = 5; node4->leftChild = NULL; node4->RightChild = NULL; TreeNode *node5 = malloc(sizeof(TreeNode)); node5->data = 6; node5->leftChild = NULL; node5->RightChild = NULL; root->leftChild = node1; root->RightChild = node2; node1->leftChild = node3; node1->RightChild = node4; node2->leftChild = node5; node2->RightChild= NULL;前序遍历 // 前序遍历 根->左->右 void preorderTraverse(TreeNode *tree, NSMutableArray *arrayM) { if(tree == NULL) {return;} [arrayM addObject:@(tree->data)];// 记录节点 preorderTraverse(tree->leftChild, arrayM); preorderTraverse(tree->RightChild, arrayM); }中序遍历//中序遍历 左->根->右 void midTraverse(TreeNode *tree, NSMutableArray *arrayM) { if(tree == NULL) {return;} midTraverse(tree->leftChild, arrayM); [arrayM addObject:@(tree->data)];// 记录节点 midTraverse(tree->RightChild, arrayM); }后序遍历 //后序遍历 左->右->根 void postorderTraversal(TreeNode *tree, NSMutableArray *arrayM) { if(tree == NULL) {return;} postorderTraversal(tree->leftChild, arrayM); postorderTraversal(tree->RightChild, arrayM); [arrayM addObject:@(tree->data)];// 记录节点 }输出遍历后的节点 NSMutableArray *arrayM = [NSMutableArray array]; preorderTraverse(root, arrayM); NSLog(@"%@",arrayM);// 124536 NSMutableArray *arrayM1 = [NSMutableArray array]; midTraverse(root, arrayM1); NSLog(@"%@",arrayM1);//425163 NSMutableArray *arrayM2 = [NSMutableArray array]; postorderTraversal(root, arrayM2); NSLog(@"%@",arrayM2);// 452631

June 27, 2019 · 1 min · jiezi

java数据结构三链表

链表是真正的动态的数据结构 缺点:丧失了随机访问的能力 (每一个节点在内存存放的位置是不一样的,不像数组,在内存中是一段连续的空间) 在链表head添加元素和在链表其它位置添加 逻辑是有区别的解决办法:dummyhead = new Node(null,null);size = 0;

June 25, 2019 · 1 min · jiezi

JavaScript来说数据结构

写在前面数据结构是工程师编程通识之一,不管你是JavaScript工程师,还是Java、python工程师,对数据结构理解够深,才能写出更简洁、更优雅的程序代码。这里我用JavaScript语言来写数据结构示例,希望对你有所帮助。什么是数据结构在计算机科学中,数据结构(data structure)是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。数据结构概念定义数据:是用来描述一种客观事物的符号,分为数据元素、数据对象、数据项等。结构:数据元素相互之间的关系,分为逻辑结构和存储结构两大类。数据逻辑结构:指数据元素之间的前后件关系,分为集合、线性结构、非线性结构等。数据存储结构:指数据的逻辑结构在计算机存储空间的存放形式,分为顺序结构、链式结构、索引结构、散列结构等。数据结构有哪些列表:一个存储元素的线性集合(collection),元素可以通过索引来任意存取,索引通常是数字,用来计算元素之间存储位置的偏移量。 示例代码 队列:用于存储按顺序排列的数据,先进先出。 示例代码 栈:一种高效的数据结构,数据只能在栈顶添加或删除,先进后出。 示例代码 链表:由一组节点组成的集合,每个节点都使用一个对象的引用指向它的后继。 示例代码 字典:以键-值对形式存储数据的数据结构。 示例代码 散列表:散列是一种常用的数据存储技术,散列后的数据可以快速地插入或取用。 示例代码 集合:一种包含不同元素的数据结构。集合中的成员是无序的,集合中不允许相同成员存在。 示例代码 树:一种非线性的数据结构,以分层的方式存储数据,被用来存储具有层级关系的数据。 示例代码 图:由边的集合及顶点的集合组成。 示例代码 上面对常用的9种数据结构做了一个简要的介绍。更好的理解数据结构,还是看图解、看示例源码比较好。参考资料https://book.douban.com/subject/25945449/https://book.douban.com/subject/27129352/https://www.cnblogs.com/shuoer/p/8424848.htmlhttps://segmentfault.com/a/1190000010343508

June 24, 2019 · 1 min · jiezi

TySheMo-前端数据管理模型

项目地址:https://github.com/tangshuang...使用文档:https://www.tangshuang.net/71...使用案例:https://github.com/tangshuang...TySheMo是一个前端数据(状态)管理工具。它面向复杂的前端数据管理,用于在前端结构化数据的不断变化中,清晰地控制数据变动,使数据变动不引起错误,并且方便表单数据的恢复、校验、格式化提交。 在需要对数据类型进行校验,数据变动引发界面变动,数据校验,格式化后输出数据,保证数据运算时得到想要的结果等一系列复杂的数据变化需求,TySheMo可以在项目中发挥不错的的作用。虽然它不追求性能,但是在确保数据类型和格式安全上,它具有非常优秀的思想,让对数据有强烈依赖的业务可以在复杂的逻辑中保证不出错。 类型检查系统虽然我们已经有typescript这样的类型检查系统,但是,typescript是在编译时进行类型检查的,在运行时,我们需要对一些动态的数据(特别是从后台api返回的数据)进行类型和格式检查。在支持graphql的系统中似乎不必这么担心,但目前大多数的restful应用,还是需要有一个工具去做这样的一件事。 TySheMo将数据类型的检查抽象出三个层面的对象:原型、类型、规则。它们分别在原子、结构、逻辑层面对数据进行校验,而且是在运行时。如果校验失败,你还可以通过抛出的错误得到更为详细的信息。 TySheMo内部提供了多个数据原型(类似基础数据类型)扩展,并且直接使用js标准库中的内置对象作为原型,免去需要用字符串来定义类型的麻烦。内置了Dict, List, Enum, Tuple, Range这几种类型。通过对后台api数据的检查,就可以即时避免由于api返回数据类型或结构引起的错误。同时,它还提供了更为丰富的类型检查规范,你可以阅读它的文档了解更多用法。 数据模式TySheMo提供了一种定义结构化数据的方式,用于对一个结构化数据进行规范化定义。数据模式是一个抽象的数据结构范型,它不是具体的数据,但规定了数据本身的结构规范。并且,作为工具库,TySheMo提供了Schema类,schema实例拥有根据定义规范数据的能力。 更为重要的是Schema是一种数据结构和行为逻辑约束的范例。行业里有json schema这样的先驱,我们在这些先行者的基础上,提炼出对业务有帮助的部分,形成一套类似后台数据库结构描述一样的定义语言,用以在前端去描述一个结构化,但随时变化的数据对象。 它基于数据类型检查系统,你需要在type属性传入对应的类型。对于Schema实例而言,它是无状态的,它所提供的接口是纯粹的工厂,不会产生任何内部资源。这样的设计,使得Schema有可能成为规范,在开发中/测试中,确保业务中给的数据是按照规范给定的,否则,就会抛出错误。 数据模型TySheMo提供了基于Schema的数据模型Model,是一个管理状态数据的容器。数据被放在数据模型中管理时,由于Schema的约束,数据不得不按照规范运行。它提供了当代数据响应的特性,你可以观察数据变化,从而来决定如何变动界面。 作为工具库,TySheMo从业务出发对数据模型的写作方式进行了约束。一个数据模型,是一个class,并且需要定义它的schema。要将数据放到模型中进行管理,你必须实例化模型,并且使用模型接口进行数据修改和格式化。但到最后,你会发现,模型的使用异常简单,真正复杂的部分,往往在于,如何通过Schema制定你的数据规范。 这只是对Model的数据响应最简单的一个演示,你还可以通过Model提供的能力,完成更多的事情。你可以把它和react, vue, angular结合使用,你可以在任何应用中使用它。特别是在表单数据管理上,TySheMo可以做到非常出色。关于Model的细节和更多特性,你可以阅读使用文档了解。 结语TySheMo不解决所有问题,而是专注将一个应用中的某个局部的数据管理做到极致。你从来没有体验过,同一个表单的业务逻辑,你不需要修改业务逻辑代码部分,而只需要修改UI交互的逻辑,就可以完全适应react, vue, angular。你也许不需要它,但也许也需要它,这取决于你如何在你但应用中管理你的数据。如果你对这个项目感兴趣,可以通过github参与项目。 (完)

June 23, 2019 · 1 min · jiezi

java数据结构二栈和队列

栈stack: 后进先出计算机程序中的撤回undo操作也是利用栈来完成的程序调用的系统栈: push pop peek getsize(), isEmpty() leetcode问题:括号匹配问题 队列Queue ![图片上传中...] 数组队列和循环队列的比较:主要是dequeue部分的性能差异,一个是O(n),一个是O(1)

June 21, 2019 · 1 min · jiezi

java数据结构一-数组array

数组最好写得支持泛型public class Array<T> { #T是自己自定义的一个类型 }java不支持直接new一个泛型,必须先new一个Object,然后前面进行类型转换data = (E[]) new Object[capacity] 动态数组:扩容部分if size == length : resize(2*data.length); private void resize(int newcapacity) { E[] newData = (E[]) new Object[newcapacity];for(int i=0;i<size;i++) { newdata[i] = data[i]; }data = newdata复杂度震荡问题:本来removelast,和addlast操作,均摊的时间复杂度是O(n),但是如果操作到了需要扩容或缩容的元素,频繁的进行,removelast,然后又addlast,这样一直是O(n)出现这样问题的原因呢:我们添加和删除时候的扩容太激进了,(too eager),应该元素个数变成总容量1/4的时候,我们只缩容到容量的一半,而不是过于激进,直接缩容到1/4

June 21, 2019 · 1 min · jiezi

Javascript中的树结构

前沿    前端中设计数据结构的方面不多,最常用的就是对树结构的一些操作。从某种意义上来说,前端工作本身就是和树结构打交道的一个工作方向。毕竟,DOM就是天然的树结构。所以如何能够良好地对树结构进行操作,是前端工程师不可或缺的一项能力。 树结构定义    什么是树结构呢?从数据结构的角度来讲: 树是非线性数据结构每个节点可能会有0个或多个后代每个节点具备唯一的父节点(如果有多个父节点,那就是图了)分类树根据节点的不同可以分为不同的类型,最常见的分类是: 二叉树二叉搜索树平衡二叉查找树红黑树具体他们之间的区别这里就不细说了,具体请查看详情 前端中常见的树结构DOM树结构下面的html结构就是一个天然的树结构。每个Dom节点下面,有0/1/多个子节点。 对象树结构数组形式特点: 每一个对象节点,下面可能会有children,也可能没有childrenlet obj = [ { id: 1, type: 'dom', children: [ { id: 2, type: 'html' } ] }, { id: 3, type: 'css', children: [ { id: 4, type: 'javascript' } ] }];对象形式最常见的就是抽象语法树: 特点: 对象的属性下面有不同的属性,每一个属性下面可能还会有不同的属性这种格式经常在数据统计中出现。 Javascript中树结构的遍历    其实在我看来,树的结构形式有很多种,但是,前端工作中很少涉及对树节点的修改等操作,大部分是遍历和统计数据。 需求场景:下面以Dom树结构为例: 1、需要输出每个节点的名称和节点深度 3、深度优先和广度优先都需要实现假定已经有了对应的树结构,子节点是childNodes(为啥不用children呢?自己去查吧)深度优先遍历深度优先遍历,又叫DFS(deep first search),遍历顺序是优先遍历节点的子节点,然后再是节点的兄弟节点。 递归输出function DeepSearch(node, deep = 0) { const child = node.childNodes; const { nodeName } = node; console.log(`name:${nodeName},deep:${deep}`); for(let i = 0, len = child.length; i < len; i++) { DeepSearch(child[i], deep + 1); }}非递归输出function deepSearch(node, deep = 0) { const stack = []; const deepArr = []; stack.push(node); deepArr.push(0); while(stack.length !== 0){ const node = stack.shift(); const deep = deepArr.shift(); const { nodeName } = node; console.log(`name:${nodeName},deep:${deep}`); const nodes = child.childNodes; for( let i = node.length; i > 0; i--) { stack.unshift(nodes[i]); deep.unshift(deep + 1); } }}广度优先遍历广度优先,正好和深度优先策略相反,先遍历节点的兄弟节点,再遍历子节点。 ...

June 16, 2019 · 2 min · jiezi

基础数据结构及js数据存储

        因为以前前端开发跟数据存储打交道比较少,javascript又具有自动垃圾回收机制。数据结构以及存储相关的概念,其实是很容易被前端er忽略的。但是因为现在大前端的趋势,其实慢慢地,这些概念对于一个前端er来说也成了必须要掌握的技巧。        了解这些概念,对于我们去理解基本数据类型,引用数据类型,闭包,原型,原型链,事件循环等都有很好的促进作用。        接下来,我们先了解堆(heap),栈(stack),队列(queue)这三种数据结构,再来分析js数据存储相关的概念。        1 数据结构         1.1 栈 栈是一种先进后出的数据结构。        数据进入栈中之后,会被压到栈底。类似于我们平常用的羽毛球球管的概念,第一个进去的是在球管的管低,第一个出来的是位于球管管顶的最后一个进去的羽毛球。 这个概念会在我们之后需要讲到的执行上下文中用到。                 1.2 堆 是一种树状的数据结构,跟书架类似。        我们在书架取书的时候是不需要知道书的内容的,只需要知道书名就知道需要取的是哪本书了。         1.3 队列 是一种先进先出(FIFO)的数据结构。        就像我们过安检,谁排第一个谁就第一个接受安检。这块的概念主要是在事件循环机制中用到,可以更好的帮我们理解事件循环机制。         好啦,介绍完我们的基本数据结构,接下来就要详细介绍js中的数据存储方式了。         2 js数据存储         2.1 基础数据类型及变量对象         我们都知道js中基础数据类型包括undefined,null,boolean,string,number。这些数据类型都是存储在变量对象中的,我们都是按值访问,可以直接操作保存在变量中的值。 ...

June 15, 2019 · 1 min · jiezi

走进KeyDB

KeyDB项目是从redis fork出来的分支。众所周知redis是一个单线程的kv内存存储系统,而KeyDB在100%兼容redis API的情况下将redis改造成多线程。 项目git地址:https://github.com/JohnSully/KeyDB 网上公开的技术细节比较少,本文基本是通过阅读源码总结出来的,如有错漏之处欢迎指正。 多线程架构线程模型KeyDB将redis原来的主线程拆分成了主线程和worker线程。每个worker线程都是io线程,负责监听端口,accept请求,读取数据和解析协议。如图所示: KeyDB使用了SO_REUSEPORT特性,多个线程可以绑定监听同个端口。每个worker线程做了cpu绑核,读取数据也使用了SO_INCOMING_CPU特性,指定cpu接收数据。解析协议之后每个线程都会去操作内存中的数据,由一把全局锁来控制多线程访问内存数据。主线程其实也是一个worker线程,包括了worker线程的工作内容,同时也包括只有主线程才可以完成的工作内容。在worker线程数组中下标为0的就是主线程。主线程的主要工作在实现serverCron,包括: 处理统计客户端链接管理db数据的resize和reshard处理aofreplication主备同步cluster模式下的任务链接管理在redis中所有链接管理都是在一个线程中完成的。在KeyDB的设计中,每个worker线程负责一组链接,所有的链接插入到本线程的链接列表中维护。链接的产生、工作、销毁必须在同个线程中。每个链接新增一个字段int iel; /* the event loop index we're registered with */用来表示链接属于哪个线程接管。KeyDB维护了三个关键的数据结构做链接管理: clients_pending_write:线程专属的链表,维护同步给客户链接发送数据的队列clients_pending_asyncwrite:线程专属的链表,维护异步给客户链接发送数据的队列clients_to_close:全局链表,维护需要异步关闭的客户链接分成同步和异步两个队列,是因为redis有些联动api,比如pub/sub,pub之后需要给sub的客户端发送消息,pub执行的线程和sub的客户端所在线程不是同一个线程,为了处理这种情况,KeyDB将需要给非本线程的客户端发送数据维护在异步队列中。同步发送的逻辑比较简单,都是在本线程中完成,以下图来说明如何同步给客户端发送数据: 如上文所提到的,一个链接的创建、接收数据、发送数据、释放链接都必须在同个线程执行。异步发送涉及到两个线程之间的交互。KeyDB通过管道在两个线程中传递消息: int fdCmdWrite; //写管道int fdCmdRead; //读管道本地线程需要异步发送数据时,先检查client是否属于本地线程,非本地线程获取到client专属的线程ID,之后给专属的线程管到发送AE_ASYNC_OP::CreateFileEvent的操作,要求添加写socket事件。专属线程在处理管道消息时将对应的请求添加到写事件中,如图所示: redis有些关闭客户端的请求并非完全是在链接所在的线程执行关闭,所以在这里维护了一个全局的异步关闭链表。 锁机制KeyDB实现了一套类似spinlock的锁机制,称之为fastlock。fastlock的主要数据结构有: struct ticket{ uint16_t m_active; //解锁+1 uint16_t m_avail; //加锁+1};struct fastlock{ volatile struct ticket m_ticket; volatile int m_pidOwner; //当前解锁的线程id volatile int m_depth; //当前线程重复加锁的次数};使用原子操作__atomic_load_2,__atomic_fetch_add,__atomic_compare_exchange来通过比较m_active=m_avail判断是否可以获取锁。fastlock提供了两种获取锁的方式: try_lock:一次获取失败,直接返回lock:忙等,每1024 * 1024次忙等后使用sched_yield 主动交出cpu,挪到cpu的任务末尾等待执行。在KeyDB中将try_lock和事件结合起来,来避免忙等的情况发生。每个客户端有一个专属的lock,在读取客户端数据之前会先尝试加锁,如果失败,则退出,因为数据还未读取,所以在下个epoll_wait处理事件循环中可以再次处理。 Active-ReplicaKeyDB实现了多活的机制,每个replica可设置成可写非只读,replica之间互相同步数据。主要特性有: 每个replica有个uuid标志,用来去除环形复制新增加rreplay API,将增量命令打包成rreplay命令,带上本地的uuidkey,value加上时间戳版本号,作为冲突校验,如果本地有相同的key且时间戳版本号大于同步过来的数据,新写入失败。采用当前时间戳向左移20位,再加上后44位自增的方式来获取key的时间戳版本号。结束语云数据库Redis版(ApsaraDB for Redis)是一种稳定可靠、性能卓越、可弹性伸缩的数据库服务。基于飞天分布式系统和全SSD盘高性能存储,支持主备版和集群版两套高可用架构。提供了全套的容灾切换、故障迁移、在线扩容、性能优化的数据库解决方案。 本文作者:羽洵阅读原文 本文为云栖社区原创内容,未经允许不得转载。

June 14, 2019 · 1 min · jiezi

数据结构的JavaScript描述

《数据结构与算法JavaScript描述》,有一种花了正版的钱买了盗版的书的感觉。花了点时间整理了一下,可以保证都能跑通。对着截图敲一遍,比复制黏贴效果好。 (点开图片加载大图需要时间) 列表 栈 队列 链表 字典 散列 集合 二叉查找树 图

June 7, 2019 · 1 min · jiezi

揭秘闲鱼拉新投放系统如何设计

背景闲鱼目前已经是国内最大的闲置物品交易平台。随着闲鱼体量的增长和用户规模不断扩大,闲鱼App上的一个普通banner抑或是feeds中的一张普通的卡片,每天都可能被数以千万计的人看到。 为了更好地服务好广大的用户群体,更加个性化的内容推荐和更加精细化的素材投放就显得尤为必要了。今天我们来聊一聊如何设计一个可以精准触达用户、运营快速试错、解放开发生产力的投放系统。 思路分析投放是什么?举例来说,往城市广场的一块广告牌上在不同时段不同场景下更换广告画就是一种投放,当然互联网技术带来了人的维度,不同用户看到的广告画可能也是不一样的。我们来看下这样一个系统应该包含哪些功能。 1、我们把“城市广场上的那块广告牌”叫资源位,那么需要一个服务端接口来获取需要透出的素材。2、不同资源位需要透出的素材格式可能是不一样的,可能是banner,可能是feeds,可能是运营自定义的手填数据,可能是任何合理的数据结构。3、同一个资源位,不同时段,针对不同平台、不同人群,透出的素材可能是不一样的,那么就需要有一个服务来在一堆素材中筛选出适合资源位的内容。在资源位命中了多个素材的时候,还需要有一些机制来裁决出最终透出的那一个。 详细设计我们设计的投放系统扮演的是前端实体资源位和后端多种数据源之间的桥梁的角色。它负责从各个业务数据源中根据一定规则筛选出在特定资源位上需要透出的数据,基本的数据流如下图所示: 资源位所谓资源位,在我们这个体系内,是指前端页面上的实体坑位。是技术同学在产品开发中创建的。理所当然,资源位需要消费的数据结构是在开发阶段就确定了,比如banner、feeds或者结构非常灵活的手填数据等。 在我们这个体系里,我们用一个 schema 描述资源位需要消费的数据结构。 这个 schema 是用 json 描述的。技术同学在前端页面上开发实体资源位后,需要在我们的系统中创建对应的虚拟资源位,并通过一个图形化的 json schema 编辑器来定义这个资源位需要消费的数据结构 投放物料上述 schema 定义了一个资源位所需要消费的数据的格式。但是光有 schema 是不够的,因为资源位要消费的数据,而不是数据结构本身。在我们的系统中,我们用一个动态表单模块根据schema生成动态的表单,产品运营同学通过动态表单生产的数据,我们称之为投放物料。资源位消费的就是投放物料。 对于一些手填数据,表单直接产生的数据就是资源位可用的了。但是对于 Feeds 之类的,表单往往只能定义 Feeds 的一些诸如选品等特征字段。对于这类特殊类型的数据源,服务端就不能简单的直接返回数据了,需要根据这些特征字段,做一些数据查询和数据解析工作,再返回给前端一个完整规范的数据。 投放单元前述文章说到,同一个banner,可能对新用户投放的是红包,对年轻男孩子投放的是手机数码内容,对年轻女孩子投放的是美妆服饰。我们把这个连接了资源位、投放物料与多个投放因子的桥梁叫做投放单元。 那么投放单元需要有多少个投放因子呢?其实是视业务而定的,我们认为基础的投放因为应该包含 投放时段、投放人群、投放平台、投放AB配置等。 当资源位向投放系统发起请求拉取数据时,投放系统在这个资源位上挂载的所有投放单元中根据投放因子筛选出命中的投放单元,最后将命中的投放单元上挂载的投放物料返回给前端的投放资源位。当命中了多个投放单元时,需要有些方法来裁决出最终胜出的那一个。这个方法简单点做,可以在投放单元中配一个权重,筛选时最后选择权重高的那个,也可以引入算法决策,根据投放的 ctr 数据做排序。 投放计划投放计划是产品运营对多个资源位管理形式。简单来说,一个投放计划下,可以挂载多个关联的资源位。试想一下,一次大促活动可能涉及到几十个资源位的投放,将这些资源位组织到同一个投放计划中进行管理,可以更加方便操作以及查看投放效果。 端侧接入对于前端来说,我们希望通过提供一个封装的npm包,通过简单调用,传入resourceId(资源位ID) 即可获取数据。 这种调用方式对业务调用方来说是比较优雅的,但是对页面性能来说却是不省心的。因为一个页面往往由很多个资源位组成,每个资源位单独发起请求就会形成大量的并发请求,不仅页面性能会降低,还会对服务器产生比较大的qps压力。 针对这种情况,我们做了一个小优化。服务端提供一个批量查询的接口,前端SDK内部,每10ms 对模块的请求调用做一次聚合,将单个资源位的数据获取转化成批量的查询。负面影响是对部分资源位的数据加载造成最大10ms的延时,优点是提升了页面整体的性能,有效减小了服务端QPS压力。 效果上述投放系统在我们的拉新业务实践中run得非常好,已为闲鱼应用内的数百个资源位提供投放能力支持,每天服务数以千万记的闲鱼用户。既实现了资源位的精细化投放,提高了单个资源位的利用率,又赋能运营更自由地进行各种拉新投放实验,减小试错成本,还减少了技术同学频繁参与运营实验改造的开发工作量,解放了技术同学的生产力。 总结上述文章介绍了一个简易的投放系统的设计思路,本质上是一个连接前端实体资源位和服务端多种数据源的桥梁的设计。 其中有很多能力是依赖了团队内部其它同学努力的成果,比如:1、描述资源位数据结构的 json schema如何设计2、根据json schema动态生成的表单怎么实现3、人群校验的服务和能力4、AB测试的能力5、feeds 的选品服务4、个性化动态banner能力 还有很多可以优化的点,比如数据回流如何做得更好,怎样引入算法能力对策略筛选进行优化。。。持续优化、持续为业务创造价值是我们一直坚持努力的方向。 本文作者:闲鱼技术-长玖阅读原文 本文为云栖社区原创内容,未经允许不得转载。

June 5, 2019 · 1 min · jiezi

Leetcode刷完31道链表题的一点总结

前几天第一次在 Segmentfault 发文—JavaScript:十大排序的算法思路和代码实现,发现大家似乎挺喜欢算法的,所以今天再分享一篇前两个星期写的 Leetcode 刷题总结,希望对大家能有所帮助。 本文首发于我的blog 前言 今天终于刷完了 Leetcode 上的链表专题,虽然只有 31 道题(总共是 35 道,但有 4 道题加了锁)而已,但也陆陆续续做了两三个星期,严重跟不上原先计划啊。本来打算数据结构课程老师讲完一个专题,我就用 JS 在 Leetcode 做一个专题的。然而老师现在都讲到图了,而我连二叉树都还没刷 Orz(附上一张 AC 图,看着还是挺有成就感的嘛)。 先写一篇博客总结一下这阵子刷链表题的收获吧,有输入也要有输出。这里就不花篇幅介绍链表的一些基本概念了,不清楚的看官就自行谷歌一下吧,本文主要介绍一些常见的链表题和解题思路。文中提到的 Leetcode 题目都有给出题目链接以及相关解题代码,使用其他方法的解题代码,或者更多 Leetcode 题解可以访问我的GitHub 算法仓库。 正文缓存 不得不说使用数组 / map 来缓存链表中结点的信息是解决链表题的一大杀器,覆盖问题的范围包括但不限于:在链表中插入 / 删除结点、反向输出链表、链表排序、翻转链表、合并链表等,Leetcode 上 31 道链表绝大部分都可以使用这种方法解题。具体实现思路是先使用一个数组或者 map 来存储链表中的结点信息,比如结点的数据值等,之后根据题目要求对数组进行相关操作后,再重新把数组元素做为每一个结点连接成链表返回即可。虽然使用缓存来解链表题很 dirty,有违链表题的本意,而且空间复杂度也达到了 O(n)(即使我们常常用空间来换时间,不过还是能避免就避免吧),但这种方法的确很简单易懂,看完题目后几乎就可以马上动手不加思考地敲代码一次 AC 了,不像常规操作那样需要去考虑到很多边界情况和结点指向问题。 当然,并不是很提倡这种解法,这样就失去了做链表题的意义。如果只是一心想要解题 AC 的话那无妨。否则的话我建议可以使用数组缓存先 AC 一遍题,再使用常规方法解一次题,我个人就是这么刷链表题的。甚至使用常规方法的话,你还可以分别使用迭代和递归来解题,迭代写起来比较容易,而递归的难点在于把握递归边界和递归式,但只要理解清楚了的话,递归的代码写起来真的很少啊(后面会说到)。 先找道题 show the code 吧,不然只是单纯的说可能会半知半解。比如这道反转链表 II:反转从位置 m 到 n 的链表。如果使用数组缓存的话,这道题就很容易了。只需要两次遍历链表,第一次把从 m 到 n 的结点值缓存到一个数组中,第二次遍历的时候再替换掉链表上 m 到 n 的结点的值就可以了(是不是很简单很清晰啊,如果使用常规方法的话就复杂得多了)。实现代码如下: ...

May 29, 2019 · 3 min · jiezi

Leetcode130-被包围的区域

题目给定一个二维的矩阵,包含 'X' 和 'O'(字母 O)。 找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。 示例: X X X XX O O XX X O XX O X X运行你的函数后,矩阵变为: X X X XX X X XX X X XX O X X解释: 被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。 任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。 题解这道题我们拿到基本就可以确定是图的dfs、bfs遍历的题目了。题目中解释说被包围的区间不会存在于边界上,所以我们会想到边界上的o要特殊处理,只要把边界上的o特殊处理了,那么剩下的o替换成x就可以了。问题转化为,如何寻找和边界联通的o,我们需要考虑如下情况。 X X X XX O O XX X O XX O O X这时候的o是不做替换的。因为和边界是连通的。为了记录这种状态,我们把这种情况下的o换成#作为占位符,待搜索结束之后,遇到o替换为x(和边界不连通的o);遇到#,替换回o(和边界连通的o)。 如何寻找和边界联通的o? 从边界出发,对图进行dfs和bfs即可。这里简单总结下dfs和dfs。 bfs递归。可以想想二叉树中如何递归的进行层序遍历。bfs非递归。一般用队列存储。dfs递归。最常用,如二叉树的先序遍历。dfs非递归。一般用stack。那么基于上面这种想法,我们有四种方式实现。 ...

May 26, 2019 · 6 min · jiezi

Python数据结构

概述    数据结构是组织数据的方式,以便能够更好的存储和获取数据。数据结构定义数据之间的关系和对这些数据的操作方式。数据结构屏蔽了数据存储和操作的细节,让程序员能更好的处理业务逻辑,同时拥有快速的数据存储和获取方式。     在这篇文章中,你将了解到多种数据结构以及这些数据结构在Python中实现的方式。 抽象数据类型和数据结构    数据结构是抽象数据类型(ADT)的实现,通常,是通过编程语言提供的基本数据类型为基础,结合相应的代码来实现。     通常来说,数据结构分为两类:原始数据结构和非原始数据结构,原始数据结构是用来表示简单的数据关系,非原始数据结构包含原始数据结构,同时,数据关系更加复杂,数据操作也更加复杂。 原始数据结构    原始数据结构 - 顾名思义 - 是最原始的或基本的数据结构。 它们是数据操作的构建块,包含纯粹,简单的数据值。 Python有四种原始变量类型: IntegersFloatStringsBooleanIntegers    您可以使用Integers表示数字数据,具体地说,可以使用从负无穷大到无穷大的整数 Float    “Float”代表“浮点数”。 您可以将它用于有理数,通常以十进制数字结尾,例如1.11或3.14。     请注意,在Python中,您不必显式声明变量或数据的类型。 那是因为Python是一种动态类型语言。 动态类型语言是对象可以存储的数据类型是可变的语言。 String    String是字母,单词或其他字符的集合。 在Python中,您可以通过在一对单引号或双引号中包含一系列字符来创建字符串。 例如:'cake',“cookie”等。您还可以对两个或多个字符串应用+操作来连接它们,就像下面的示例中一样: x = 'Cake'y = 'Cookie'x + ' & ' + y #结果:'Cake & Cookie'以下是您可以使用字符串执行的一些其他基本操作; 例如,您可以使用*重复字符串一定次数: # Repeatx * 2 #结果:'CakeCake'您还可以切割字符串 z1 = x[2:] print(z1)z2 = y[0] + y[1] print(z2)# 结果keCo请注意,字符串也可以是字母数字字符,但+操作仍然用于连接字符串。 x = '4'y = '2'x + y'42'Python有许多内置方法或辅助函数来操作字符串。 替换子字符串,大写段落中的某些单词,在另一个字符串中查找字符串的位置是一些常见的字符串操作。 例如: 大写首字母str.capitalize('cookie')'Cookie'以字符为单位检索字符串的长度。 请注意,空格也计入最终结果:str1 = "Cake 4 U"str2 = "404"len(str1)8检查字符串是否数字str1 = "Cake 4 U"str2 = "404"str1.isdigit()Falsestr2.isdigit()True替换str1 = "Cake 4 U"str2 = "404"str1.replace('4 U', str2)'Cake 404'查找子字符串str1 = 'cookie'str2 = 'cook'str1.find(str2)0str1 = 'I got you a cookie'str2 = 'cook'str1.find(str2)12Boolean    这种内置数据类型值为:True和False,这通常使它们可以与整数1和0互换。布尔值在条件和比较表达式中很有用,就像在下面的例子中一样: ...

May 18, 2019 · 4 min · jiezi

etcd-在超大规模数据场景下的性能优化

作者 | 阿里云智能事业部高级开发工程师 陈星宇(宇慕)概述etcd是一个开源的分布式的kv存储系统, 最近刚被cncf列为沙箱孵化项目。etcd的应用场景很广,很多地方都用到了它,例如kubernetes就用它作为集群内部存储元信息的账本。本篇文章首先介绍我们优化的背景,为什么我们要进行优化, 之后介绍etcd内部存储系统的工作方式,之后介绍本次具体的实现方式及最后的优化效果。 优化背景由于阿里巴巴内部集群规模大,所以对etcd的数据存储容量有特殊需求,之前的etcd支持的存储大小无法满足要求, 因此我们开发了基于etcd proxy的解决方案,将数据转储到了tair中(可类比redis))。这种方案虽然解决了数据存储容量的问题,但是弊端也是比较明显的,由于proxy需要将数据进行搬移,因此操作的延时比原生存储大了很多。除此之外,由于多了tair这个组件,运维和管理成本较高。因此我们就想到底是什么原因限制了etcd的存储容量,我们是否可以通过技术手段优化解决呢? 提出了如上问题后我们首先进行了压力测试不停地像etcd中注入数据,当etcd存储数据量超过40GB后,经过一次compact(compact是etcd将不需要的历史版本数据删除的操作)后发现put操作的延时激增,很多操作还出现了超时。监控发现boltdb内部spill操作(具体定义见下文)耗时显著增加(从一般的1ms左右激增到了8s)。之后经过反复多次压测都是如此,每次发生compact后,就像世界发生了停止,所有etcd读写操作延时比正常值高了几百倍,根本无法使用。 etcd内部存储工作原理etcd存储层可以看成由两部分组成,一层在内存中的基于btree的索引层,一层基于boltdb的磁盘存储层。这里我们重点介绍底层boltdb层,因为和本次优化相关,其他可参考上文。 etcd中使用boltdb作为最底层持久化kv数据库,boltdb的介绍如下: Bolt was originally a port of LMDB so it is architecturally similar. Both use a B+tree, have ACID semantics with fully serializable transactions, and support lock-free MVCC using a single writer and multiple readers.Bolt is a relatively small code base (<3KLOC) for an embedded, serializable, transactional key/value database so it can be a good starting point for people interested in how databases work。如上介绍,它短小精悍,可以内嵌到其他软件内部,作为数据库使用,例如etcd就内嵌了boltdb作为内部存储k/v数据的引擎。boltdb的内部使用B+ tree作为存储数据的数据结构,叶子节点存放具体的真实存储键值。它将所有数据存放在单个文件中,使用mmap将其映射到内存,进行读取,对数据的修改利用write写入文件。数据存放的基本单位是一个page, 大小默认为4K. 当发生数据删除时,boltdb不直接将删掉的磁盘空间还给系统,而是内部将他先暂时保存,构成一个已经释放的page池,供后续使用,这个所谓的池在boltdb内叫freelist。例子如下: ...

May 15, 2019 · 2 min · jiezi

Leetcode120三角形最小路径和

题目给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。 例如,给定三角形: [ [2], [3,4], [6,5,7], [4,1,8,3]]自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。 说明: 如果你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题,那么你的算法会很加分。 题解这道题目和之前A过的杨辉三角差不多,一看就是动态规划。动态规划最主要的是确定状态表达式。而要求在o(n)的空间复杂度来解决这个问题,最主要的是要想清楚,更新状态的时候,不破坏下一次计算需要用到的状态。我们采用"bottom-up"的动态规划方法来解本题。 状态表达式为:dp[j] = min(dp[j], dp[j+1]) + triangle[j]; class Solution { public int minimumTotal(List<List<Integer>> triangle) { int row = triangle.size(); List<Integer> res = new LinkedList<>(triangle.get(row - 1)); for (int i = row - 2; i >= 0; i--) { List<Integer> currentRow = triangle.get(i); for (int j = 0; j < currentRow.size(); j++) { res.set(j, Math.min(res.get(j), res.get(j + 1)) + currentRow.get(j)); } } return res.get(0); }}热门阅读百度社招面试题——Redis实现分布式锁【Leetcode】114. 二叉树展开为链表社招面试总结——算法题篇Redis中的集合类型是怎么实现的? ...

May 12, 2019 · 1 min · jiezi

AVL树的Java实现

定义Wikipedia - AVL树 在计算机科学中,AVL树是最早被发明的自平衡二叉查找树。在AVL树中,任一节点对应的两棵子树的最大高度差为1,因此它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下的时间复杂度都是 {displaystyle O(log {n})} O(log{n})。增加和删除元素的操作则可能需要借由一次或多次树旋转,以实现树的重新平衡。AVL树得名于它的发明者G. M. Adelson-Velsky和Evgenii Landis,他们在1962年的论文《An algorithm for the organization of information》中公开了这一数据结构。理论实现AVL树的要点为:每次新增/删除节点后判断平衡性然后通过调整使整棵树重新平衡 判断平衡性:每次新增/删除节点后,刷新受到影响的节点的高度,即可通过任一节点的左右子树高度差判断其平衡性 调整:通过对部分节点的父子关系的改变使树重新平衡 实现基本结构public class Tree<T extends Comparable<T>> { private static final int MAX_HEIGHT_DIFFERENCE = 1; private Node<T> root; class Node<KT> { KT key; Node<KT> left; Node<KT> right; int height = 1; public Node(KT key, Node<KT> left, Node<KT> right) { this.key = key; this.left = left; this.right = right; } }}插入(insert)四种不平衡范型对于任意一次插入所造成的不平衡,都可以简化为下述四种范型之一: ...

May 7, 2019 · 7 min · jiezi

递归和尾递归的运行流程解释

递归和尾递归的运行流程解释递归定义递归(英语:recursion)在计算机科学中是指一种通过重复将问题分解为同类的子问题而解决问题的方法。[1] 递归式方法可以被用于解决很多的计算机科学问题,因此它是计算机科学中十分重要的一个概念。[2] 绝大多数编程语言支持函数的自调用,在这些语言中函数可以通过调用自身来进行递归。计算理论可以证明递归的作用可以完全取代循环,因此有很多在函数编程语言(如Scheme)中用递归来取代循环的例子。(摘自维基百科) 尾递归定义在计算机学里,尾调用是指一个函数里的最后一个动作是返回一个函数的调用结果的情形,即最后一步新调用的返回值直接被当前函数的返回结果。[1]此时,该尾部调用位置被称为尾位置。尾调用中有一种重要而特殊的情形叫做尾递归。经过适当处理,尾递归形式的函数的运行效率可以被极大地优化。[1]尾调用原则上都可以通过简化函数调用栈的结构而获得性能优化(称为“尾调用消除”),但是优化尾调用是否方便可行取决于运行环境对此类优化的支持程度如何。(摘自维基百科) 前提知识递归我将它分为两个过程,一个我将它称为递归,另一个我将它称为回溯. 递归的函数的运行主要有这两个流程,递归的进入,回溯的退出,这两个过程的分界是以递归出口为分界的.递归的实现形式是使用栈,递归函数的进入(递归)类似与压栈,递归函数的退出(回溯)类似于出栈.递归样例和解释【编程题】幂运算三(递归函数)题目ID:1137【问题描述】 求x^n。【输入形式】一行2个数,第一个整数表示x,第二个大于等于零的整数表示n,二数之间用空格分隔。【输出形式】一行一个整数,表示x的n次方【样例输入】2 3【样例输出】8 【样例说明】2的3次方结果为8【评分标准】5组测试用例,每组2分,共计10分 【测试用例】1)输入:2 3输出:82)输入:3 5输出:243 3)输入:-17 4输出:83521 4)输入:22 0输出:1 5)输入:-1287 0输出:1 //普通递归#include<stdio.h>long my_pow1(long x,int n){ if(n==0) return 1; //递归出口 return x*(my_pow1(x,--n)); //除了调用自身外还乘多了个x,即一般的递归}int main(){ long x; int n; scanf("%ld%d",&x,&n); printf("%ld\n",my_pow1(x,n)); return 0;} 运行图解解释:普通的递归过程是在一个函数中,结果依靠自身的调用来得出,例如求幂运算,pow(2,3)代表求2的3次方,由于pow(2,3)未知,我们可以把它分解成2*pow(2,2),pow(2,2)也未知,又可分解成2*pow(2,1),以此类推,直到pow(2,0)可知(if中定义0时返回1),即pow(2,0)返回值是1.在这个递归过程中,pow函数的建立就是一个个压栈的过程我把它称为函数栈 压栈压入所以函数后,直到最后一个,可以获得最后一个函数的返回值,由这个返回值可以依次推出栈内所有函数的返回值(回溯),即退栈,pow(2,0)返回1,推的pow(2,1)返回2*pow(2,0),即2*1=2,pow(2,2)返回2*pow(2,1),即2*2=4,直到退到栈内最后一个函数pow(2,3),可获得pow(2,3)的返回值为2*pow(2,2)即8; 尾递归样例和解释【编程题】吃糖(尾递归函数) 题目ID:1135【问题描述】小樱是个爱吃糖的女孩, 哥哥送了她n(1<=n<=30)颗糖,怎么吃?一天吃1颗;一天吃2颗。嗯,那就每天吃一颗或两颗吧。1颗糖,肯定只有(1)一种吃法;2颗糖,有(1,1)和(2)两种吃法;3颗糖,有(1,1,1)、(1,2)和(2,1)三种吃法。注 (2,1)表示第一天吃2颗,第二天吃1颗。*你能帮小樱算出,吃完这n颗糖,有多少种吃法吗?请编写一个尾递归函数来解决此问题【输入形式】 【测试用例】1)输入:1输出:result=1 2)输入:4输出:result=5 3)输入:15输出:result=987 4)输入:20输出:result=10946 5)输入:30输出:result=1346269 实际上这道题是一个斐波那契数列的变体,可用尾递归函数解决 //尾递归#include <stdio.h>int ci(int n,int pre,int next){ int sum; if (n==1){ //递归出口(递归和回溯的分界点) return pre; } return ci(n-1,next,pre+next); //除了调用自身外没有其他操作即为尾递归}int main(){ int n; int sum; scanf ("%d",&n); printf ("result=%d",ci(n,1,2)); return 0;}运行图解 ...

May 7, 2019 · 1 min · jiezi

Tree相关概念及特点总结

平衡:树的左右子树的高度差距在一个可控的范围内 B-TREE 多路搜索树AVL 平衡二叉树: 空树或它的左右两个子树的高度差的绝对值不超过1,左右两个子树都是一颗平衡二叉树。RB-TREE 红黑树: 红黑树属于平衡二叉树,但不是严格的平衡二叉树,相对接近平衡的二叉树, 最大深度<=最小深度的两倍(即没有一条路径比其他路径长出两倍)BST 二叉搜索树(Binary Search Tree):BBST 平衡二叉排序树(Balance Binary Sort Tree)红黑树概念: 红黑树特点: 红黑树应用场景: TreeMap 基于红黑树实现的排序Map,默认按key(实现comparable接口)来比较排序。 TreeMap的增删查改以及统计操作的时间复杂度都为O(logn)TreeSetJDK1.8中 HashMap每个数组节点挂的元素个数超过8 ConcurrentHashMap每个数组节点挂的元素个数超过8红黑树与AVL树对比: 红黑树的查询性能略逊色于AVL树 红黑树放弃了追求完全平衡,追求大致平衡, 红黑树的高度相对更高,因此红黑树的插入和删除性能比AVL效率高

May 6, 2019 · 1 min · jiezi

java集合Iterator接口

    上一篇文章中我在集合元素的遍历中已经有涉及到Iterator的普遍使用方法,但是并没有对此进行解释。    其实,Iterator来源于java.util包,也是属于Java集合框架中的一份子,不同于Collection(存放单一数据)和Map(存放具有映射关系的数据),Iterator主要用于集合元素的迭代输出,所以它的对象又被称为迭代器。     Iterator的方法包括: 表头表头boolean hasNext();判断迭代器是否还有未遍历的元素E next();返回迭代器中下一未遍历元素void remove();移除迭代器上一遍历的元素void forEachRemaining(Consumer action)以特定的Lambda表达式遍历元素注:void forEachRemaining(Consumer action)为Java 8新增的默认方法。     下面是Iterator的实例化以及各方法的使用方法 Iterator的实例化Iterator it=c5.iterator();    Iterator本身不存在容纳对象的能力,它的对象必须依附于Collection对象。同时,一个Iterator对象只能使用一次,复用会导致java.util.ConcurrentModificationException。我还没搞懂为什么会这样,应该是关于设计模式的内容。 boolean hasNext();以及next();while(it.hasNext()) { System.out.println(it.next());}输出:5void remove();while(it.hasNext()) { System.out.println(it.next()); it.remove();}输出:5    remove方法要在next方法之后调用,次序调换会导致java.lang.IllegalStateException。 void forEachRemaining(Consumer action);Iterator it=c5.iterator();it.forEachRemaining(obj->System.out.println(obj));输出:5    通过特定的Lambda表达式格式输出元素。      Iterator接口就写到这里了。通常我都是用它作为测试Collection对象的,好处就是不用通过循环遍历,代码量会少一点。但是正式使用我还是会使用循环遍历Collection对象,虽则代码量多,但是可以省掉一个对象的内存空间,能省一点是一点,哈哈!     如果你还想了解关于java集合的内容,欢迎点击https://segmentfault.com/a/1190000019071471

May 5, 2019 · 1 min · jiezi

java集合Collection接口

    在概述里面也说过:Collection是java集合两大接口之一,旗下有三大子接口:Set(元素不能重复,且无序)、Queue、List(元素可重复,且有序)。    Collection来源于java.util包,主要方法包括: 主要方法作用 boolean add(Object o)将传入的Object对象添加到容器中,添加后方法返回trueboolean addAll(Collection c)将传入的集合c中的所有对象添加到容器中,添加后方法返回truevoid clear()清空集合,集合长度变为0boolean contains(Object o)检查集合是否存在对象o,若存在返回trueboolean containsAll(Collection c)检查集合是否存在集合c的所有对象,若存在返回trueboolean isEmpty()返回集合的size是否为0Iterator iterator()返回Iterator对象boolean remove(Object o)删除集合中第一个符合条件的元素,若集合存在对象o,删除并返回trueboolean removeAll(Collection c)删除集合中所有与集合c重合的元素,若删除后对象进行了改变返回trueboolean retainAll(Collection c)删除集合中所有与集合c不重合的元素,若删除后对象进行了改变返回trueint size()返回集合元素的个数Object[] toArray()把集合转变成数组,集合的元素变成对应的数组元素    下面是Collection的实例化以及各成员方法的使用方法 实例化Collection c1=new TreeSet(); Collection c2=new HashSet(); Collection c3=new LinkedHashSet(); Collection c4=new ArrayDeque(); Collection c5=new ArrayList(); Collection c6=new LinkedList(); boolean isEmpty();boolean isEmpty=c5.isEmpty();System.out.print(isEmpty?"c5为空":"c5不为空");输出: c5为空    因为c5在此之前并没有添加任何元素,所以为空。 Iterator iterator();Iterator it5=c5.iterator();while(it5.hasNext()) { System.out.println("集合5元素:"+it5.next());} 输出:集合5元素:5    注:每个Iterator对象只能使用一次, 复用会导致java.util.ConcurrentModificationException。    在本代码中,新建了一个局部变量it5保存c5的迭代器,再通过迭代器的成员方法hasNext判断是否存在下一元素,若true,输出此元素。    java8为Collection的遍历新增了一个来源于Collection的父接口iterable的方法:forEach(Consumer action)。 c5.forEach(obj->System.out.println("集合元素为"+obj));输出:集合元素为5boolean add(Object o);c5.add(5);Iterator it=c5.iterator();while(it.hasNext()) { System.out.println("集合元素:"+it.next());}输出:集合元素:5    在本代码中,c5被添加了一个元素5,所以经过迭代器输出只能得到5。 boolean addAll(Collection c);c6.addAll(c5);Iterator it6=c6.iterator();while(it6.hasNext()) { System.out.println("集合6元素:"+it6.next());}输出:集合6元素:5    在本代码中,c5中的所有元素(5)被整体添加到c6中,所以c6迭代输出5。 void clear();c6.clear();Iterator it6=c6.iterator();while(it6.hasNext()) { System.out.println("集合6元素:"+it6.next());}无输出    在本代码中,c6被清空,所以迭代输出并没有结果。 ...

May 5, 2019 · 1 min · jiezi

数据结构队列

数据结构-队列定义队列(queue)在计算机科学中,是一种先进先出的线性表。它只允许在表的前端进行删除操作,而在表的后端进行插入操作。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。 基于自定义数组实现的队列新建queue接口,用来规范所有queue子类package com.datastructure.queue;import java.awt.*;/** * @program: test * @description: * @author: Mr.Yang * @create: 2019-05-03 16:44 **/public class LoopQueue<E> implements Queue<E> { private E[] data; //指向队列的第一个元素,初始指向0 private int front; //指向队列的最后一个元素的后一个位置,初始指向0 private int tail; private int size; public LoopQueue(int capacity){ data = (E[]) new Object[capacity+1]; front=0; tail=0; size=0; } public LoopQueue(){ this(10); } /** * 因为容量放的时候多了个1,所以get容量的时候,需要减1 * @return */ public int getCapacity(){ return data.length-1; } /** * 1.if((tail + 1) % data.length == front) 如果tail + 1 超过了data.length的大小, * 代表当前tail指向已经超出了容量的大小,因为是循环式,所以需要tail去循环头元素中查看值是否有被占用, * 如果 == front 代表循环头没有,就需要扩容了。 * 2.举例: 元素容量为8,tail目前指向7 front 指向2 * if((7 + 1) % 8 == 2 ) if(0 == 2) 这里是false,因为front指向了2,所以代表 第0,1位是没有值的 * 所以这个值需要在在第0位放(空间利用) * 3.data[tail] = param tail当前指向的地方需要赋值,然后tail自增 循环体 的1,size+1 * @param param */ @Override public void enqueue(E param) { if((tail + 1) % data.length == front){ resize(getCapacity() * 2); } data[tail] = param ; tail = (tail + 1) % data.length; size ++ ; } /** * 扩充队列的容量 * 1.front代表了当前元素初始位置的指向 * 2.newData的第i位元素,应该等于 i + front % data.length 的值 * 3.举例:元素容量20,i 等于 0 ,front 等于 2,结果: newData[0] = data[(0 + 2) % 20] * = data[2] 意思就是,newData的第一位元素,应该等于data有值的第一位元素 * % data.length 的原因主要是为了防止数组越界错误 * 4.新数组赋值完成需要将 front 重新指向0,因为新数组的front指针是从0开始的。 * tail最后要指向等于size大小的值, * @param newCapacity */ private void resize(int newCapacity) { E[] newData = (E[]) new Object[newCapacity + 1]; for(int i = 0 ; i < size ; i++){ newData[i] = data[(i + front ) % data.length]; } data=newData; front = 0 ; tail = size; } /** * 1.如果队列为空抛出异常 * 2.用ret变量来接受当前队列头的值 * 3.接收成功之后将,队列头元素置空 * 4.front指针指向下一个元素 * 5.size大小-1 * 6.如果size大小占据了容量的1/4和size为容量的1/2且不等于0的时候,对容量进行缩减,缩减为原来容量的1/2 * 7.返回ret变量 * @return */ @Override public E dequeue() { if(isEmpty()){ throw new IllegalArgumentException("dequeue is fail ,because queue is empty"); } E ret = data[front]; data[front] = null; front = (front + 1) % data.length; size -- ; if(size == getCapacity() / 4 && getCapacity() / 2 != 0 ){ resize(getCapacity() / 2 ); } return ret; } @Override public E getFront() { if(isEmpty()){ throw new IllegalArgumentException("queue is empty"); } return data[front]; } @Override public int getSize() { return size; } /** * 当front和tail的值相等时,队列为空,初始两个指向的是同一个值(只有初始的时候,指向的是同一个地方) * @return */ @Override public boolean isEmpty() { return front == tail; } /** * 1.元素从 front位置开始循环遍历,i的值不能等于tail, * 也就是到tail的前一位,i = i + 1 且%data.length, * 因为i有可能从循环头重新开始 * 2.( i + 1 ) % data.length != tail 如果当前i + 1 % data.length * 不等于tail表示不到最后一个元素,就拼接, * @return */ @Override public String toString(){ StringBuffer sb = new StringBuffer(); sb.append(String.format("Array: size = %d , capacity = %d\n", size, getCapacity())); sb.append("front ["); for (int i = front; i != tail ; i = (i + 1) % data.length) { sb.append(data[i]); if (( i + 1 ) % data.length != tail) { sb.append(", "); } } sb.append("] tail"); return sb.toString(); }}新建ArrayQueue实现类package com.datastructure.queue;import com.datastructure.array.Array;/** * @program: test * @description: * @author: Mr.Yang * @create: 2019-05-03 18:19 **/public class ArrayQueue<E> implements Queue<E>{ Array<E> array; public ArrayQueue(int capacity){ array=new Array<E>(capacity); } public ArrayQueue(){ array=new Array<E>(); } @Override public void enqueue(E param) { array.addLast(param); } @Override public E dequeue() { return array.removeFirst(); } @Override public E getFront() { return array.getFirst(); } @Override public int getSize() { return array.getSize(); } @Override public boolean isEmpty() { return array.isEmpty(); } @Override public String toString(){ StringBuffer sb = new StringBuffer(); sb.append("front: "); sb.append("["); for(int i=0;i<array.getSize();i++){ sb.append(array.get(i)); if(i!=array.getSize()-1){ sb.append(", "); } } sb.append("] tail"); return sb.toString(); }}测试类package com.datastructure.queue;/** * @program: test * @description: * @author: Mr.Yang * @create: 2019-05-03 18:26 **/public class QueueTest { public static void main(String[] args) { ArrayQueue<Integer> integerArrayQueue = new ArrayQueue<>(); for (int i = 0; i < 10; i++) { integerArrayQueue.enqueue(i); System.out.println(integerArrayQueue); if(i % 3 == 2){ integerArrayQueue.dequeue(); System.out.println(integerArrayQueue); } } }}测试结果front: [0] tailfront: [0, 1] tailfront: [0, 1, 2] tailfront: [1, 2] tailfront: [1, 2, 3] tailfront: [1, 2, 3, 4] tailfront: [1, 2, 3, 4, 5] tailfront: [2, 3, 4, 5] tailfront: [2, 3, 4, 5, 6] tailfront: [2, 3, 4, 5, 6, 7] tailfront: [2, 3, 4, 5, 6, 7, 8] tailfront: [3, 4, 5, 6, 7, 8] tailfront: [3, 4, 5, 6, 7, 8, 9] tail可以看到测试结果是正确的,也符合队列结构的数据存取,但是因为是基于自定义数组来实现的,所以会调用数组方法的removeFirst方法,删除第一个元素的同时,会重新将后面所有元素前移,索引前移,均摊时间复杂度为O(n)。 ...

May 3, 2019 · 7 min · jiezi

NodeJS实现简易区块链

之前由于课程要求,基于Nodejs做了一个实现简易区块链。要求非常简单,结构体记录区块结构,顺便能向链中插入新的区块即可。 但是如果要支持多用户使用,就需要考虑“可信度”的问题。那么按照区块链要求,链上的数据不能被篡改,除非算力超过除了攻击者本身之外其余所以机器的算力。 想了想,就动手做试试咯。 ????查看全部教程 / 阅读原文???? 技术调研在google上搜了搜,发现有个项目不错: https://github.com/lhartikk/naivechain 。大概只有200行,但是其中几十行都是关于搭建ws和http服务器,美中不足的是没有实现批量插入区块链和计算可信度。 结合这个项目,基本上可以确定每个区块会封装成一个class(结构化表示),区块链也封装成一个class,再对外暴露接口。 区块定义为了方便表示区块,将其封装为一个class,它没有任何方法: /** * 区块信息的结构化定义 */class Block { /** * 构造函数 * @param {Number} index * @param {String} previousHash * @param {Number} timestamp * @param {*} data * @param {String} hash */ constructor(index, previousHash, timestamp, data, hash) { this.index = index // 区块的位置 this.previousHash = previousHash + '' // 前一个区块的hash this.timestamp = timestamp // 生成区块时候的时间戳 this.data = data // 区块本身携带的数据 this.hash = hash + '' // 区块根据自身信息和规则生成的hash }}至于怎么生成hash,这里采用的规则比较简单: ...

May 3, 2019 · 2 min · jiezi

数据结构栈

数据结构-栈定义栈(英语:stack)又称为堆栈或堆叠,栈作为一种数据结构,它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。 由于堆叠数据结构只允许在一端进行操作,因而按照后进先出(LIFO, Last In First Out)的原理运作。栈也称为后进先出表 栈的应用场景undo操作(撤销)例如:将操作的每组数据存入栈中,如果想要撤销,只需要弹出栈顶元素,就可以恢复上一步操作了。程序调用的系统栈例如:A方法调用B方法得到返回值,B调用C得到返回值,A操作走到了B方法,这个时候可以将A的代码位置存储到栈中,然后走到B方法,B操作走到了C方法,这个时候可以将B的代码位置存储到栈中。最后C执行完成,根据栈的结构开始弹出数据,一步一步再走回A方法。判断括号是否有效。下文会有代码实现(详细规则描述可以参考leetcode第20题)开括号必须用同一类型的括号闭合。开方括号必须按正确顺序闭合。例如:正确的:{[()]} [{()}] ({[]}) 等 。错误的:[{(})] [}{()] 等。自定义栈基类的代码实现栈在java.util有一个工具类,先不用,自定义实现一个创建一个接口,用来统一规范所有栈实现package com.datastructure.stack;public interface Stack<E> { /** * 向栈插入元素 * @param e */ public void push(E e); /** * 取出最上面的元素,并且返回 * @return */ public E pop(); /** * 获取栈的大小 * @return */ public int getSize(); /** * 判断栈是否为空 * @return */ public boolean isEmpty(); /** * 获取栈最上面的元素 * @return */ public E peek();}用基于数组的方式来实现一个栈(上文所写的自定义数组)package com.datastructure.stack;import com.datastructure.array.Array;/** * @program: test * @description: * @author: Mr.Yang * @create: 2019-05-02 15:27 **/public class ArrayStack<E> implements Stack<E>{ Array<E> array; public ArrayStack(int capacity){ array=new Array<E>(capacity); } public ArrayStack(){ array=new Array<E>(); } @Override public void push(E e) { array.addLast(e); } @Override public E pop() { return array.removeLast(); } @Override public int getSize() { return array.getSize(); } @Override public boolean isEmpty() { return array.isEmpty(); } @Override public E peek() { return array.getLast(); } /** * 获取容量值 * @return */ public int getCapacity(){ return array.getCapacity(); } @Override public String toString(){ StringBuffer sb = new StringBuffer(); sb.append("stack: "); sb.append("["); for(int i=0;i<array.getSize();i++){ sb.append(array.get(i)); if(i!=array.getSize()-1){ sb.append(", "); } } sb.append("] right value is stack top"); return sb.toString(); }}测试代码package com.datastructure.stack;/** * @program: test * @description: * @author: Mr.Yang * @create: 2019-05-02 16:11 **/public class StackTest { public static void main(String[] args) { ArrayStack<Integer> integerArrayStack = new ArrayStack<>(); for(int i=0;i<5;i++){ integerArrayStack.push(i); System.out.println(integerArrayStack); } Integer pop = integerArrayStack.pop(); System.out.println("----移除上级元素----value is "+pop); System.out.println("-------------移除之后的栈打印------------------"); System.out.println(integerArrayStack); }}测试结果stack: [0] right value is stack topstack: [0, 1] right value is stack topstack: [0, 1, 2] right value is stack topstack: [0, 1, 2, 3] right value is stack topstack: [0, 1, 2, 3, 4] right value is stack top----移除上级元素----value is 4-------------移除之后的栈打印------------------stack: [0, 1, 2, 3] right value is stack topleetCode第20题,花括号正确闭合思路根据栈的数据结构特点,我们可以先将所有左括号‘[{(’放进栈中,然后判断当前字符如果是‘)]}’这种的右括号,但是栈顶的括号却不匹配,返回false注意控制判断这里使用java自带的栈工具类来实现leetcode给的测试例子: 12345输入例子()()[]{}(]([)]{[]}代码实现package com.datastructure.stack;import java.util.Stack;/** * @program: test * @description: * @author: Mr.Yang * @create: 2019-05-02 16:59 **/public class Solution { public static void main(String[] args) { Solution solution = new Solution(); System.out.println(solution.isValid("{\"name\": \"网站\",\"num\": 3,\"sites\": [ \"Google.com\", \"Taobao.com\", \"Waibo.wang\" ]}")); } public boolean isValid(String s) { Stack<Character> characters = new Stack<>(); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (c == '{' || c == '[' || c == '(') { characters.push(c); } else { if(characters.isEmpty()){ return false; } Character peek = characters.pop(); switch (c) { case '}': if (!peek.equals('{')) { return false; } continue; case ']': if (!peek.equals('[')) { return false; } continue; case ')': if (!peek.equals('(')) { return false; } continue; } } } return characters.isEmpty(); } /*public boolean isValid(String s) { Stack<Character> characters = new Stack<>(); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (c == '{' || c == '[' || c == '(') { characters.push(c); } else { if(characters.isEmpty()){ return false; } Character toChar = characters.pop(); if(c == ')' && toChar != '('){ return false; } if(c == '}' && toChar != '{'){ return false; } if(c == ']' && toChar != '['){ return false; } } } return characters.isEmpty(); }*/}如果想实现更多字符串关于括号的匹配,如JSON等等,可以根据栈的特点来实现代码例子GIT地址:https://git.dev.tencent.com/y...项目简介:这个项目是我做测试,学习的主要项目,目前里面包含了: ...

May 2, 2019 · 3 min · jiezi

数据结构数组

数据结构-数组数组数据结构中最基本的一个结构就是线性结构,而线性结构又分为连续存储结构和离散存储结构。所谓的连续存储结构其实就是数组。优点:插入块如果知道坐标可以快速去地存取缺点:查找慢,删除慢,大小固定二次封装数组的增删改查基类的定义定义一个工具类名称-Array接受的参数包括基本类型和自定义类型(用泛型来接受,基本类型用包装类)自定义属性两个:size用来表示数组的大小,data用来表示一个准确的集合概念区分:size表示数组的大小,capacity表示数组容量的大小构造函数:有参构造,接受一个int值,用来初始化数组容量;无参构造:给容量一个默认值toString()方法,输出数组的大小和数组容量的大小,以及数组中的值getSize()方法,调用方通过方法来获取数组的大小getCapacity()方法,调用方通过方法来获取数组容量的大小isEmpty()方法,调用方通过方法来判断数组是否为空(指的是数组中是否有值,没值就为空)基类的代码package com.datastructure.array;/** * @program: test * @description: 自定义数组封装工具类 * @author: Mr.Yang * @create: 2019-05-01 15:36 **/public class Array<E> { private Integer size; private E[] data; /** * 有参构造函数 * @param capacity 封装data的容量值 */ public Array(Integer capacity){ data= (E[]) new Object[capacity]; size=0; } /** * 无参构造函数,设置默认容量为10 */ public Array(){ this(10); } /** * 获取数组中的元素个数 * @return */ public Integer getSize(){ return size; } /** * 获取数组的容量 * @return */ public Integer getCapacity(){ return data.length; } /** * 判断数组是否为空 * @return */ public boolean isEmpty(){ return size==0; } @Override public String toString(){ StringBuffer sb = new StringBuffer(); sb.append(String.format("Array: size = %d , capacity = %d\n",size,data.length)); sb.append("["); for(int i =0;i<size;i++){ sb.append(data[i]); if(i !=size-1){ sb.append(", "); } } sb.append("]"); return sb.toString(); }}增添加的方法add()方法,两个参数,添加元素的索引位置,和元素的值addFirst()方法,在所有元素的最前面添加addLast()方法,在所有元素的最后面添加添加的代码(增)/** * 在索引为index的位置,插入param * 1.假如在索引为2的位置插入元素 * 2.需要将原来索引为2及其以后的位置向后移动一整位 * 3.移动完成之后,在索引为2的位置插入指定的值 * 4.因为数组中多了一个值,所以size需要+1 * * @param index 元素的索引 * @param param 值 */ public void add(int index, E param) { if (index < 0 || index > size) { throw new IllegalArgumentException("添加失败,索引的值不能小于0,并且索引的值不能大于数组的大小"); } if (size == data.length) { throw new IllegalArgumentException("添加失败,数组的大小已经等于了数组容量的大小"); } for (int i = size - 1; i >= index; i--) { data[i + 1] = data[i]; } data[index] = param; size++; } /** * 在所有元素之前添加值 * * @param param */ public void addFirst(E param) { add(0, param); } /** * 在所有元素之后添加值 * * @param param */ public void addLast(E param) { add(size, param); }测试代码package com.datastructure.array;/** * @program: test * @description: * @author: Mr.Yang * @create: 2019-05-01 16:46 **/public class ArrayTest { public static void main(String[] args) { Array<Integer> integerArray = new Array<Integer>(20); for(int i = 0; i < 10 ; i ++){ integerArray.addLast(i); } System.out.println(integerArray); System.out.println("--------------------索引为3的地方添加元素-----------------------"); integerArray.add(3,666); System.out.println(integerArray); System.out.println("--------------------所有元素的最后面添加值-----------------------"); integerArray.addLast(10000); System.out.println(integerArray); System.out.println("--------------------所有元素的最前面添加值-----------------------"); integerArray.addFirst(-1); System.out.println(integerArray); }}测试结果Array: size = 10 , capacity = 20[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]--------------------索引为3的地方添加元素-----------------------Array: size = 11 , capacity = 20[0, 1, 2, 666, 3, 4, 5, 6, 7, 8, 9]--------------------所有元素的最后面添加值-----------------------Array: size = 12 , capacity = 20[0, 1, 2, 666, 3, 4, 5, 6, 7, 8, 9, 10000]--------------------所有元素的最前面添加值-----------------------Array: size = 13 , capacity = 20[-1, 0, 1, 2, 666, 3, 4, 5, 6, 7, 8, 9, 10000]改添加的方法set,两个参数,修改的元素的索引位置,和将要修改的值添加的代码(改)/** * 修改数组中元素的值 * @param index * @param param */ public void set(int index,E param){ if(index<0 || index>= size){ throw new IllegalArgumentException("获取失败,索引值无效"); } data[index]=param; }测试代码package com.datastructure.array;/** * @program: test * @description: * @author: Mr.Yang * @create: 2019-05-01 16:46 **/public class ArrayTest { public static void main(String[] args) { Array<Integer> integerArray = new Array<Integer>(20); for (int i = 0; i < 10; i++) { integerArray.addLast(i); } System.out.println(integerArray); System.out.println("--------------------索引为3的地方修改值为1010-----------------------"); integerArray.set(3, 1010); System.out.println(integerArray); }}测试结果Array: size = 10 , capacity = 20[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]--------------------索引为3的地方修改值为1010-----------------------Array: size = 10 , capacity = 20[0, 1, 2, 1010, 4, 5, 6, 7, 8, 9]查添加的方法get()方法,一个参数,索引值,根据索引返回对应的值contains()方法,一个参数,判断数组中是否包含某个元素find()方法,一个参数,查找数组中是否包含param,如果包含返回索引值,不包含返回-1findAll()方法,一个参数,查找数组中是否包含param,返回包含的索引数组添加的代码(查) /** * 获取索引位置的元素 * @param index * @return */ public E get(Integer index){ if(index<0 || index>=size){ throw new IllegalArgumentException("获取失败,索引的值不能小于0,并且索引的值不能大于等于数组的大小"); } return data[index]; } /** * 查看数组中是否包含某个元素 * @param param * @return */ public boolean contains(E param){ for(int i = 0 ; i < size ; i++){ if(data[i] == param){ return true; } } return false; } /** * 查找数组中是否包含param,如果包含返回索引值,不包含返回-1 * @param param * @return */ public Integer find(E param){ for(int i = 0 ; i < size ; i++){ if(data[i] == param){ return i; } } return -1; } /** * 查找数组中是否包含param * 1.创建一个int数组用来接收返回的索引值 * 2.索引容量最大为数组的大小 * 3.用临时变量来存储int数组的大小 * 4.如果相等,给 int数组 为临时变量的元素赋值(相等的索引),临时变量自增 * @param param * @return */ public Integer[] findAll(E param){ int intTemp=0; Integer[] integers = new Integer[size]; for(int i = 0 ; i < size ; i++){ if(data[i] == param){ integers[intTemp]=i; intTemp++; } } return integers; }测试代码package com.datastructure.array;import java.util.Arrays;/** * @program: test * @description: * @author: Mr.Yang * @create: 2019-05-01 16:46 **/public class ArrayTest { public static void main(String[] args) { Array<Integer> integerArray = new Array<Integer>(20); for (int i = 0; i < 10; i++) { integerArray.addLast(i); } integerArray.addLast(3); System.out.println(integerArray); System.out.println("--------------------得到索引为3的值-----------------------"); System.out.println(integerArray.get(3)); System.out.println("--------------------判断是否包含有包含3的值-----------------------"); System.out.println(integerArray.contains(3)); System.out.println("--------------------查找是否包含参数,并返回索引-----------------------"); System.out.println(integerArray.find(3)); System.out.println("--------------------查找是否包含参数,并返回索引数组-----------------------"); System.out.println(Arrays.asList(integerArray.findAll(3))); }}测试结果Array: size = 11 , capacity = 20[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 3]--------------------得到索引为3的值-----------------------3--------------------判断是否包含有包含3的值-----------------------true--------------------查找是否包含参数,并返回索引-----------------------3--------------------查找是否包含参数,并返回索引数组-----------------------[3, 10, null, null, null, null, null, null, null, null, null]删添加的方法remove()方法,数组中删除index位置的元素,并且返回对应的值removeFirst()方法,删除第一位元素removeLast()方法,删除最后一位元素removeElement()方法,查看某个值是否在数组中存在,如果存在,删除并返回true,如果不存在 返回false、removeLast()方法, 查找所有元素,获取所有相等的索引,遍历移除添加的代码(删)/** * 从数组中删除index位置的元素,并且返回对应的值 * 1.假如删除索引为3的元素 * 2.需要将索引大于3的元素向前移动一位 * 3.因为数组中少了一个值,所以元素-1 * 4.用临时变量来存储删除的值,用来返回 * @param index * @return */ public E remove(int index){ if(index<0 || index>=size){ throw new IllegalArgumentException("删除失败,索引的值不能小于0,并且索引的值不能大于等于数组的大小"); } E temp=data[index]; for(int i = index+1 ; i < size ; i++){ data[i-1]=data[i]; } size--; return temp; } /** * 删除第一位元素 * @return */ public E removeFirst(){ return remove(0); } /** * 删除最后一位元素 */ public E removeLast(){ return remove(size-1); } /** * 查看某个值是否在数组中存在,如果存在,删除并返回true,如果不存在 返回false * @param param */ public boolean removeElement(E param){ Integer index = find(param); if(index != -1){ remove(index); return true; } return false; } /** * 遍历数组,移除元素 * @param param */ public void removeAllElement(E param){ for(E d:data){ removeElement(param); } }测试代码package com.datastructure.array;import java.util.Arrays;/** * @program: test * @description: * @author: Mr.Yang * @create: 2019-05-01 16:46 **/public class ArrayTest { public static void main(String[] args) { Array<Integer> integerArray = new Array<Integer>(20); for (int i = 0; i < 10; i++) { integerArray.addLast(i); } integerArray.addLast(3); integerArray.addLast(4); System.out.println(integerArray); System.out.println("--------------------数组中删除6位置的元素,并且返回对应的值-----------------------"); System.out.println(integerArray.remove(6)); System.out.println(integerArray); System.out.println("--------------------删除第一位元素-----------------------"); System.out.println(integerArray.removeFirst()); System.out.println(integerArray); System.out.println("--------------------删除最后一位元素-----------------------"); System.out.println(integerArray.removeLast()); System.out.println(integerArray); System.out.println("--------------------查看8是否在数组中存在,返回true和fals-----------------------"); System.out.println(integerArray.removeElement(8)); System.out.println(integerArray); System.out.println("--------------------查找所有元素,获取所有相等的索引,遍历移除-----------------------"); integerArray.removeAllElement(3); System.out.println(integerArray); }}测试结果Array: size = 12 , capacity = 20[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 3, 4]--------------------数组中删除6位置的元素,并且返回对应的值-----------------------6Array: size = 11 , capacity = 20[0, 1, 2, 3, 4, 5, 7, 8, 9, 3, 4]--------------------删除第一位元素-----------------------0Array: size = 10 , capacity = 20[1, 2, 3, 4, 5, 7, 8, 9, 3, 4]--------------------删除最后一位元素-----------------------4Array: size = 9 , capacity = 20[1, 2, 3, 4, 5, 7, 8, 9, 3]--------------------查看8是否在数组中存在,如果存在,删除并返回true,如果不存在 返回false、-----------------------trueArray: size = 8 , capacity = 20[1, 2, 3, 4, 5, 7, 9, 3]--------------------查找所有元素,获取所有相等的索引,遍历移除-----------------------Array: size = 6 , capacity = 20[1, 2, 4, 5, 7, 9]数组动态扩容添加的方法resize()方法,定义为私有,调用方不能通过方法来调用,去修改,否则容易造成BUG扩容倍数,如果用固定值,不好抉择。比如:100容量,扩容10个,没有意义,很快就满了;100容量,扩充1000个,太多了,容易早证浪费。最好选择倍数,都在同一个单位级别,这里代码选择的是2倍添加的时候需要判断扩容,删除的时候需要删除容量,减少资源浪费删除的时候,当元素减少到容量的1/4的时候开始缩,缩减到容量的1/2如果选择1/2的时候开始缩减,然后缩减到1/2会有一定的问题,例如:容量10,现在容量满了,来了一个元素,需要扩容,按照设置的扩容机制,会扩容2倍,也就是20容量,如果删除了一个元素,元素达到了容量的1/2,我们开始进行缩减,缩减1/2,容量又变为10。如果一直在这个临界值,那么元素会一直进行扩容缩减扩容缩减,性能影响太大。如果选择1/4的时候开始缩减,然后缩减到1/2,这样能够较好的避开以上问题,例如:容量10,容量满了,来了一个元素,扩容容量到了20,如果删除一个元素,还不到容量的1/4,所以还不会进行缩减,如果真的到了容量的1/4的时候,我们选择缩减1/2,容量也需要一定的元素,才会进行扩容,防止了容量一直扩容或者缩减添加的代码 /** * 扩容方法 * 1.需要new一个object,new E[i] java不支持这样写 * 2.object是所有类的基类,用object然后强转一下就可以 * 3.扩展之后,需要将原数组中的值,放入扩展之后的数组中 * @param newCapacity */ private void resize(int newCapacity) { E[] newData = (E[]) new Object[newCapacity]; for(int i=0;i<size;i++){ newData[i]=data[i]; } data=newData; }修改的代码add() 和 remove()/** * 在索引为index的位置,插入param * 1.假如在索引为2的位置插入元素 * 2.需要将原来索引为2及其以后的位置向后移动一整位 * 3.移动完成之后,在索引为2的位置插入指定的值 * 4.因为数组中多了一个值,所以size需要+1 * * @param index 元素的索引 * @param param 值 */ public void add(int index, E param) { if (index < 0 || index > size) { throw new IllegalArgumentException("添加失败,索引的值不能小于0,并且索引的值不能大于数组的大小"); } if (size == data.length) { resize(2 * data.length); } for (int i = size - 1; i >= index; i--) { data[i + 1] = data[i]; } data[index] = param; size++; }/** * 从数组中删除index位置的元素,并且返回对应的值 * 1.假如删除索引为3的元素 * 2.需要将索引大于3的元素向前移动一位 * 3.因为数组中少了一个值,所以元素-1 * 4.用临时变量来存储删除的值,用来返回 * @param index * @return */ public E remove(int index){ if(index<0 || index>=size){ throw new IllegalArgumentException("删除失败,索引的值不能小于0,并且索引的值不能大于等于数组的大小"); } E temp=data[index]; for(int i = index+1 ; i < size ; i++){ data[i-1]=data[i]; } size--; if(size == data.length / 4 ){ resize(data.length / 2 ); } return temp; }测试代码package com.datastructure.array;import java.util.Arrays;/** * @program: test * @description: * @author: Mr.Yang * @create: 2019-05-01 16:46 **/public class ArrayTest { public static void main(String[] args) { Array<Integer> integerArray = new Array<Integer>(10); for (int i = 0; i < 10; i++) { integerArray.addLast(i); } System.out.println(integerArray); System.out.printf("--------------------将容量设置为10,添加10个元素,元素的容量 : %d -------------------\r\n",integerArray.getCapacity()); integerArray.addLast(21); System.out.printf("--------------------元素+1,元素的容量 : %d --------------------\r\n",integerArray.getCapacity()); integerArray.removeLast(); System.out.printf("--------------------元素-1,元素的容量 : %d --------------------\r\n",integerArray.getCapacity()); integerArray.removeLast(); System.out.printf("--------------------元素-1,元素的容量 : %d --------------------\r\n",integerArray.getCapacity()); for(int x=0;x<=5;x++){ integerArray.removeLast(); } System.out.printf("--------------------元素-1/4,元素的容量 : %d --------------------\r\n",integerArray.getCapacity()); }}测试结果Array: size = 10 , capacity = 10[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]--------------------将容量设置为10,添加10个元素,元素的容量 : 10 ---------------------------------------元素+1,元素的容量 : 20 ----------------------------------------元素-1,元素的容量 : 20 ----------------------------------------元素-1,元素的容量 : 20 ----------------------------------------元素-1/4,元素的容量 : 10 --------------------

May 1, 2019 · 6 min · jiezi

leetcode中的vector常见用法

vector介绍我对vector的认识就是C++提供的包装好的数组,即对象的集合,一般来说,刷题过程中普通数组都可以用vector来代替,毕竟vector有很对简单用法并且不用考虑长度问题。因为是基础用法部分,就不深究vector和数组的区别以及vector的特性了,直接进入使用吧 本vector用法说明由于只是针对leetcode刷题所写的常用用法,所以内容可能比较简略,只是vector的部分内容。 声明虽然leetcode上自动包含了所有头文件,但是如果自己编程使用vector要加上头文件 #include <vector>再加上using namespace std;或者using std::vector;创建,复制vector<int> a; //创建一个空的vectorvector<int> a(n); //创建一个含有n个对象的vector, //此时数据已缺省构造产生,比如现在的数组里全是0vector<int> a(n, elem);//创建一个含有n个elem拷贝的vectorvector<int> a1(a2); //复制一个vectorvector<int> a1(begin, end); //这里的begin和end可以是另一个vector的迭代器 //会将[begin,end)范围内的数据复制过来关于最后一个用法有一个小坑可以注意一下,假设a2是一个含有4个int数据的vector,如果你是这样用的:vector<int> a1(a2.begin(), a2.begin()+3),那么a1中实际有3个对象;但如果你是这样用的:vector<int> a1(a2.begin(), a2.end()),那么a1中实际有4个对象。这是因为a2.end()实际上指向的并不是vector最后一个对象,而是最后一个对象的下一个位置。 一般来说,leetcode刷题过程中都不用手动释放vector内存,所以我也就不写了 访问数据a.begin() 返回指向首个对象的指针,也就是一般所说的迭代器a.end() 返回指向最后一个对象的下一个位置的指针a.begin()+1 返回指向首个对象的下一个对象的指针a.end()-1 返回返回指向最后一个对象的指针a.rbegin() 返回指向最后一个对象的指针,反向迭代器a.rend() 返回指向首个对象的前一个位置的指针,反向迭代器迭代器我个人觉得一开始不用深究,只需要知道他们是指向vector对象的指针即可反向迭代器很容易搞混。。可以不用,如果用的话要记住,rend和end分别在vector的两头 a.front() 返回首个对象的数据,和*a.begin()是一样的a.back() 返回最后一个对象的数据a.at(i) 返回编号i位置处的对象数据,会检查数据是否存在a[i] 返回编号i位置处的对象数据这里建议大家尽量使用a.at(i)来返回数据,因为会检查数据是否存在 插入a.push_back(i); //最简单的插入,直接向vector末尾加入一个数据a.insert(pos,elem); //向pos迭代器指向的对象前插入一个对象,注意是对象前a.insert(pos, n, elem); //向pos迭代器指向的对象前插入n个相同对象a.insert(pos, begin, end); //向pos迭代器指向的对象前插入[begin,end)之间的对象后三个函数都会返回新数据的位置 删除a.clear(); //删除所有对象a.pop_back(); //删除最后一个对象a.erase(pos); //删除pos迭代器对应的对象a.erase(begin, end); //删除[begin,end)之间的对象后两个函数会返回被删除数据的下一个位置 赋值a[1] = 1; //令编号1的对象数据为1a.assign(1, 1); //令a为{1}a.assign(begin,end); //把另一个迭代器[begin,end)中的数据赋值给a要注意第一行和第二行结果是完全不同的,assign函数有点类似复制函数,是对整体的操作 其它常用函数a.size() 返回vector中元素的个数a.empty() 返回vector是否为空,空返回1,不为空返回0a1.swap(a2); //交换a1,a2数据swap(a1, a2); //交换a1,a2数据,同上swap(a1[1], a1[2]); //交换a1的第2个数据和第3个数据 注意,是没有a1[1].swap(a1[2])这种用法的 ...

May 1, 2019 · 1 min · jiezi

数据结构之二叉搜索树

二叉搜索树二叉搜索树也叫二叉查找树或者二叉排序树,它要么是一颗空树,要么满足以下几点: 1.若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值。 2.若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值。 3.任意节点的左、右子树也分别为二叉搜索树。 4.没有键值相等的节点。 二叉搜索树的实现1.二叉搜索树的存储结构 public class BinarySearchTree { public static Node root; public BinarySearchTree(){ this.root = null; }}class Node{ int data; Node left; Node right; public Node(int data){ this.data = data; left = null; right = null; }}2.二叉搜索树的插入 a.循环二分查找到需要插入的地方。 b.假如插入的值小于当前的值,并且当前左节点为空,那么左节点就指向新节点。 c.假如插入的值大于当前的值,并且当前右节点为空,那么右节点就指向新节点。 public void insert(int id){ Node newNode = new Node(id); if(root == null){ root = newNode; return; } Node current = root; Node parent = null; while(true){ parent = current; if(id < current.data){ current = current.left; if(current == null){ parent.left = newNode; return; } } else { current = current.right; if(current == null){ parent.right = newNode; return; } } }}3.二叉搜索树的删除 a.当删除节点为叶子节点时,直接删除节点。 b.当删除节点只有左子树时,重接左子树。 c.当删除节点只有右子树时,重接右子树。 d.当删除节点既有左子树,又有右子树时,先找一个可以替换删除节点的节点。由于二叉树的性质,左子树的值小于根节点的值,右子树的值大于根节点的值。所以右子树的最左的节点就是替换删除的节点,然后在重接右子树。 第 d 点的图例: ...

April 29, 2019 · 2 min · jiezi

数据结构初探了解不同类型的数据结构

数据结构概要数据结构可以分为两类: 线性数据结构非线性数据结构在线性数据结构中,数据以线性或顺序方式构造。 数组,列表,堆栈和队列是线性结构的例子。 在非线性结构中,数据不是按顺序方式构建的。 图形和树是非线性数据结构的最常见例子。 编程世界中存在许多不同类型的数据结构。 其中,以下是最常用的: Struct(结构体)Array(数组)Linked list(链表)Double linked list(双链表)Stack(栈)Queue(队列)Priority Queue(优先队列)Set(集合)Map(映射)Tree(树)Graph(图)Heap(堆)结构体(Struct)通常,变量可以存储单个数据类型,单个标量数据类型只能存储单个值。在许多情况下,我们可能需要将一些数据类型组合在一起作为单个复杂数据类型。例如,我们希望将一些学生信息存储在学生数据类型中。 我们需要学生姓名,地址,电话号码,电子邮件,出生日期,学生所在班级等。 为了将每个学生记录存储到一个独特的学生数据类型,我们需要一个特殊的结构。 这可以通过结构体轻松实现。 换句话说,结构体是值的容器,通常使用名称访问。 结构体在C编程语言中非常流行,我们也可以在PHP中使用类似的概念。 数组(Array)待续。。。

April 29, 2019 · 1 min · jiezi

数据结构之二叉树

二叉树二叉树(Binary Tree)是每个节点最多只有两个子节点的结构,通常左边的叫左子树,右边的叫右子树,二叉树的节点是具有左右次序的,不能随意颠倒。 二叉树的 4 种形态1.仅仅只有一个根节点,没有子节点。 2.根节点只有左子树。 3.根节点只有右子树。 4.根节点既有左子树,又有右子树。 二叉树的分类1.完全二叉树 假设其深度为 d(d>1)。除了第 d 层外,其它各层的节点数目均已达最大值,且第 d 层所有节点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树。 2.满二叉树 所有叶子节点全都出现在最底层的完全二叉树就叫满二叉树。就相当于这棵树的第一层是 1 个节点,第二层是 2 个节点,第三层是 4 个节点,第五层是 8 个节点,以此类推。 3.斜树 所有的节点都只有左子树的二叉树叫左斜树,所有的节点都只有右子树的二叉树叫右子树,它们都叫斜树。实际上这棵二叉树看起来像一撇或者一捺。 4.二叉搜索树(也叫二叉查找树或者二叉排序树) 若它的左子树不为空,则左子树上所有节点的值均小于它的根节点的值;若它的右子树不为空,则右子树上所有节点的值均大于它的根节点的值;它的左、右子树也分别是二叉排序树。说明它是一颗有顺序的树,左子树节点的值小于根节点的值,右子树节点的值大于根节点的值。 5.平衡二叉树 它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树。 二叉树的存储以下面的二叉树为例 1.顺序存储 二叉树的顺序存储结构就是用一维数组存储二叉树中的节点,并且节点的存储位置,也就是数组的下标要能体现节点之间的逻辑关系。如果某个节点的索引为 i,(假设根节点的索引为 0)则在它左子节点的索引会是 2i+1,以及右子节点会是 2i+2。 2.链式存储 因为在二叉树中,一个父节点最多只允许 2 个子节点,所以我们只需要一个存储数据和左右子节点的指针,这样的结构就是链式存储,也叫二叉链表。 二叉树的遍历以下面的二叉树为例 1.前序遍历 先访问根节点,然后前序遍历左子树,再前序遍历右子树。根据上面的二叉树前序遍历结果是 ECBADGFH。 2.中序遍历 从根节点开始(注意并不是先访问根节点),中序遍历根节点的左子树,然后是访问根节点,最后中序遍历右子树。根据上面的二叉树中序遍历结果是 ABCDEFGH。 3.后序遍历 从左到右先叶子节点后父节点的方式遍历访问左右子树,最后是访问根节点。根据上面的二叉树后序遍历结果是 ABDCFHGE。 4.层序遍历 从树的第一层,也就是根节点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对节点逐个访问。根据上面的二叉树层序遍历结果是 ECGBDFHA。 总结二叉树有多种形态,多种类型,多种存储方式和多种遍历方法。完全二叉树和满二叉树还是难以理解的,满二叉树一定是一棵完全二叉树,但完全二叉树不一定是满二叉树,主要是「满」和「完全」的区别分清楚。 由于线性查找需要遍历全部数据,在数据量大的时候效率就非常低下,到底有多低,在数据库有几百万几千万以上数据量写过查询语句的就知道有索引和没索引的区别。那为什么有索引的就那么快呢?当然数据库的索引不是用二叉树来实现的,想想如果使用二叉树来实现,那这个索引树得多高啊。而是采用更适合数据库查找的 B+树 来实现的,不过 B+树 也是由二叉树进化而来的。 ...

April 26, 2019 · 1 min · jiezi

数据结构之树

什么是树?树是由n(n>0)个有限节点组成一个具有层次关系的集合,一个父节点有0个或多个子节点。用树结构来表示一对多的关系。树的特点:1.没有父节点的节点称为根节点。2.每一个非根节点有且只有一个父节点。3.除了根节点外,每个子节点可以分为多个不相交的子树。4.每个节点都 0 个或多个子节点。5.树里没有环路,就是节点只能向下衍生,跟树一样,不能相交于其它子树。 树的术语根节点:没有父几点,并且每棵树只有一个根节点。父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点。子节点:一个节点含有的子树的根节点称为该节点的子节点。叶子节点:最底层的,度为零的节点。兄弟节点:具有相同父节点的节点互称为兄弟节点。堂兄弟节点:父节点在同一层的节点互为堂兄弟。分支节点:中间的,度不为零的节点。节点的度:一个节点含有的子树的个数称为该节点的度。树的度:一棵树中,最大的节点的度称为树的度。深度:对于任意节点 n, n 的深度为从根到 n 的唯一路径长,根的深度为 0。高度:对于任意节点 n, n 的高度为从 n 到一片树叶的最长路径,所有叶子节点的高度为 0。节点的祖先:从根到该节点所经分支上的所有节点。节点的子孙:以某节点为根的子树中任一节点都称为该节点的子孙。节点的层次:从根开始定义起,根为第 1 层,根的子节点为第 2 层,以此类推。森林:由m(m >= 0)棵互不相交的树的集合称为森林。 总结树 实际上就是表示一对多的关系,数组和链表遍历查找时间复杂度是 O(n),当 n 很大时,就非常影响查询效率,因此需要其他的数据结构来解决此类问题。就像二叉查找树、平衡二叉树、B树、B+树等,都是用来解决查询效率低的。 PS: 清山绿水始于尘,博学多识贵于勤。 我有酒,你有故事吗? 微信公众号:「清尘闲聊」。 欢迎一起谈天说地,聊代码。

April 25, 2019 · 1 min · jiezi

Leetcode116-填充同一层的兄弟节点2

题目给定一个完美二叉树,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下: struct Node { int val; Node *left; Node *right; Node *next;}填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。 初始状态下,所有 next 指针都被设置为 NULL。 示例: 输入:{"$id":"1","left":{"$id":"2","left":{"$id":"3","left":null,"next":null,"right":null,"val":4},"next":null,"right":{"$id":"4","left":null,"next":null,"right":null,"val":5},"val":2},"next":null,"right":{"$id":"5","left":{"$id":"6","left":null,"next":null,"right":null,"val":6},"next":null,"right":{"$id":"7","left":null,"next":null,"right":null,"val":7},"val":3},"val":1}输出:{"$id":"1","left":{"$id":"2","left":{"$id":"3","left":null,"next":{"$id":"4","left":null,"next":{"$id":"5","left":null,"next":{"$id":"6","left":null,"next":null,"right":null,"val":7},"right":null,"val":6},"right":null,"val":5},"right":null,"val":4},"next":{"$id":"7","left":{"$ref":"5"},"next":null,"right":{"$ref":"6"},"val":3},"right":{"$ref":"4"},"val":2},"next":null,"right":{"$ref":"7"},"val":1}解释:给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。题解方法一: 层序遍历使用层序遍历,遍历的时候把同层的节点连接起来; class Solution { public Node connect(Node root) { if (root == null) return null; Queue<Node> queue = new LinkedList<>(); queue.add(root); while (!queue.isEmpty()) { int size = queue.size(); Node current = null; while (size > 0) { Node node = queue.poll(); if (node.right != null) queue.add(node.right); if (node.left != null) queue.add(node.left); node.next = current; current = node; size--; } } return root; }}方法二:递归递归的时候我们通常就分解为递归子问题和递归结束条件。 ...

April 24, 2019 · 2 min · jiezi

【Leetcode】116. 填充同一层的兄弟节点

题目给定一个完美二叉树,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下: struct Node { int val; Node *left; Node *right; Node *next;}填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。 初始状态下,所有 next 指针都被设置为 NULL。 示例: 输入:{"$id":"1","left":{"$id":"2","left":{"$id":"3","left":null,"next":null,"right":null,"val":4},"next":null,"right":{"$id":"4","left":null,"next":null,"right":null,"val":5},"val":2},"next":null,"right":{"$id":"5","left":{"$id":"6","left":null,"next":null,"right":null,"val":6},"next":null,"right":{"$id":"7","left":null,"next":null,"right":null,"val":7},"val":3},"val":1}输出:{"$id":"1","left":{"$id":"2","left":{"$id":"3","left":null,"next":{"$id":"4","left":null,"next":{"$id":"5","left":null,"next":{"$id":"6","left":null,"next":null,"right":null,"val":7},"right":null,"val":6},"right":null,"val":5},"right":null,"val":4},"next":{"$id":"7","left":{"$ref":"5"},"next":null,"right":{"$ref":"6"},"val":3},"right":{"$ref":"4"},"val":2},"next":null,"right":{"$ref":"7"},"val":1}解释:给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。题解方法一: 层序遍历使用层序遍历,遍历的时候把同层的节点连接起来; class Solution { public Node connect(Node root) { if (root == null) return null; Queue<Node> queue = new LinkedList<>(); queue.add(root); while (!queue.isEmpty()) { int size = queue.size(); Node current = null; while (size > 0) { Node node = queue.poll(); if (node.right != null) queue.add(node.right); if (node.left != null) queue.add(node.left); node.next = current; current = node; size--; } } return root; }}方法二:递归递归的时候我们通常就分解为递归子问题和递归结束条件。 ...

April 21, 2019 · 2 min · jiezi

数据结构之「哈希表」

什么是哈希表?哈希表(Hash table, 也叫散列表),是根据键(Key)来直接访问在内存存储位置的数据结构。它通过一个哈希函数将所需要查询的数据映射到一张哈希表中,来提升查询效率。哈希函数的实现方法:1.除留余数法取关键字被某个不大于哈希表表长的数除后所得的余数为散列地址。2.折叠法将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址。3.平方取中法取关键字平方后的中间几位为哈希地址。4.直接定址法取关键字或关键字的某个线性函数值为哈希地址。哈希冲突不管用什么哈希函数去计算哈希地址,都是会产生哈希冲突的,因此我们需要想办法解决哈希冲突,并且在设计哈希函数时,尽可能减少哈希冲突。1.单独的链表法在哈希表的后面单独链上一个单链表来存储冲突的元素,JDK 1.8 里的 HashMap 就是选择的这种方式解决冲突的,不过它对链表做了一层优化。当元素个数大于等于 8 时,会把链表转换成红黑树,提升查询效率。2.线性探测法当发生哈希冲突时,逐个探测存放地址的表,直到查找到一个空单元。这个方式不便于查找,不建议使用。3.建立一个公共溢出区当发生哈希冲突时,就把元素存入到公用的溢出区,查询时遍历溢出区。从上面这几种处理方法来说,还是链表法效率比较高,推荐使用。不过都有现成的工具类使用,因此只需要知道实现原理,最好自己可以去写代码实现它。哈希表有什么用?哈希表在日常开发中还是比较常用的,因为它最优的查询时间复杂度是 O(1),当哈希冲突比较严重的时候,查询效率就相当于线性的,因此哈希算法直接影响到查询的效率。哈希表怎么实现的?哈希表的结构public class HashMap<K,V> { //用节点数组当作哈希表 Node<K,V>[] table; int size; //节点 static class Node<K,V> { //哈希值 final int hash; //键 final K key; //值 V value; //哈希值冲突时存储 Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } }}总结哈希表是一个键值对的存储结构,并且根据键进行哈希算法找到对应的存储位置。哈希算法会直接影响到哈希表的查询效率,一般选择哈希冲突小的实现方式,以便提升查询效率。当哈希冲突时,一般选择链表来存储冲突的元素,当冲突的元素增多时,可以采用红黑树来存储,以提升查询效率。JDK 1.8 版本的 HashMap,当链表个数大于等于 8 时,就是采用红黑树来存储的。在知道元素个数时,初始化哈希表时直接指定哈希表大小,因为当元素达到哈希表大小时,会做 resize 操作。当元素越来越多时,resize 是很耗时的,相当于重建哈希表。因此直接指定哈希表大小,减少 resize 次数以便提升插入性能。PS: 清山绿水始于尘,博学多识贵于勤。 我有酒,你有故事吗? 微信公众号:「清尘闲聊」。 欢迎一起谈天说地,聊代码。 ...

April 17, 2019 · 1 min · jiezi

手写布隆过滤器 第一场第一镜

随着最后一缕春风拂过,空气中弥漫起了夏天的味道,又该滚去学习了。最近在学习Redis,发现了一个好玩的东西叫布隆过滤器。可是我的水平又不足以研究源码,那我就自己写一个简单的玩玩。原理请原谅我的班门弄斧。我认为布隆过滤器就是用来判断key是否存在的,基于位图。有一个特点是,如果我说key不存在,那么您可以完全信任我,如果我说key存在,您可能就要掂量一下啦。恩恩,具体说来就是来了一个key,我们对其进行n次不同的hash,生成n个bit的索引,在位图里将这几个bit置成1,再来了另一个key,对其进行相同的hash,看看生成的索引对应的bit是否都为1,都为1即为(可能)存在,反之(一定)不存在。至于为什么会出现这种不准确的情况,可以去google一下,比我讲得清楚多了。开工首先,我们需要一个位图,既然决定纯手写,那就从0开始DIY了。老爷请上码:type BM int64//用切片来充当位图,切片的每个位与二级制位方向正好相反type BitMap struct { BitSlice []BM BitNum uint}//求一个BM的bit数,肯定有更好的计算方法var bmBitNum = uint(unsafe.Sizeof(BM(1)) * 8)//n为需要的bit数func NewBitMap(n int) *BitMap { //计算需要几个元素bit才能够 bitNum := uint(n) / bmBitNum + 1 return &BitMap { BitSlice : make([]BM,bitNum,bitNum), BitNum : uint(n), }}//n为位图的索引func (bm *BitMap) Set (n uint) { if n > bm.BitNum { return } //求出应该是切片的第几个元素 byteIndex := n / bmBitNum //求出是此元素的第几个bit bitIndex := n % bmBitNum //通过位运算将该bit置成1 bm.BitSlice[byteIndex] |= BM(uint(1) << bitIndex)}//同样的思路去找所在bit是否已经被置成1func (bm *BitMap) Get (n uint) bool{ if n > bm.BitNum { return false } byteIndex := n / bmBitNum bitIndex := n % bmBitNum return (bm.BitSlice[byteIndex] & BM(uint(1) << bitIndex)) != 0}OK,就这样我们完成了一个简单的位图。至于以后的各种优化只能等我水平高一些来再续前缘了。有了位图就可以搞布隆过滤器了。老弟来了://这里的两个切片是用来hash的,mod里最大的质数为101,我准备用3个hash生成3个bit,hash过后生成的最大的bit为101。var cap = []uint{7, 11, 13}var mod = []uint{31, 37, 101}//刚刚手写的位图派上用场了type BloomFilter struct { BitMap *bitMap.BitMap}//n依旧为需要的位数func NewBloomFilter(n int) *BloomFilter { return &BloomFilter { BitMap:bitMap.NewBitMap(n), }}//经过3次hashfunc (bf BloomFilter) Set(value string) { for i := 0; i < len(cap); i++ { bf.BitMap.Set(hash(value,i)) }}//同样规则的三次hash判是否存在func (bf BloomFilter) Exist(value string) bool { for i := 0; i < len(cap); i++ { if !bf.BitMap.Get(hash(value,i)) { return false } } return true}//我自己写的hash算法,浓浓的乡土气息。等我再学习一段时间肯定能搞一个更好的!func hash(s string,index int) uint { bit := uint(1) for i := 0; i < len(s); i++ { bit = (bit * cap[index] + (uint(s[i] - ‘a’) + uint(1))) % mod[index] } return bit}好了,就是这样。其实我只是想说,学习任何东西,要从原理抓起,要疯狂地实践。不要拿半路转行的码农不当程序员哦。最后,分享一个公众号吧,叫做算法梦想家,来跟我一起玩算法,玩音乐,聊聊文学创作,咱们一起天马行空! ...

April 16, 2019 · 1 min · jiezi

数据结构之「双端队列」

什么是双端队列?双端队列(deque)是指允许两端都可以进行入队和出队操作的队列,deque 是 “double ended queue” 的简称。那就说明元素可以从队头出队和入队,也可以从队尾出队和入队。双端队列怎么实现?双端队列的存储结构public class LinkedBlockingDeque<E> { //队头 Node<E> first; //队尾 Node<E> last; //元素个数 int count; static final class Node<E> { //存储元素 E item; //上一个元素 Node<E> prev; //下一个元素 Node<E> next; }}从队头入队public boolean offerFirst(Node<E> node) { //头节点临时变量 Node<E> f = first; //把当前的下一个节点指向头节点 node.next = f; //更新当前节点为头节点 first = node; //假如尾节点为空,则把当前节点设置为尾节点 if (last == null) last = node; //就把以前的头节点指向当前节点 else f.prev = node; //总数加一 ++count; return true;}从队头出队public E pollFirst() { Node<E> f = first; //头节点的下一个节点 Node<E> n = f.next; //获取头节点元素 E item = f.item; //置空 f.item = null; //孤立头节点,不指向任何节点 f.next = f; // help GC //重置头节点 first = n; //说明是最后一个节点 if (n == null) last = null; //否则把头节点的上一个节点置空 else n.prev = null; //总数减一 –count; return item;}从队尾入队public boolean offerLast(Node<E> node) { //尾节点临时变量 Node<E> l = last; if (l == null) return null; //把当前的上一个节点指向尾节点 node.prev = l; //更新当前节点为尾节点 last = node; //假如头节点为空,则把头节点置为当前节点 if (first == null) first = node; //否则把临时的尾节点的下一个节点指向当前节点 else l.next = node; //总数加一 ++count; return true;}从队尾出队public E pollLast() { Node<E> l = last; if (l == null) return null; //最后节点的上一个节点 Node<E> p = l.prev; //获取元素 E item = l.item; //置空 l.item = null; //孤立尾节点 l.prev = l; // help GC //更新尾节点 last = p; //假如是最后一个元素,置空头节点 if (p == null) first = null; //否则置空下一个节点指向 else p.next = null; //总数减一 –count; return item;}总结双端队列其实和队列差不多的,只是更加灵活了,队头和队尾均可进行入队和出队操作。这里是基于链表的双端队列实现,具体详情可查看 JDK 的 LinkedBlockingDeque 的实现,它还考虑了线程安全相关的东西,这里只是简单的一个实现,了解双端队列的结构和运作方式。PS: 清山绿水始于尘,博学多识贵于勤。 我有酒,你有故事吗? 微信公众号:「清尘闲聊」。 欢迎一起谈天说地,聊代码。 ...

April 16, 2019 · 2 min · jiezi

【Leetcode】114. 二叉树展开为链表

题目给定一个二叉树,原地将它展开为链表。例如,给定二叉树 1 / \ 2 5 / \ \3 4 6将其展开为:1 \ 2 \ 3 \ 4 \ 5 \ 6题解这算是比较经典的一道题目了, 博主面试快手的时候原题。最开始一想,觉得递归的求解不就好了,但是递归的时候发现需要注意一个地方就是:需要先递归右子树,然后记录下右子树展开完成之后的链表头。然后再递归的求解左子树,把左子树的最后一个链到右子树的链表头。基于这个,我们用一个pre指针来记录右子树的头结点。class Solution { private TreeNode prev = null; public void flatten(TreeNode root) { if (root == null) return; flatten(root.right); flatten(root.left); root.right = prev; root.left = null; prev = root; }}递归的方式转换为迭代的方式用stack就好了,反而比较好理解。class Solution { public void flatten(TreeNode root) { if (root == null) return; Stack<TreeNode> stack = new Stack<TreeNode>(); stack.push(root); while (!stack.isEmpty()) { TreeNode current = stack.pop(); if (current.right != null) stack.push(current.right); if (current.left != null) stack.push(current.left); if (!stack.isEmpty()) current.right = stack.peek(); current.left = null; } }}有问题加手撕代码QQ群讨论:805423079 ...

April 16, 2019 · 1 min · jiezi

数据结构之「队列」

什么是队列?队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。是一种先进先出(First In First Out)的线性表,简称 FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。队列有 2 种方式来存储:数组 和 链表。数组我们都知道它是预先分配好长度的,因此会出现溢出现象,而且删除元素需要向队头移动一个位置,时间复杂度就变成 O(n)。因此,需要一种新的方式来解决这个问题,那就是循环队列。队列的这种头尾相接的顺序存储结构称为循环队列。为了避免队列删除元素需要移动整个队列,使得队头和队尾可以在数组中循环变化。解决了移动元素的时间损耗,使得本来删除是 O(n) 的时间复杂度变成了 O(1)。1. 数组式队列申请一片连续的存储空间,并设置两个指针进行管理。一个是队头指针front,它指向队头元素;另一个是队尾指针rear,它指向下一个入队元素的存储位置。一般是实现循环队列,队头和队尾会随着入队和出队的变化而变化的。例如 JDK 中 ArrayBlockingQueue 就是基于循环队列来实现的。不过它会有溢出现象,一般解决方案是如果队列满了,设置入队等待时间或者返回入队不成功。一般在确定元素个数情况下使用,如果不确定元素个数,建议使用链表式队列。2. 链表式队列它就是基于链表存储结构的队列,可以动态的创建和删除元素,不用关心队列的长度,因此不用担心溢出现象。新元素插入到队尾,读取的时候从队头开始,每次读取一个元素,释放一个元素,这就是所谓的动态创建和动态删除。队列有什么用?1. 保证输入顺序比如吃饭排队,先找服务员拿个号码,上面会写着前面还有 n 桌,这就相当于服务员把你加入了她们店的队列中,当有空位置时,就直接叫你入座吃饭,当没有空位子时,要么排队等候,要么换一家吃饭。这就是队列的用处。2. 解耦在系统设计中,好的设计是低耦合,高内聚。意思就是一个系统只做一件事情,把一件事情做好。既方便代码维护,又方便扩展。比如又一个下单的场景,用户下单之后需要加积分,需要给各种优惠券等等。我们就可以利用队列来解偶,但真实使用一般是用开源的消息队列,如Rocket MQ,Kafka 等等。3. 提升系统吞吐量就拿上面订单来说,本来以前是用户下单,给用户添加积分,给用户发优惠券是一起处理成功后返回的。现在只是处理下单,然后告诉某某系统,给某某用户加积分,给某某用户发优惠券了,它自己并不真正做这个动作,所以会提升系统响应,当需要保证最终一致性。一般也是用开源的消息队列来完成的。队列怎么实现?这里是基于数组的循环队列实现,也就是 JDK 的 ArrayBlockingQueue。有兴趣的朋友也可以看看基于链表的 LinkedBlockingQueue 的实现。存储结构public class ArrayBlockingQueue<E> { //用数组来存储元素 Object[] items; //数组里出队的下标 int takeIndex; //入队的下标 int putIndex; //元素个数 int count;}入队public boolean offer(E e) { //加入数组满了,则返回入队失败 if (count == items.length) { return false; } else { //获得当前数组 Object[] items = this.items; //把元素 e 加入到队尾 items[putIndex] = e; //判断是不是队尾是不是倒数第二个元素 //是的话把下标为 0 置为队尾,说明一圈了 if (++putIndex == items.length) { putIndex = 0; } //总数加一 count++; return true; }}出队public E poll() { //空队列则返回 null if (count == 0) { return null; } else { //获取当前数组 Object[] items = this.items; //获取队头元素 E item = (E) items[takeIndex]; //置空 items[takeIndex] = null; //重置队头为下标 0; if (++takeIndex == items.length) { takeIndex = 0; } //总数减一 count–; return item; }}总结队列的作用还是很大的,比如保证输入顺序,解偶,提升系统吞吐量等等,都基于队列的原理来实现的。队列有数组式队列和链表式队列。数组式队列就是基于数组存储结构来实现的,数组的优缺点它全有。链表式队列就是基于链表存储结构来实现的,它也包含来链表的优缺点。所以在使用时可以根绝业务需求来选择最优的方案。PS: 清山绿水始于尘,博学多识贵于勤。 我有酒,你有故事吗? 微信公众号:「清尘闲聊」。 欢迎一起谈天说地,聊代码。 ...

April 15, 2019 · 1 min · jiezi

【Leetcode】113. 路径总和II

题目给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。说明: 叶子节点是指没有子节点的节点。示例:给定如下二叉树,以及目标和 sum = 22, 5 / \ 4 8 / / \ 11 13 4 / \ / \ 7 2 5 1返回:[ [5,4,11,2], [5,8,4,5]]题解这道题目是上一道的延伸,但是需要记录下路径,返回回去。这就是一个典型的backtrack的题目了。我们用迭代的方式需要记录中间的路径状态,稍显复杂,所以我们想用递归的方式来解,先探索左子树,然后探索右子树。如果都探索完之后,右满足的就加入到最终结果中。public class Solution { public List<List<Integer>> pathSum(TreeNode root, int sum) { List<List<Integer>> res = new LinkedList<>(); helper(root, sum, res, new LinkedList<>()); return res; } public void helper(TreeNode root, int sum, List<List<Integer>> res, List<Integer> current) { if (root == null) { return; } current.add(root.val); if (root.left == null && root.right == null && sum == root.val) { // leaf node. res.add(new LinkedList<>(current)); // back track. current.remove(current.size() - 1); return; } helper(root.left, sum - root.val, res, current); helper(root.right, sum - root.val, res, current); // back track. current.remove(current.size() - 1); }}热门阅读技术文章汇总【Leetcode】103. 二叉树的锯齿形层次遍历【Leetcode】102. 二叉树的层次遍历【Leetcode】101. 对称二叉树【Leetcode】100. 相同的树【Leetcode】98. 验证二叉搜索树手撕代码QQ群:805423079, 群密码:1024 ...

April 14, 2019 · 1 min · jiezi

数据结构之「栈」

什么是栈?栈 是一种抽象的数据结构,只允许在有序线性数据集合的一段进行入栈和出栈操作,因此是按照后进先出(LIFO, Last In First Out)的原理操作元素。允许进行入栈和出栈的一端称为栈顶(TOP),另一端称为栈底(BOTTOM)。栈顶会随出栈入栈浮动的,当栈中元素个数为零时称为空栈。栈常用一维数组或链表来实现。栈的特点:先进后出,后入先出。除头尾节点外,每个元素有一个前驱节点,一个后继节点。栈有什么作用?由于栈结构是后进先出原则,常用于程序里的递归,函数调用,局部变量临时存储,浏览器里的前进后。在大话数据结构和算法一书中举例特形象,手枪的弹夹就是栈的典型使用,装弹其实就是入栈操作,开枪其实就是出栈操作。栈该怎么使用?我用一维数组来简单实现入栈出栈。栈的存储结构public class Stack { //存储元素 Object[] elementData; //元素个数 int elementCount;}入栈public E push(E item) { //需要的最小容量是当前元素总数 + 1 int minCapacity = elementCount + 1; //假如最小需要元素总数大于数组长度,这扩容一倍 if (minCapacity - elementData.length > 0) { int newCapacity = minCapacity * 2; elementData = Arrays.copyOf(elementData, newCapacity); } //给元素总数加一并把元素赋值给数组 elementData[elementCount++] = item;}出栈public E pop() { //获取元素总数 int len = elementCount; //获取栈顶元素 E obj = (E) elementData[len - 1]; //元素减一 elementCount–; //置空 elementData[elementCount] = null;}总结栈 在计算机的世界里是一个非常常用的数据结构,函数调用的存储和函数里分配的局部变量都用栈结构来存储的。浏览器里的前进和后退也就是一个入栈和出栈的操作。这里是用数组的方式来作为栈的存储结构,有兴趣的朋友可以用链表的方式来实现栈结构。PS: 清山绿水始于尘,博学多识贵于勤。 我有酒,你有故事吗? 微信公众号:「清尘闲聊」。 欢迎一起谈天说地,聊代码。 ...

April 14, 2019 · 1 min · jiezi

JDK源码那些事儿之红黑树基础下篇

说到HashMap,就一定要说到红黑树,红黑树作为一种自平衡二叉查找树,是一种用途较广的数据结构,在jdk1.8中使用红黑树提升HashMap的性能,今天就来说一说红黑树,上一讲已经给出插入平衡的调整操作,这一讲就说说更为复杂的删除平衡操作。前言限于篇幅,本文只对红黑树的基础进行说明,暂不涉及源码部分,大部分摘抄自维基百科,这里也贴出对应链接:维基百科(中文):https://zh.wikipedia.org/wiki…维基百科:https://en.wikipedia.org/wiki…笔者这里会根据维基百科的讲解做些说明,方便初学者理解。删除平衡在正式进入红黑树的删除说明之前,想下二叉搜索树中的删除是如何做的?参照二叉搜索树的删除调整原理:如果需要删除的节点有两个儿子,那么问题可以被转化成删除另一个只有一个儿子的节点的问题(为了表述方便,这里所指的儿子,为非叶子节点的儿子)。对于二叉查找树,在删除带有两个非叶子儿子的节点的时候,我们要么找到它左子树中的最大元素、要么找到它右子树中的最小元素,并把它的值转移到要删除的节点中(如在这里所展示的那样)。我们接着删除我们从中复制出值的那个节点,它必定有少于两个非叶子的儿子。因为只是复制了一个值(没有复制颜色),不违反任何性质,这就把问题简化为如何删除最多有一个儿子的节点的问题。它不关心这个节点是最初要删除的节点还是我们从中复制出值的那个节点。那么红黑树中会出现哪些情况呢?删除节点的左右子树都非空删除节点的左子树为空,右子树非空删除节点的右子树为空,左子树非空删除节点的左右子树都为空对于第一种情况,我们可以找到删除节点的后继节点,将值替换,然后删除后继节点,这样保证了该树仍然是一棵二叉树,但是在删除后继节点时可能破坏了红黑树的特性,故需要进行调整。强调一下,红黑树中的叶子节点指的都是NIL节点。这样来看,被删除的节点一定有一个右子树,可能为NIL也可能为非空子树,接下来就具体看看情况。1.如果删除节点E为红色,则E子节点F则必为黑色(红黑树特性),这种情况只有在E的两个节点都为叶子节点时才会发生。故删除之后红黑树平衡不用调整。可以自行画图验证:删除节点E有两个非叶子节点,这不可能,因为E已经是后继节点。E有一个非叶子节点(右子节点),则这个非叶子节点F为黑色,通过E的两条黑色路径不同,不满足特性52.如果删除节点E为黑色,F为红色,这种情况下,F节点有两个叶子节点(需保证红黑树特性,黑色路径需保持一致)则F放置到E处时只需要变色就可以使得红黑树平衡3.如果删除节点E为黑色,F也为黑色,这种情况只有在E的两个节点都为叶子节点时才会发生。参考上边情况1的验证。这里删除了一个黑色节点,红黑树平衡被破坏(黑色路径不同了),需要进行调整针对3这里就又会有以下几种情况:N为删除的位置节点,现在被删除的节点的子节点取代(这里子节点对应上边的F,即删除后,N的位置为叶子节点),N为黑色,P为N的父母,S为P的右子,SL代表S的左子,SR代表S的右子。S必不为叶子节点,反推下,如果为叶子节点,在未删除之前P的两边黑色路径就不一致了(未删除之前是P->E->F这种,自行画图理解)。注意,下面列举的情况都是在删除E节点后,子节点取代E形成的情况,在此基础上进行红黑树的调整。按照顺序每种情况进行判断处理。注意每种情况处理之后会重新标记,以适应下次情况的对比调整,并且下列过程只以第一次调用时说明,递归调用下列顺序过程时将叶子节点当成一个已经平衡的局部红黑树即可。和之前的插入平衡调整类似,每次都是局部化调整。第一种情况:如果N为根节点,不需要调整平衡了,原有树只有一个非叶子节点,两个叶子节点,删除了根节点,相当于删除了红黑树。继续第二种情况判断。第二种情况:如果N(这里是叶子节点NIL)是其父P的左子节点,S为红色,P必为黑色,参照下图,反转P和S的颜色,然后在P处向左旋转,将S转换为N的祖父母。这里N处的黑色路径少一个,还未平衡。N是其父P的右子节点参照相似处理。SL标记为新的S继续以N,P,S这块局部树进行处理。继续第三种情况处理。第三种情况:如果P,S和S的孩子都是黑色,左图显示了出现的情况,在N替换掉之前的父节点后形成的状态,这里重新绘制S为红色,变为右图,在这个局部中满足平衡红黑树的特性4和5,但是通过P节点的黑色路径相比原有结构减少了1,还需要进行调整,需重新进行平衡。这里重新平衡即从第一种情况再次顺序执行,以P节点进行重新平衡,相当于以P为新的N,黑色路径少1,再次进行平衡调整。不满足当前情况,再继续执行第4种情况处理。第四种情况:如果S和S的孩子是黑色,但P是红色的。在这种情况下,我们只需交换S和P的颜色。这不会影响通过S的路径上的黑色节点数量,但它确实会在通过N的路径上添加一个黑色节点数,从而弥补这些路径上已删除的黑色节点。将达到红黑树平衡。不满足当前情况,则继续第五种情况的处理。第五种情况:如果S是黑色,S的左孩子是红色,S的右孩子是黑色,N是其父母的左孩子。在这种情况下,我们在S处右转,这样S的左边孩子就成了S的父母和N的新兄弟。然后我们交换S及其新父母的颜色。所有路径仍然有相同数量的黑色结点,但是P的左子树因为删除一个节点导致黑色路径少1,还未完全平衡。这里进行调整主要是为了满足第六种情况,继续第六种情况的处理。第六种情况: 如果S是黑色,S的右子是红色,N是其父P的左子。在这种情况下,我们向左旋转P,这样S成为P和S的右子的父亲。然后,我们交换P和S的颜色,并使S的右子节点变黑。比较删除前与N替换调整后的属性,满足4和5,与删除前是一致的。总结删除操作相对插入操作之后的平衡要复杂的多,不过按照情况一步步处理也是比较明了的,同样为了方便初学者理解,从上边的过程我们也可以发现,在一次局部平衡调整中,最多进行3次旋转操作,我这里再进行一个流程梳理,帮助各位更好的理解红黑树的删除操作。到此关于红黑树的基础已经介绍完毕,下一章我将就JDK源码中的TreeNode进行讲解说明,看一看红黑树是如何在源码中实现的。参考资料:https://zh.wikipedia.org/wiki…https://en.wikipedia.org/wiki...https://my.oschina.net/hosee/...https://www.cnblogs.com/tongy…

April 13, 2019 · 1 min · jiezi

推荐一个采用方便程序员在线动画学习常用算法的良心网站

网址:https://algorithm-visualizer….进去之后的页面是程序员熟悉的码农风格:假设我想学习冒泡排序算法,在搜索栏里输入sort,在结果列表里选择bubble sort:点击之后,排序操作处于就绪状态,点击play开始:此时右边的JavaScript代码像我们平时单步调试一样逐行执行,同时每一步执行后排序的效果在屏幕正中实时显示:比单步调试更强大之处是,我们能随时回退到前面的执行结果,通过下图高亮的84/144这个柱状开关控制。144意思是这个排序全过程总共要进行144次单步执行,当前已经执行了84步。自动播放的速度也可以在下图所示的Speed开关控制。这是非波拉契数列的生成动画:二叉树的遍历动画:Dijkstra迪杰斯特拉算法最短路径算法:有了这个网站,算法学习从此不再枯燥。这个网站的源代码是完全开源的,如果你有新的算法想给全世界的编程爱好者展示,可以按照Readme.md里定义的规范,提交您的动画。https://github.com/algorithm-…截至2019年3月16日,已经有14000多个赞了,顺手去点一个吧。要获取更多Jerry的原创文章,请关注公众号"汪子熙":

April 13, 2019 · 1 min · jiezi

数据结构之「链表」

什么是链表?链表是一种线性表,但并不会按线性的顺序存储数据,而是在每一个节点里存储到下一个节点的指针 (Pointer)。因此它不需要分配连续的存储空间,也不需要预先固定元素的大小,它可以动态的添加和删除元素,而且时间复杂度是 O(1)。只不过查找某个元素时,时间复杂度是 O(n)。链表有多种不同的类型:单向链表,双向链表和循环链表。单向链表单向链表是最简单的一种,它包含两个域,一个信息域和一个指针域。这个指针域指向列表中的下一个节点,而最后一个节点则指向一个空值。双向链表双向链表中不仅有指向后一个节点的指针,还有指向前一个节点的指针。这样可以从任何一个节点访问前一个节点,当然也可以访问后一个节点。循环链表循环链表是把链表的首节点和末节点连接在一起,形成一个环。这种方式在单向和双向链表中皆可实现。链表有什么作用?根据链表的特性,动态的把元素加入到链表中,不需要预先指定链表长度,理论上链表可以无限长,直到内存耗尽。链表会存储下一个元素的地址,因此插入和删除会很方便,但查询指定元素时,最坏的情况是遍历整个链表。链表该怎么使用?这里我用 Java 语言来简单实现链表,可参考 JDK 的 LinkedList。构建链表结构//创建一个私有的静态的Node泛型对象public class LinkedList<E> {Node<E> first;//第一个节点Node<E> last;//最后一个节点private static class Node<E> { E item;//存储元素 Node<E> next;//下一个元素 Node<E> prev;//上一个元素 //通过构造器设置值 Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; }}在链表后插入元素,前面插入也类似//中间变量存储链表的最后一个节点Node<E> l = last;//构建新节点,头节点指向链表的最后节点Node<E> newNode = new Node<>(l, 5, null);//把新节点当作最后一个节点last = newNode;//if 最后节点为空,说明是一个新链表if (l == null) first = newNode;//否则把链表最后一个节点的 next 节点指向新节点else l.next = newNode;删除指定节点 dE element = d.item;Node<E> next = d.next;Node<E> prev = d.prev;//if 当前节点的上一个节点为空,//说明删除的是第一个节点,//把当前节点的下一个节点置为第一个节点。if (prev == null) { first = next;//否则把上一个节点的下一个节点指向当前节点的下一个节点,//相当于跳过了当前节点,并把当前节点的上一个节点置空。} else { prev.next = next; d.prev = null;}//if 当前节点的下一个节点为空,说明是最后一个节点。//把最后一个节点置为上一个节点。if (next == null) { last = prev;//否则把当前的上一节点指向当前的下一个节点,//相当于跳过当前节点,在把当前节点的下一个节点置空。} else { next.prev = prev; d.next = null;}//置空当前节点元素,帮助 GC 回收d.item = null;遍历链表Node<E> d = first;while (d != null && d.next != null) { System.out.println(d.item); d = d.next;}总结数组需要初始化确定好长度,并且不能修改数组的长度。在有的场景下,是不知道有多少元素的,因此我们是不能准确的分配数组的长度。但也提供了数组扩容的方案,就是在现有的数组大小上,在按一定算法来创建一个新的数组,并把数组的数据拷贝进扩容后的数组里去,但数组的扩容是很影响性能的。因此需要一种新的数据结构来存储不能确定有多少元素的数据,这就是链表的应用场景。但它也有缺点,每个节点多需要多的空间来存储下一个节点的地址,查询时最坏情况是遍历整个数组。它的优点是不需要预分配固定大小,不限制元素个数,理论上可以直到内存耗尽,插入和删除时间复杂度是 O(1)。PS: 清山绿水始于尘,博学多识贵于勤。 我有酒,你有故事吗? 公众号:「清尘闲聊」。 欢迎一起谈天说地,聊代码。 ...

April 13, 2019 · 1 min · jiezi

【Leetcode】125. 验证回文串

题目给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。说明:本题中,我们将空字符串定义为有效的回文串。示例 1:输入: “A man, a plan, a canal: Panama"输出: true示例 2:输入: “race a car"输出: false题解这道题目就比较简单了,用两个指针一前一后,遇到不是字母的就直接忽略过就好了。可能对java的一些方法不太熟悉,注释说明一下:class Solution { public boolean isPalindrome(String s) { if (s == null) return false; if (s.length() == 0) return true; int i = 0; int j = s.length() - 1; while (i < j) { // isLetterOrDigit 判断是不是字母或者数字 while (i < j && !Character.isLetterOrDigit(s.charAt(i))) i++; while (i < j && !Character.isLetterOrDigit(s.charAt(j))) j–; // toLowerCase 都转化为小写的字母 if (Character.toLowerCase(s.charAt(i)) != Character.toLowerCase(s.charAt(j))) return false; i++; j–; } return true; }}手撕代码QQ群:805423079, 群密码:1024 ...

April 9, 2019 · 1 min · jiezi

【Leetcode】124. 二叉树中的最大路径和

题目给定一个非空二叉树,返回其最大路径和。本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。示例 1:输入: [1,2,3] 1 / \ 2 3输出: 6示例 2:输入: [-10,9,20,null,null,15,7] -10 / \ 9 20 / \ 15 7输出: 42题解这道题虽然是标记成hard难度的题目,但是我觉得主要是因为需要处理的细节可能需要仔细考虑,倒不是因为题目本身比较复杂。我们很容易想到用递归去解这道题。递归子问题:左子树的路径和 + 右子树的路径和 + 当前结点的数字class Solution { private int maxSum; public int maxPathSum(TreeNode root) { maxSum = Integer.MIN_VALUE; helper(root); return maxSum; } private int helper(TreeNode root) { if (root == null) return 0; int leftSum = Math.max(helper(root.left), 0); // 和0比较,要么要这个分支,要么不要这个分支 int rightSum = Math.max(helper(root.right), 0); // 当前节点路径下最大值和全局最大值做比较 maxSum = Math.max(leftSum + rightSum + root.val, maxSum); // 返回的是左右子树中最大的 + 当前结点的值作为这棵树的路径。因为必须要走根节点。 return Math.max(leftSum, rightSum) + root.val; }}考虑以下极端情况: -2 / \ -1 -3手撕代码QQ群:805423079, 群密码:1024 ...

April 9, 2019 · 1 min · jiezi

社招面试总结——算法题篇

面试结果总结下最近的面试:头条后端:3面技术面挂蚂蚁支付宝营销-机器学习平台开发: 技术面通过,年后被通知只有P7的hc蚂蚁中台-机器学习平台开发: 技术面通过, 被蚂蚁HR挂掉(脉脉上好多人遇到这种情况,一个是今年大环境不好,另一个,面试尽量不要赶上阿里财年年底,这算是一点tips吧)快手后端: 拿到offer百度后端: 拿到offer最终拒了百度,去快手了, 一心想去阿里, 个人有点阿里情节吧,缘分差点。总结下最近的面试情况, 由于面了20多面, 就按照题型分类给大家一个总结。推荐大家每年都要抽出时间去面一下,不一定跳槽,但是需要知道自己的不足,一定要你的工龄匹配上你的能力。比如就我个人来说,通过面试我知道数据库的知识不是很懂,再加上由于所在组对数据库接触较少,这就是短板,作为一个后端工程师对数据库说不太了解是很可耻的,在选择offer的时候就可以适当有偏向性。下面分专题把最近的面试总结和大家总结一下。过分简单的就不说了,比如打印一个图形啥的, 还有的我不太记得清了,比如快手一面好像是一道动态规划的题目。当然,可能有的解决方法不是很好,大家可以在手撕代码群里讨论。最后一篇我再谈一下一些面试的技巧啥的。麻烦大家点赞转发支持下~股票买卖(头条)Leetcode 上有三题股票买卖,面试的时候只考了两题,分别是easy 和medium的难度Leetcode 121. 买卖股票的最佳时机给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。注意你不能在买入股票前卖出股票。示例 1:输入: [7,1,5,3,6,4]输出: 5解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。示例 2:输入: [7,6,4,3,1]输出: 0解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。题解纪录两个状态, 一个是最大利润, 另一个是遍历过的子序列的最小值。知道之前的最小值我们就可以算出当前天可能的最大利润是多少class Solution { public int maxProfit(int[] prices) { // 7,1,5,3,6,4 int maxProfit = 0; int minNum = Integer.MAX_VALUE; for (int i = 0; i < prices.length; i++) { if (Integer.MAX_VALUE != minNum && prices[i] - minNum > maxProfit) { maxProfit = prices[i] - minNum; } if (prices[i] < minNum) { minNum = prices[i]; } } return maxProfit; }}Leetcode 122. 买卖股票的最佳时机 II这次改成股票可以买卖多次, 但是你必须要在出售股票之前把持有的股票卖掉。示例 1:输入: [7,1,5,3,6,4]输出: 7解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。 随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。示例 2:输入: [1,2,3,4,5]输出: 4解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。 注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。 因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。示例 3:输入: [7,6,4,3,1]输出: 0解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。题解由于可以无限次买入和卖出。我们都知道炒股想挣钱当然是低价买入高价抛出,那么这里我们只需要从第二天开始,如果当前价格比之前价格高,则把差值加入利润中,因为我们可以昨天买入,今日卖出,若明日价更高的话,还可以今日买入,明日再抛出。以此类推,遍历完整个数组后即可求得最大利润。class Solution { public int maxProfit(int[] prices) { // 7,1,5,3,6,4 int maxProfit = 0; for (int i = 0; i < prices.length; i++) { if (i != 0 && prices[i] - prices[i-1] > 0) { maxProfit += prices[i] - prices[i-1]; } } return maxProfit; }}LRU cache (头条、蚂蚁)这道题目是头条的高频题目,甚至我怀疑,头条这个面试题是题库里面的必考题。看脉脉也是好多人遇到。第一次我写的时候没写好,可能由于这个挂了。转自知乎:https://zhuanlan.zhihu.com/p/…题目运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。进阶:你是否可以在 O(1) 时间复杂度内完成这两种操作?LRUCache cache = new LRUCache( 2 /* 缓存容量 / );cache.put(1, 1);cache.put(2, 2);cache.get(1); // 返回 1cache.put(3, 3); // 该操作会使得密钥 2 作废cache.get(2); // 返回 -1 (未找到)cache.put(4, 4); // 该操作会使得密钥 1 作废cache.get(1); // 返回 -1 (未找到)cache.get(3); // 返回 3cache.get(4); // 返回 4题解这道题在今日头条、快手或者硅谷的公司中是比较常见的,代码要写的还蛮多的,难度也是hard级别。最重要的是LRU 这个策略怎么去实现,很容易想到用一个链表去实现最近使用的放在链表的最前面。比如get一个元素,相当于被使用过了,这个时候它需要放到最前面,再返回值,set同理。那如何把一个链表的中间元素,快速的放到链表的开头呢?很自然的我们想到了双端链表。基于 HashMap 和 双向链表实现 LRU 的整体的设计思路是,可以使用 HashMap 存储 key,这样可以做到 save 和 get key的时间都是 O(1),而 HashMap 的 Value 指向双向链表实现的 LRU 的 Node 节点,如图所示。LRU 存储是基于双向链表实现的,下面的图演示了它的原理。其中 head 代表双向链表的表头,tail 代表尾部。首先预先设置 LRU 的容量,如果存储满了,可以通过 O(1) 的时间淘汰掉双向链表的尾部,每次新增和访问数据,都可以通过 O(1)的效率把新的节点增加到对头,或者把已经存在的节点移动到队头。下面展示了,预设大小是 3 的,LRU存储的在存储和访问过程中的变化。为了简化图复杂度,图中没有展示 HashMap部分的变化,仅仅演示了上图 LRU 双向链表的变化。我们对这个LRU缓存的操作序列如下:save(“key1”, 7)save(“key2”, 0)save(“key3”, 1)save(“key4”, 2)get(“key2”)save(“key5”, 3)get(“key2”)save(“key6”, 4)相应的 LRU 双向链表部分变化如下:总结一下核心操作的步骤:save(key, value),首先在 HashMap 找到 Key 对应的节点,如果节点存在,更新节点的值,并把这个节点移动队头。如果不存在,需要构造新的节点,并且尝试把节点塞到队头,如果LRU空间不足,则通过 tail 淘汰掉队尾的节点,同时在 HashMap 中移除 Key。get(key),通过 HashMap 找到 LRU 链表节点,因为根据LRU 原理,这个节点是最新访问的,所以要把节点插入到队头,然后返回缓存的值。 private static class DLinkedNode { int key; int value; DLinkedNode pre; DLinkedNode post; } /* * 总是在头节点中插入新节点. / private void addNode(DLinkedNode node) { node.pre = head; node.post = head.post; head.post.pre = node; head.post = node; } /* * 摘除一个节点. / private void removeNode(DLinkedNode node) { DLinkedNode pre = node.pre; DLinkedNode post = node.post; pre.post = post; post.pre = pre; } /* * 摘除一个节点,并且将它移动到开头 / private void moveToHead(DLinkedNode node) { this.removeNode(node); this.addNode(node); } /* * 弹出最尾巴节点 / private DLinkedNode popTail() { DLinkedNode res = tail.pre; this.removeNode(res); return res; } private HashMap<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>(); private int count; private int capacity; private DLinkedNode head, tail; public LRUCache(int capacity) { this.count = 0; this.capacity = capacity; head = new DLinkedNode(); head.pre = null; tail = new DLinkedNode(); tail.post = null; head.post = tail; tail.pre = head; } public int get(int key) { DLinkedNode node = cache.get(key); if (node == null) { return -1; // cache里面没有 } // cache 命中,挪到开头 this.moveToHead(node); return node.value; } public void put(int key, int value) { DLinkedNode node = cache.get(key); if (node == null) { DLinkedNode newNode = new DLinkedNode(); newNode.key = key; newNode.value = value; this.cache.put(key, newNode); this.addNode(newNode); ++count; if (count > capacity) { // 最后一个节点弹出 DLinkedNode tail = this.popTail(); this.cache.remove(tail.key); count–; } } else { // cache命中,更新cache. node.value = value; this.moveToHead(node); } } 二叉树层次遍历(头条)这个题目之前也讲过,Leetcode 102题。题目给定一个二叉树,返回其按层次遍历的节点值。 (即逐层地,从左到右访问所有节点)。例如:给定二叉树: [3,9,20,null,null,15,7], 3 / \ 9 20 / \ 15 7返回其层次遍历结果:[ [3], [9,20], [15,7]]题解我们数据结构的书上教的层序遍历,就是利用一个队列,不断的把左子树和右子树入队。但是这个题目还要要求按照层输出。所以关键的问题是: 如何确定是在同一层的。我们很自然的想到:如果在入队之前,把上一层所有的节点出队,那么出队的这些节点就是上一层的列表。由于队列是先进先出的数据结构,所以这个列表是从左到右的。/* * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } /class Solution { public List<List<Integer>> levelOrder(TreeNode root) { List<List<Integer>> res = new LinkedList<>(); if (root == null) { return res; } LinkedList<TreeNode> queue = new LinkedList<>(); queue.add(root); while (!queue.isEmpty()) { int size = queue.size(); List<Integer> currentRes = new LinkedList<>(); // 当前队列的大小就是上一层的节点个数, 依次出队 while (size > 0) { TreeNode current = queue.poll(); if (current == null) { continue; } currentRes.add(current.val); // 左子树和右子树入队. if (current.left != null) { queue.add(current.left); } if (current.right != null) { queue.add(current.right); } size–; } res.add(currentRes); } return res; }}这道题可不可以用非递归来解呢?递归的子问题:遍历当前节点, 对于当前层, 遍历左子树的下一层层,遍历右子树的下一层递归结束条件: 当前层,当前子树节点是null/* * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } /class Solution { public List<List<Integer>> levelOrder(TreeNode root) { List<List<Integer>> res = new LinkedList<>(); if (root == null) { return res; } levelOrderHelper(res, root, 0); return res; } /* * @param depth 二叉树的深度 */ private void levelOrderHelper(List<List<Integer>> res, TreeNode root, int depth) { if (root == null) { return; } if (res.size() <= depth) { // 当前层的第一个节点,需要new 一个list来存当前层. res.add(new LinkedList<>()); } // depth 层,把当前节点加入 res.get(depth).add(root.val); // 递归的遍历下一层. levelOrderHelper(res, root.left, depth + 1); levelOrderHelper(res, root.right, depth + 1); }}二叉树转链表(快手)这是Leetcode 104题。给定一个二叉树,原地将它展开为链表。例如,给定二叉树 1 / \ 2 5 / \ \3 4 6将其展开为:1 \ 2 \ 3 \ 4 \ 5 \ 6这道题目的关键是转换的时候递归的时候记住是先转换右子树,再转换左子树。所以需要记录一下右子树转换完之后链表的头结点在哪里。注意没有新定义一个next指针,而是直接将right 当做next指针,那么Left指针我们赋值成null就可以了。class Solution { private TreeNode prev = null; public void flatten(TreeNode root) { if (root == null) return; flatten(root.right); // 先转换右子树 flatten(root.left); root.right = prev; // 右子树指向链表的头 root.left = null; // 把左子树置空 prev = root; // 当前结点为链表头 }}用递归解法,用一个stack 记录节点,右子树先入栈,左子树后入栈。class Solution { public void flatten(TreeNode root) { if (root == null) return; Stack<TreeNode> stack = new Stack<TreeNode>(); stack.push(root); while (!stack.isEmpty()) { TreeNode current = stack.pop(); if (current.right != null) stack.push(current.right); if (current.left != null) stack.push(current.left); if (!stack.isEmpty()) current.right = stack.peek(); current.left = null; } }}二叉树寻找最近公共父节点(快手)Leetcode 236 二叉树的最近公共父亲节点题解从根节点开始遍历,如果node1和node2中的任一个和root匹配,那么root就是最低公共祖先。 如果都不匹配,则分别递归左、右子树,如果有一个 节点出现在左子树,并且另一个节点出现在右子树,则root就是最低公共祖先. 如果两个节点都出现在左子树,则说明最低公共祖先在左子树中,否则在右子树。public class Solution { public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { //发现目标节点则通过返回值标记该子树发现了某个目标结点 if(root == null || root == p || root == q) return root; //查看左子树中是否有目标结点,没有为null TreeNode left = lowestCommonAncestor(root.left, p, q); //查看右子树是否有目标节点,没有为null TreeNode right = lowestCommonAncestor(root.right, p, q); //都不为空,说明做右子树都有目标结点,则公共祖先就是本身 if(left!=null&&right!=null) return root; //如果发现了目标节点,则继续向上标记为该目标节点 return left == null ? right : left; } }数据流求中位数(蚂蚁)面了蚂蚁中台的团队,二面面试官根据汇报层级推测应该是P9级别及以上,在美国面我,面试风格偏硅谷那边。题目是hard难度的,leetcode 295题。这道题目是Leetcode的hard难度的原题。给定一个数据流,求数据流的中位数,求中位数或者topK的问题我们通常都会想用堆来解决。但是面试官又进一步加大了难度,他要求内存使用很小,没有磁盘,但是压榨空间的同时可以忍受一定时间的损耗。且面试官不仅要求说出思路,要写出完整可经过大数据检测的production code。先不考虑内存不考虑内存的方式就是Leetcode 论坛上的题解。基本思想是建立两个堆。左边是大根堆,右边是小根堆。如果是奇数的时候,大根堆的堆顶是中位数。例如:[1,2,3,4,5]大根堆建立如下: 3 / \ 1 2小根堆建立如下: 4 / 5 偶数的时候则是最大堆和最小堆顶的平均数。例如: [1, 2, 3, 4]大根堆建立如下: 2 / 1 小根堆建立如下: 3 / 4 然后再维护一个奇数偶数的状态即可求中位数。public class MedianStream { private PriorityQueue<Integer> leftHeap = new PriorityQueue<>(5, Collections.reverseOrder()); private PriorityQueue<Integer> rightHeap = new PriorityQueue<>(5); private boolean even = true; public double getMedian() { if (even) { return (leftHeap.peek() + rightHeap.peek()) / 2.0; } else { return leftHeap.peek(); } } public void addNum(int num) { if (even) { rightHeap.offer(num); int rightMin = rightHeap.poll(); leftHeap.offer(rightMin); } else { leftHeap.offer(num); int leftMax = leftHeap.poll(); rightHeap.offer(leftMax); } System.out.println(leftHeap); System.out.println(rightHeap); // 奇偶变换. even = !even; }}压榨内存但是这样做的问题就是可能内存会爆掉。如果你的流无限大,那么意味着这些数据都要存在内存中,堆必须要能够建无限大。如果内存必须很小的方式,用时间换空间。流是可以重复去读的, 用时间换空间;可以用分治的思想,先读一遍流,把流中的数据个数分桶;分桶之后遍历桶就可以得到中位数落在哪个桶里面,这样就把问题的范围缩小了。public class Median { private static int BUCKET_SIZE = 1000; private int left = 0; private int right = Integer.MAX_VALUE; // 流这里用int[] 代替 public double findMedian(int[] nums) { // 第一遍读取stream 将问题复杂度转化为内存可接受的量级. int[] bucket = new int[BUCKET_SIZE]; int step = (right - left) / BUCKET_SIZE; boolean even = true; int sumCount = 0; for (int i = 0; i < nums.length; i++) { int index = nums[i] / step; bucket[index] = bucket[index] + 1; sumCount++; even = !even; } // 如果是偶数,那么就需要计算第topK 个数 // 如果是奇数, 那么需要计算第 topK和topK+1的个数. int topK = even ? sumCount / 2 : sumCount / 2 + 1; int index = 0; int indexBucketCount = 0; for (index = 0; index < bucket.length; index++) { indexBucketCount = bucket[index]; if (indexBucketCount >= topK) { // 当前bucket 就是中位数的bucket. break; } topK -= indexBucketCount; } // 划分到这里其实转化为一个topK的问题, 再读一遍流. if (even && indexBucketCount == topK) { left = index * step; right = (index + 2) * step; return helperEven(nums, topK); // 偶数的时候, 恰好划分到在左右两个子段中. // 左右两段中 [topIndex-K + (topIndex-K + 1)] / 2. } else if (even) { left = index * step; right = (index + 1) * step; return helperEven(nums, topK); // 左边 [topIndex-K + (topIndex-K + 1)] / 2 } else { left = index * step; right = (index + 1) * step; return helperOdd(nums, topK); // 奇数, 左边topIndex-K } }}这里边界条件我们处理好之后,关键还是helperOdd 和 helperEven这两个函数怎么去求topK的问题. 我们还是转化为一个topK的问题,那么求top-K和top(K+1)的问题到这里我们是不是可以用堆来解决了呢? 答案是不能,考虑极端情况。中位数的重复次数非常多eg:[100,100,100,100,100…] (1000亿个100)你的划分恰好落到这个桶里面,内存同样会爆掉。再用时间换空间假如我们的划分bucket大小是10000,那么最大的时候区间就是20000。(对应上面的偶数且落到两个分桶的情况)那么既然划分到某一个bucket了,我们直接用数数字的方式来求topK 就可以了,用堆的方式也可以,更高效一点,其实这里问题规模已经划分到很小了,两种方法都可以。我们选用TreeMap这种数据结构计数。然后分奇数偶数去求解。 private double helperEven(int[] nums, int topK) { TreeMap<Integer, Integer> map = new TreeMap<>(); for (int i = 0; i < nums.length; i++) { if (nums[i] >= left && nums[i] <= right) { if (!map.containsKey(nums[i])) { map.put(nums[i], 1); } else { map.put(nums[i], map.get(nums[i]) + 1); } } } int count = 0; int kNum = Integer.MIN_VALUE; int kNextNum = 0; for (Map.Entry<Integer, Integer> entry : map.entrySet()) { int currentCountIndex = entry.getValue(); if (kNum != Integer.MIN_VALUE) { kNextNum = entry.getKey(); break; } if (count + currentCountIndex == topK) { kNum = entry.getKey(); } else if (count + currentCountIndex > topK) { kNum = entry.getKey(); kNextNum = entry.getKey(); break; } else { count += currentCountIndex; } } return (kNum + kNextNum) / 2.0; } private double helperOdd(int[] nums, int topK) { TreeMap<Integer, Integer> map = new TreeMap<>(); for (int i = 0; i < nums.length; i++) { if (nums[i] >= left && nums[i] <= right) { if (!map.containsKey(nums[i])) { map.put(nums[i], 1); } else { map.put(nums[i], map.get(nums[i]) + 1); } } } int count = 0; int kNum = Integer.MIN_VALUE; for (Map.Entry<Integer, Integer> entry : map.entrySet()) { int currentCountIndex = entry.getValue(); if (currentCountIndex + count >= topK) { kNum = entry.getKey(); break; } else { count += currentCountIndex; } } return kNum; }至此,我觉得算是一个比较好的解决方案,leetcode社区没有相关的解答和探索,欢迎大家交流。热门阅读技术文章汇总【Leetcode】101. 对称二叉树【Leetcode】100. 相同的树【Leetcode】98. 验证二叉搜索树 ...

April 9, 2019 · 8 min · jiezi

数据结构(二)数组

数组就是把数据码成一排进行存放:数组的最大优点:快速查询。scores[2]我们基于Java的静态数组,封装一个属于自己的动态数组类Array,加深对于数组这种数据结构的理解。我们基于Java静态数组data来封装我们的动态数组Array类,capacity表示数组容量,可以通过data.length获得。size表示数组元素个数,初始为0(也可以看成是下一个新增元素的索引位置)。据此,设计Array类结构。初始数组类结构public class Array<E> { private E[] data; private int size; // 构造函数,传入数组的容量captacity构造Array public Array(int capacity) { data = (E[])new Object[capacity]; size = 0; } // 无参数的构造函数,默认数组的容量capacity=10 public Array() { this(10); } // 获取数组中的元素个数 public int getSize() { return size; } // 获取数组的容量 public int getCapacity() { return data.length; } // 返回数组是否为空 public boolean isEmpty() { return size == 0; } }向数组末尾添加元素添加元素前:添加元素后:分析得出,如果要向数组添加元素e,只要在size所在索引位置设置元素e,然后size向后移动(size++)即可。据此,增加添加元素相关的代码:// 在所有元素后添加一个新元素public void addLast(E e) { if(size == data.length) { // 数组容量已填满,则不能再添加 throw new IllegalArgumentException(“AddLast failed. Array is full.”); } data[size] = e; size++;}向指定位置添加元素添加前:添加后:分析得出,只要把要插入元素的索引位置的当前元素以及其后的所有元素,都往后挪动一位,然后在指定索引位置设置新元素即可,最后size++。为避免元素覆盖,具体挪的时候,需要从后往前推进完成元素挪动的整个过程。修改代码:// 在第index个位置插入一个新元素epublic void add(int index, E e) { if(size == data.length) { throw new IllegalArgumentException(“Add failed. Array is full.”); } if(index < 0 || index > size) { throw new IllegalArgumentException(“Add failed. Require index >= 0 and index <= size.”); } for(int i = size - 1; i >= index; i–) { data[i + 1] = data[i]; } data[index] = e; size++;}调整addLast,复用add方法,同时增加一个addFirst:// 在所有元素后添加一个新元素public void addLast(E e) { add(size, e);}// 在所有元素前添加一个新元素public void addFirst(E e) { add(0, e);}获取元素和修改元素// 获取index索引位置的元素public E get(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException(“Get failed. Index is illegal.”); } return data[index];}// 修改index索引位置的元素public void set(int index, E e) { if (index < 0 || index >= size) { throw new IllegalArgumentException(“Set failed. Index is illegal.”); } data[index] = e;}包含、搜索// 查找数组中是否有元素epublic boolean contains(E e) { for (int i = 0; i < size; i++) { if (data[i].equals(e)) { return true; } } return false;}// 查找数组中元素e所在的索引,如果不存在元素e,则返回-1public int find(E e) { for (int i = 0; i < size; i++) { if (data[i].equals(e)) { return i; } } return -1;}从数组中删除元素删除前:删除后:分析得出,只要将要删除位置之后的元素都往前挪动一位即可。然后size减1。修改代码:// 从数组中删除index位置的元素,返回删除的元素public E remove(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException(“Remove failed. Index is illegal.”); } E ret = data[index]; for (int i = index + 1; i < size; i++) { data[i-1] = data[i]; } size–; return ret;}// 从数组中删除第一个元素,返回删除的元素public E removeFirst() { return remove(0);}// 从数组中删除最后一个元素,返回删除的元素public E removeLast() { return remove(size - 1);}// 从数组中删除元素e(只删除一个)public boolean removeElement(E e) { int index = find(e); if (index != -1) { remove(index); return true; } return false;}动态数组容量开太大,浪费空间,容量开小了,空间不够用。所以需要实现动态数组。具体做法如下:就是在方法中开辟一个更大容量的数组,循环遍历原来的数组元素,设置到新的数组上。然后将原数组的data指向新数组。修改代码:// 数组容量扩容/缩容public void resize(int newCapacity) { E[] newData = (E[])new Object[newCapacity]; for (int i = 0; i < size; i++) { newData[i] = data[i]; } data = newData;}修改添加元素的代码,添加时自动扩容:// 在第index个位置插入一个新元素epublic void add(int index, E e) { if (index < 0 || index > size) { throw new IllegalArgumentException(“AddLast failed. Require index >= 0 and index <= size”); } if (size == data.length) { resize(2 * data.length); // 扩容为原来的2倍 } for (int i = size - 1; i >= index; i–) { data[i + 1] = data[i]; } data[index] = e; size++;}修改删除元素的代码,必要时自动缩容:// 从数组中删除index位置的元素,返回删除的元素public E remove(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException(“Remove failed. Index is illegal.”); } E ret = data[index]; for (int i = index + 1; i < size; i++) { data[i - 1] = data[i]; } size–; data[size] = null; // loitering objects != memory leak if (size == data.length / 4 && data.length / 2 != 0) { // 缩减数组使用lazy方式(避免复杂度震荡),在1/4的时候缩容 resize(data.length / 2); // 缩容为原来的一半 } return ret;}测试数组@Overridepublic String toString() { StringBuilder res = new StringBuilder(); res.append(String.format(“Array: size = %d, capacity = %d\n”, size, data.length)); res.append("["); for (int i = 0; i < size; i++) { res.append(data[i]); if (i != size - 1) { res.append(", “); } } res.append(”]"); return res.toString();}public static void main(String[] args) { Array<Integer> arr = new Array<>(); for (int i = 0; i < 10; i++) { arr.addLast(i); } System.out.println(arr); arr.add(1, 100); System.out.println(arr); arr.addFirst(-1); System.out.println(arr); arr.remove(2); System.out.println(arr); arr.removeElement(4); System.out.println(arr); arr.removeFirst(); System.out.println(arr);}console输出:Array: size = 10, capacity = 10[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]Array: size = 11, capacity = 20[0, 100, 1, 2, 3, 4, 5, 6, 7, 8, 9]Array: size = 12, capacity = 20[-1, 0, 100, 1, 2, 3, 4, 5, 6, 7, 8, 9]Array: size = 11, capacity = 20[-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]Array: size = 10, capacity = 20[-1, 0, 1, 2, 3, 5, 6, 7, 8, 9]Array: size = 9, capacity = 20[0, 1, 2, 3, 5, 6, 7, 8, 9]完整代码public class Array<E> { private E[] data; private int size; // 构造函数,传入数组的容量captacity构造Array public Array(int capacity) { data = (E[])new Object[capacity]; size = 0; } // 无参数的构造函数,默认数组的容量capacity=10 public Array() { this(10); } // 获取数组中的元素个数 public int getSize() { return size; } // 获取数组的容量 public int getCapacity() { return data.length; } // 返回数组是否为空 public boolean isEmpty() { return size == 0; } // 在第index个位置插入一个新元素e public void add(int index, E e) { if (index < 0 || index > size) { throw new IllegalArgumentException(“Add failed. Require index >= 0 and index <= size”); } if (size == data.length) { resize(2 * data.length); // 扩容为原来的2倍 } for (int i = size - 1; i >= index; i–) { data[i + 1] = data[i]; } data[index] = e; size++; } // 在所有元素后添加一个新元素 public void addLast(E e) { add(size, e); } // 在所有元素前添加一个新元素 public void addFirst(E e) { add(0, e); } // 获取index索引位置的元素 public E get(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException(“Get failed. Index is illegal.”); } return data[index]; } // 修改index索引位置的元素 public void set(int index, E e) { if (index < 0 || index >= size) { throw new IllegalArgumentException(“Set failed. Index is illegal.”); } data[index] = e; } // 查找数组中是否有元素e public boolean contains(E e) { for (int i = 0; i < size; i++) { if (data[i].equals(e)) { return true; } } return false; } // 查找数组中元素e所在的索引,如果不存在元素e,则返回-1 public int find(E e) { for (int i = 0; i < size; i++) { if (data[i].equals(e)) { return i; } } return -1; } // 从数组中删除index位置的元素,返回删除的元素 public E remove(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException(“Remove failed. Index is illegal.”); } E ret = data[index]; for (int i = index + 1; i < size; i++) { data[i - 1] = data[i]; } size–; data[size] = null; // loitering objects != memory leak if (size == data.length / 4 && data.length / 2 != 0) { // 缩减数组使用lazy方式(避免复杂度震荡),在1/4的时候才缩容 resize(data.length / 2); // 缩容为原来的一半 } return ret; } // 从数组中删除第一个元素,返回删除的元素 public E removeFirst() { return remove(0); } // 从数组中删除最后一个元素,返回删除的元素 public E removeLast() { return remove(size - 1); } // 从数组中删除元素e(只删除一个) public boolean removeElement(E e) { int index = find(e); if (index != -1) { remove(index); return true; } return false; } public void resize(int newCapacity) { E[] newData = (E[])new Object[newCapacity]; for (int i = 0; i < size; i++) { newData[i] = data[i]; } data = newData; } @Override public String toString() { StringBuilder res = new StringBuilder(); res.append(String.format(“Array: size = %d, capacity = %d\n”, size, data.length)); res.append("["); for (int i = 0; i < size; i++) { res.append(data[i]); if (i != size - 1) { res.append(", “); } } res.append(”]"); return res.toString(); } public static void main(String[] args) { Array<Integer> arr = new Array<>(); for (int i = 0; i < 10; i++) { arr.addLast(i); } System.out.println(arr); arr.add(1, 100); System.out.println(arr); arr.addFirst(-1); System.out.println(arr); arr.remove(2); System.out.println(arr); arr.removeElement(4); System.out.println(arr); arr.removeFirst(); System.out.println(arr); }} ...

April 1, 2019 · 6 min · jiezi

Java版-数据结构-链表

概要之前我们分别学习了解了动态数组、栈、队列,其实他们的底层都是依托静态数组来实现的、只是通过我们定义的resize方法来动态扩容解决固定容量的问题,那么我们即将学习的链表,它其实是一种真正的动态数据结构。介绍链表是一种最简单的动态数据结构,它能够辅助组成其它的数据结构,链表中的元素可存储在内存中的任何地方(不需要连续的内存,这一点和我们的数组具有很大的区别,数组需要连续的内存),链表中的每个元素都存储了下一个元素的地址,从而使一系列随机的内存地址串接在一起。存储链表的数据的我们一般称为节点(Node),节点一般分为两部分,一部分存储我们真正的数据,而另外一部分存储的是下一个节点的引用地址。class Node{ private E e; // 存储的真正元素 private Node next; // 存储下一个node的引用地址(指向下一个node)}比如现在我们将元素A、B、C三个节点添加到链表中,示意图如下:从图中节点A到节点B之间的箭头代表,节点A指向了节点B(NodeA.next = NodeB),因为在实际业务中我们的链表长度不可能是无穷无尽的,基本上都是有限个节点,通常定义链表中的最后一个元素它的next存储的是NULL(空),换句话说,如果在链表中,一个节点的next是空(NULL)的话,那么它一定是最后一个节点(对应我们图中的C节点)。根据我们上面介绍的链表基本结构,下面我们用代码定义一下链表的基本骨架(这里我们引入了一个虚拟的头结点,下面我们会作说明)public class LinkedList<E> { /** * 虚拟的头结点 / private Node dummyHead; /* * 链表中节点的个数 / private int size; public LinkedList() { // 创建一个虚拟的头结点 dummyHead = new Node(); } // 节点定义 private class Node { // 存储节点的元素 public E e; // 存储下一个节点的地址 public Node next; public Node() { } public Node(E e) { this.e = e; } public Node(E e, Node next) { this.e = e; this.next = next; } @Override public String toString() { return “Node{” + “e=” + e + “, next=” + next + ‘}’; } }}后面我们对链表的添加节点、删除节点以及查询节点,代码实现都会基于这个基本骨架向链表中添加节点思路分析:一般我们向链表中添加节点,基本思路:找到添加节点位置的前一个节点preNode,然后再改变链表的地址引用;由于链表的第一个节点也就是头结点没有前节点,此时我们为了操作方便,为链表新增了不存储任何元素的一个虚拟的头结点dummyHead(不是必须的,对用户来讲是不可见的),其实链表中真正的第一个节点是节点A(dummyHead.next),这样我们就能保证了,链表中存储元素的节点都有前一个节点。下面我们来看一下,如果现在有一个节点D,我们准备把它插入到节点B的位置,我们需要做哪些操作第一步:我们首先要找到节点B的前一个节点,也就是节点A第二步:将新节点D的指向指到节点B(NodeD.next = NodeA.next),然后再将节点A的指向,指到节点D(NodeA.next = NodeD),这样我们的节点就能串接起来了代码实现:/* * 向链表中指定位置插入节点(学习使用,真正插入不会指定索引) * * @param index 索引的位置 * @param e 节点元素 /public void add(int index, E e) { if (index < 0 || index > size) { throw new IllegalArgumentException(“不是有效的索引”); } Node prev = dummyHead; // 找到index位置的前一个节点 for (int i = 0; i < index; i++) { prev = prev.next; } // 新建一个节点,进行挂接 Node node = new Node(e); node.next = prev.next; prev.next = node; size++;}链表的遍历进行链表遍历,我们需要从链表中真正的第一个元素开始,也就是dummyHead.next/* * 获取链表中index位置的元素 * * @param index 索引的位置 * @return 节点的元素 /public E get(int index) { if (index < 0 || index > size) { throw new IllegalArgumentException(“不是有效的索引”); } Node cur = dummyHead.next; for (int i = 0; i < index; i++) { cur = cur.next; } return cur.e;}修改链表中元素/* * 修改链表中index位置节点的元素 * * @param index 索引的位置 * @param e 节点的元素 /public void set(int index, E e) { if (index < 0 || index > size) { throw new IllegalArgumentException(“不是有效的索引”); } Node cur = dummyHead.next; for (int i = 0; i < index; i++) { cur = cur.next; } cur.e = e;}查找链表中是否包含某元素/* * 查找链表中是否包含元素e * * @param e * @return /public boolean contains(E e) { Node cur = dummyHead.next; while (cur != null) { if (cur.e.equals(e)) { return true; } cur = cur.next; } return false;}删除链表中的元素在链表中删除元素,与在链表中添加元素有点类似第一步:我们首先找到删除节点位置的前一个节点,我们用prev表示,被删除的节点我们用delNode表示第二步:改变链表的引用地址:prev.next = delNode.next(等同于,将节点在链表中删除)/* * 删除链表中index位置的节点 * * @param index */public void remove(int index) { if (index < 0 || index > size) { throw new IllegalArgumentException(“不是有效的索引”); } Node prev = dummyHead; for (int i = 0; i < index; i++) { prev = prev.next; } Node delNode = prev.next; prev.next = delNode.next; delNode.next = null; size–;}完整版代码GitHub仓库地址:Java版数据结构-链表 欢迎大家【关注】和 【Star】至此笔者已经为大家带来了数据结构:静态数组、动态数组、栈、数组队列、循环队列、链表;接下来,笔者还会一一的实现其它常见的数组结构,大家一起加油!静态数组动态数组栈数组队列循环队列链表循环链表二分搜索树优先队列堆线段树字典树AVL红黑树哈希表….持续更新中,欢迎大家关注公众号 小白程序之路(whiteontheroad),第一时间获取最新信息!!!笔者博客地址:http:www.gulj.cn ...

March 30, 2019 · 2 min · jiezi

数据结构之堆

定义堆是一种特别的树状结构,我们首先来看看维基百科的上定义。堆(英语:Heap)是计算机科学中的一种特别的树状数据结构。若是满足以下特性,即可称为堆:“给定堆中任意节点 P 和 C,若 P 是 C 的母节点,那么 P 的值会小于等于(或大于等于) C 的值”。若母节点的值恒小于等于子节点的值,此堆称为最小堆(min heap);反之,若母节点的值恒大于等于子节点的值,此堆称为最大堆(max heap)。在堆中最顶端的那一个节点,称作根节点(root node),根节点本身没有母节点(parent node)。总结来说,堆是一个完全二叉树,最多只有两个子节点,并且必须保证根节点是最大的值或者最小的值,所以对于一个堆而言,根节点是最大的值或者是最小的值。存储因为堆是一棵完全二叉树,所以使用数组可以高效的存储数据。对于使用数组存储的方式,有两个性质非常关键,对于一个非根节点的节点i来说,如果它的下标为k,那么它的父节点的下标为 (k -1)/2,子节点的下标为2 *k +1和2 *k + 2基本操作堆化插入删除堆化对于任意一个无序数组而言,实现堆化的步骤如下,我们以构建一个最大堆为例:找到第一个非叶子节点,对这个节点的左子树和右子树与节点比较,将大的元素放置到父节点的位置,直到父节点已经是最大值或者改节点已经是叶子节点。依次对所有的非叶子节点进行第一步的操作private Heap(int[] data){ int last_p = data.length - 1; //i是第一个非叶子节点 for (int i = (last_p - 1) / 2; i >= 0; i–) { heapify(data, i, last_p); } this.data = data;}private void heapify(int[] data, int start, int end){ int value = data[start]; int current_p = start; //左孩子 int left_p = 2 * current_p + 1; while (left_p <= end) { //右节点大于左节点 if (left_p < end && data[left_p] < data[left_p + 1]) { //移动位置到右节点 left_p++; } //当前的父节点已经是最大值 if (data[left_p] < value) { break; } else { //子节点上移到父节点的位置 data[current_p] = data[left_p]; current_p = left_p; left_p = current_p * 2 + 1; } } data[current_p] = value;}插入插入的算法如下:.将新增的节点放在数组的最末端,也就是数组的最后一个位置。然后计算出父节点的位置,让当前节点与父节点比较,如果父节点比较小,交换位置。重复上述步骤,直到父节点大于当前节点或者当前节点是根节点。public int insert(int value){ checkSize(); int position = data.length - 1; data[position] = value; int current_p = position; int parent_p = (position - 1) / 2; while (current_p > 0) { if (data[parent_p] > value) { break; } else { data[current_p] = data[parent_p]; current_p = parent_p; parent_p = (current_p - 1) / 2; } } data[current_p] = value; return position;}删除删除的算法如下:将数组最后一个节点与当前需要删除的节点替换,删除最后一个节点,对于替换的节点来说,相当于进行了依次插入操作,不过这次是从上往下的插入。算法与remove相同,也是比较最大的值进行替换,直到不满足条件为止。删除根节点public int remove(){ int root = data[0]; int last = data[data.length - 1]; data[0] = last; data = Arrays.copyOf(data, data.length - 1); if (data.length == 0) { return root; } //从顶点开始调整 int current_p = 0; int l = current_p * 2 + 1; int value = data[current_p]; while (l < data.length) { if (l < data.length - 1 && data[l] < data[l + 1]) { l++; //右孩子大 } if (data[l] <= value) { break; } else { data[current_p] = data[l]; current_p = l; l = current_p * 2 + 1; } } data[current_p] = value; return root;}应用堆应用比较多的一个用处就是堆排序,对于一个数组进行堆化之后,第一个数组是最大的值,然后交换第一个数和最后一个数,这样最大的数就落在了最后一个数组的位置。缩小数组,重复之前的步骤,最后就得到了一个排序好的数组。int[] data = new int[] {20, 40, 80, 33, 111, 47, 21, 90, -1};HeapSort hs = new HeapSort();hs.heap_array(data, data.length - 1);for (int i = 0; i < data.length - 1; i++){ int tmp = data[0]; data[0] = data[data.length - 1 - i]; data[data.length - 1 - i] = tmp; hs.heap_array(data, data.length - 2 - i);}System.out.println(Arrays.toString(data));代码地址: https://gitee.com/devnew/Algo… ...

March 28, 2019 · 2 min · jiezi

这破旧的脑子——二叉树

为什么会写这篇文章学习的时间越来越长总会忘掉一些东西,就比如向量,矩阵,二叉树,邻接表,太多太多东西,不用就都给忘了,今天看了这样一道面试题:总结下来就是根据二叉树的前中序遍历,然后写出后序遍历,清晰的记得当时学习二叉树的时候做这种题是很快的,可是我还真就卡住了,不是说需要做一会儿,是做不出来,看过好多遍使用程序实现DFS(深度优先)BFS(广度优先)的例子,可是让我用笔推断,我还真就脑子瓦特了,所以也记录一下,顺便帮一下也忘记了手工推断的你们回忆一下,你们一定都比我优秀,perfect。题目:前序遍历A D C E F G H B中序遍历C D F E G H A B后序遍历?这些遍历就是根据遍历根节点的顺序而定义的,前序遍历就是优先遍历根节点然后遍历左右子节点,当然左右子节点也是根据这个原则遍历的,那么中后序遍历也一样。那么我们应该怎么去做呢?其实就是根据前中遍历的结果推断出这颗树。。。第一步根据前序遍历原则找出根节点:A 因为优先遍历根节点根据根节点A和中序遍历划分前中序遍历的左右子树,以中左表示,前序遍历的左右子树,以前左表示:中左:C D F E G H中右:B前左:D C E F G H前右:B第二步根据上面的中左,前左继续划分根节点:D 由于右子树就一个节点,所以就结束了根据根节点D和中序遍历划分前中序遍历的左右子树中左:C中右:F E G H前左:C前右:E F G H第三步根据上面的中右,前右继续划分根节点:E 由于左子树就一个节点,所以就结束了根据根节点E和中序遍历划分前中序遍历的左右子树中左:F中右:G H前左:F前右:G H第四步根据上面的中右,前右继续划分根节点:G 由于左子树就一个节点,所以就结束了根据根节点G和中序遍历划分前中序遍历的左右子树中左:中右:H前左:前右:H终于构建出来这颗树了,接下来根据后序遍历原则去写:后序遍历结果亮相:C F H G E D B A只有多学习才能变得更强,还是那句话:坚持一件事,对自己。

March 28, 2019 · 1 min · jiezi

代码面试需要知道的8种数据结构(附面试题及答案链接)

译者按: 搞定面试,不要急着刷题,先弄懂什么是数据结构!原文:The top data structures you should know for your next coding interview译者:Fundebug为了保证可读性,本文采用意译而非直译。另外,本文版权归原作者所有,翻译仅用于学习。1976 年,一个瑞士计算机科学家写一本书《Algorithms + Data Structures = Programs》。即:算法 + 数据结构 = 程序。40 多年过去了,这个等式依然成立。很多代码面试题都要求候选者深入理解数据结构,不管你来自大学计算机专业还是编程培训机构,也不管你有多少年编程经验。有时面试题会直接提到数据结构,比如“给我实现一个二叉树”,然而有时则不那么明显,比如“统计一下每个作者写的书的数量”。什么是数据结构?数据结构是计算机存储、组织数据的方式。对于特定的数据结构(比如数组),有些操作效率很高(读某个数组元素),有些操作的效率很低(删除某个数组元素)。程序员的目标是为当前的问题选择最优的数据结构。为什么我们需要数据结构?数据是程序的核心要素,因此数据结构的价值不言而喻。无论你在写什么程序,你都需要与数据打交道,比如员工工资、股票价格、杂货清单或者电话本。在不同场景下,数据需要以特定的方式存储,我们有不同的数据结构可以满足我们的需求。8 种常用数据结构数组栈队列链表图树前缀树哈希表1. 数组数组(Array)大概是最简单,也是最常用的数据结构了。其他数据结构,比如栈和队列都是由数组衍生出来的。下图展示了 1 个数组,它有 4 个元素:每一个数组元素的位置由数字编号,称为下标或者索引(index)。大多数编程语言的数组第一个元素的下标是 0。根据维度区分,有 2 种不同的数组:一维数组(如上图所示)多维数组(数组的元素为数组)数组的基本操作Insert - 在某个索引处插入元素Get - 读取某个索引处的元素Delete - 删除某个索引处的元素Size - 获取数组的长度常见数组代码面试题查找数组中第二小的元素查找第一个没有重复的数组元素合并 2 个排序好的数组重新排列数组中的正数和负数2. 栈撤回,即 Ctrl+Z,是我们最常见的操作之一,大多数应用都会支持这个功能。你知道它是怎么实现的吗?答案是这样的:把之前的应用状态(限制个数)保存到内存中,最近的状态放到第一个。这时,我们需要栈(stack)来实现这个功能。栈中的元素采用 LIFO (Last In First Out),即后进先出。下图的栈有 3 个元素,3 在最上面,因此它会被第一个移除:栈的基本操作Push — 在栈的最上方插入元素Pop — 返回栈最上方的元素,并将其删除isEmpty — 查询栈是否为空Top — 返回栈最上方的元素,并不删除常见的栈代码面试题使用栈计算后缀表达式使用栈为栈中的元素排序检查字符串中的括号是否匹配正确3. 队列队列(Queue)与栈类似,都是采用线性结构存储数据。它们的区别在于,栈采用 LIFO 方式,而队列采用先进先出,即FIFO(First in First Out)。下图展示了一个队列,1 是最上面的元素,它会被第一个移除:队列的基本操作Enqueue — 在队列末尾插入元素Dequeue — 将队列第一个元素删除isEmpty — 查询队列是否为空Top — 返回队列的第一个元素常见的队列代码面试题使用队列实现栈倒转队列的前 K 个元素使用队列将 1 到 n 转换为二进制4. 链表链表(Linked List)也是线性结构,它与数组看起来非常像,但是它们的内存分配方式、内部结构和插入删除操作方式都不一样。链表是一系列节点组成的链,每一个节点保存了数据以及指向下一个节点的指针。链表头指针指向第一个节点,如果链表为空,则头指针为空或者为 null。链表可以用来实现文件系统、哈希表和邻接表。下图展示了一个链表,它有 3 个节点:链表分为 2 种:单向链表双向链表链表的基本操作InsertAtEnd — 在链表结尾插入元素InsertAtHead — 在链表开头插入元素Delete — 删除链表的指定元素DeleteAtHead — 删除链表第一个元素Search — 在链表中查询指定元素isEmpty — 查询链表是否为空常见的队列代码面试题倒转 1 个链表检查链表中是否存在循环返回链表倒数第 N 个元素移除链表中的重复元素5. 图图(graph)由多个节点(vertex)构成,节点之间阔以互相连接组成一个网络。(x, y)表示一条边(edge),它表示节点 x 与 y 相连。边可能会有权值(weight/cost)。图分为两种:无向图有向图在编程语言中,图有可能有以下两种形式表示:邻接矩阵(Adjacency Matrix)邻接表(Adjacency List)遍历图有两周算法广度优先搜索(Breadth First Search)深度优先搜索(Depth First Search)常见的图代码面试题实现广度优先搜索实现深度优先搜索检查图是否为树统计图中边的个数使用 Dijkstra 算法查找两个节点之间的最短距离6. 树树(Tree)是一个分层的数据结构,由节点和连接节点的边组成。树是一种特殊的图,它与图最大的区别是没有循环。树被广泛应用在人工智能和一些复杂算法中,用来提供高效的存储结构。下图是一个简单的树以及与树相关的术语:树有很多分类:N 叉树(N-ary Tree)平衡树(Balanced Tree)二叉树(Binary Tree)二叉查找树(Binary Search Tree)平衡二叉树(AVL Tree)红黑树(Red Black Tree)2-3 树(2–3 Tree)其中,二叉树和二叉查找树是最常用的树。常见的树代码面试题计算树的高度查找二叉平衡树中第 K 大的元素查找树中与根节点距离为 k 的节点查找二叉树中某个节点所有祖先节点7. 前缀树前缀树(Prefix Trees 或者 Trie)与树类似,用于处理字符串相关的问题时非常高效。它可以实现快速检索,常用于字典中的单词查询,搜索引擎的自动补全甚至 IP 路由。下图展示了“top”, “thus”和“their”三个单词在前缀树中如何存储的:单词是按照字母从上往下存储,“p”, “s”和“r”节点分别表示“top”, “thus”和“their”的单词结尾。常见的树代码面试题统计前缀树表示的单词个数使用前缀树为字符串数组排序8. 哈希表哈希(Hash)将某个对象变换为唯一标识符,该标识符通常用一个短的随机字母和数字组成的字符串来代表。哈希可以用来实现各种数据结构,其中最常用的就是哈希表(hash table)。哈希表通常由数组实现。哈希表的性能取决于 3 个指标:哈希函数哈希表的大小哈希冲突处理方式下图展示了有数组实现的哈希表,数组的下标即为哈希值,由哈希函数计算,作为哈希表的键(key),而数组中保存的数据即为值(value):常见的哈希表代码面试题查找数组中对称的组合确认某个数组的元素是否为另一个数组元素的子集确认给定的数组是否互斥参考Fundebug 博客 - Node.js 面试题之 2017Fundebug 博客 - 快速掌握 JavaScript 面试基础知识(一)Fundebug 博客 - 快速掌握 JavaScript 面试基础知识(二)Fundebug 博客 - 快速掌握 JavaScript 面试基础知识(三)GeeksforGeeks关于FundebugFundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了10亿+错误事件,付费客户有Google、360、金山软件、百姓网等众多品牌企业。欢迎大家免费试用!版权声明转载时请注明作者Fundebug以及本文地址:https://blog.fundebug.com/2018/08/27/code-interview-data-structure/ ...

March 27, 2019 · 1 min · jiezi

数据结构

什么是数据结构1、数据数据是描述客观世界的数字、字符以及一切能够输入到计算机中,并且能够被计算机程序处理的符号集合。简言之,数据就是计算机加工处理的原料,是信息的载体。2、数据元素数据元素是能够独立、完整地描述问题世界中的实体的最小单位,它是数据这个集合中的一个一个的元素,数据元素也成为数据结点,或者简称结点3、数据对象一个数据对象被定义为具有相同性质的数据元素的集合。4、结构数据元素之间必然存在着某种联系,这种联系称为结构。人们不仅要考虑到数据的这种结构,而且还要考虑到将要施加于数据上的各种操作及其种类。从这个意义上来说,数据元素是具有结构的数据元素的集合。这里也可以给数据结构一个形式化的描述。数据结构是一个二元组Data-Structure=(D,R),其中,D是数据元素的有限集合,R是D上关系的集合。上述定义中的"关系"通常是指数据元素之间的逻辑关系,也称为数据的逻辑结构。通常把数据结构在计算机中的表示称为数据的物理结构。物理结构又称存储结构,包括数据元素的表示以及关系的表示两个方面。数据的逻辑结构与存储结构是密不可分的两个方面,一个算法的设计取决于选定的逻辑结构,而算法的实现则依赖于采用的存储结构。数据结构是相互之间存在一种或者多种特定关系的数据元素的集合。数据结构包括三方面的内容:逻辑结构,存储结构和数据的运算。数据的逻辑结构和存储结构是密不可分的两个方面,一个算法的设计取决于所选定的逻辑结构,而算法的实现取决于所选定的存储结构。数据的存储结构主要有:顺序存储结构、链式存储结构、索引存储结构和散列存储结构。1、顺序存储结构:顺序存储结构的优点是简单,易理解,并且实际占用最少的存储空间;缺点是需要占用一片地址连续的整块空间,并且存储分配要实现进行;另外,对于一些操作的时间效率较低也是这种存储结构的主要缺陷之一。2、链式存储结构:链式存储结构是指在计算机存储器中用一片地址任意的(连续的或者不连续的)存放数据元素的信息,一般称每个数据元素占用的若干存储单元的组合作为一个链结点。每个链结点中不仅要存放一个数据元素的元素信息,还要存放一个指出这个元素在逻辑关系中的直接后继元素所在链结点的地址,该地址被称为指针。这就是说,数据元素之间的逻辑关系通过指针间接地反映。由于不要求存储空间地址连续,因此,逻辑上相邻的数据元素在物理上不一定相邻。这种存储结构的优点是存储空间不必事先分配,在需要存储空间时可以临时申请,不会造成存储空间的浪费。像插入和删除这样操作的时间效率采用链式存储结构远比采用顺序存储结构要高。但是在这种情况中,不仅数据元素本身的数据信息需要占用存储空间,而且指针也有存储空间的开销,因此,从这一点来说,链式存储结构要比顺序存储结构的空间开销大。3、索引结构是利用数据元素的索引关系来确定数据元素的存放位置的一种存储结构,它是由数据元素本身的数据信息以及索引表两个部分组成的。4、散列结构是由事先构造的散列函数关系及处理冲突的方法来确定数据元素在散列表中的位置。1、数据的逻辑结构:逻辑结构是指数据元素之间的逻辑关系,即在逻辑上描述数据,它与数据的存储无关,是独立于计算机的,数据的逻辑结构分为线性结构和非线性结构,线性表是典型的线性结构;集合,树,图是典型的非线性结构。2、数据的存储结构:存储结构是指数据结构在计算机中的表示(又称物理结构)。它包括数据元素的表示和关系的表示。数据的存储结构是逻辑结构用计算机语言的实现。数据的存储结构是逻辑结构用计算机语言的实现,它依赖于计算机语言。数据的存储结构主要有顺序存储结构、链式存储结构、索引存储结构和散列存储结构。3、数据的运算:施加在数据上的运算包括数据的定义和实现。因此,数据结构课程所要研究的主要内容可以简要地归纳为以下3个方面:1、研究数据元素之间固有的客观联系(逻辑结构)2、研究数据在计算机内部的存储方法(存储结构)3、研究数据在数据的各种结构(逻辑的和物理的)的基础上如何对数据实施有效的操作或处理(算法)为此,应该说数据结构是一门抽象的,研究数据之间结构关系的学科。

March 25, 2019 · 1 min · jiezi

leetcode 链表相关题目解析

前言本文基于leetcode的探索链表卡片所编写。遗憾的是, 我的卡片的进度为80%, 依然没有全部完成。我在探索卡片的时候, 免不了谷歌搜索。并且有一些题目, 我的解答可能不是最优解。敬请见谅。关于链表链表属于比较简单的数据结构, 在这里我在过多的赘述。值的注意的是, 本文都是基于单链表的, 双链表的设计我还没有实现。常见的套路关于链表的算法题目, 我自己总结了以下几种套路, 或者说常见的手段同时保有当前节点的指针, 以及当前节点的前一个节点的指针。快慢指针, fast指针的移动速度是slow指针的两倍, 如果链表成环那么fast和slow必然会相遇。虚假的链表头, 通过 new ListNode(0), 创建一个虚假的头部。获取真正链表只需返回head.next(这在需要生成一个新链表的时候很有用)。同时保有当前链表的尾部的指针, 以及头部的节点指针。善用while循环。链表的头部和尾部是链表比较特殊的节点, 需要注意区别对待设计单链表原题的地址, 我在原题的基础使用了TypeScript模拟实现了链表。链表需要拥有以下几种方法:get, 根据链表的索引获取链表节点的valueaddAtTail, 添加一个节点到链表的尾部addAtHead, 添加一个节点到链表的头部addAtIndex, 添加一个节点到链表的任意位置deleteAtIndex, 删除任意位置的节点// 定义链表节点类以及链表类的接口interface LinkedListNodeInterface { val: number; next: LinkedListNode;}interface LinkedListInterface { head: LinkedListNode; length: number; get(index: number): number; addAtHead(val: number): void; addAtTail(val: number): void; addAtIndex(index: number, val: number): void; deleteAtIndex(index: number): void}class LinkedListNode implements LinkedListNodeInterface { constructor ( public val: number = null, public next: LinkedListNode = null ) {}}class LinkedList implements LinkedListInterface { constructor ( public head: LinkedListNode = null, public length: number = 0 ) {} /** * 通过while循环链表, 同时在循环的过程中使用计数器计数, 既可以实现 / public get(index: number): number { if (index >= 0 && index < this.length) { let num: number = 0 let currentNode: LinkedListNode = this.head while (index !== num) { num += 1 currentNode = currentNode.next } return currentNode.val } return -1 } /* * 将新节点的next属性指向当前的head, 将head指针指向新节点 / public addAtHead (val: number): void { let newNode: LinkedListNode = new LinkedListNode(val, this.head) this.head = newNode this.length += 1 } /* * 将链表尾部的节点的next属性指向新生成的节点, 获取链表尾部的节点需要遍历链表 / public addAtTail(val: number): void { let newNode: LinkedListNode = new LinkedListNode(val, null) let currentNode: LinkedListNode = this.head if (!this.head) { this.head = newNode } else { while (currentNode && currentNode.next) { currentNode = currentNode.next } currentNode.next = newNode } this.length += 1 } /* * 这里需要需要运用技巧, 遍历链表的同时, 同时保留当前的节点和当前节点的前一个节点的指针 / public addAtIndex(index: number, val: number): void { if (index >= 0 && index <= this.length) { let newNode: LinkedListNode = null if (index === 0) { // 如果index为0, 插入头部需要与其他位置区别对待 this.addAtHead(val) } else { let pointer: number = 1 let prevNode: LinkedListNode = this.head let currentNode: LinkedListNode = this.head.next while (pointer !== index && currentNode) { prevNode = currentNode currentNode = currentNode.next pointer += 1 } // 中间插入 newNode = new LinkedListNode(val, currentNode) prevNode.next = newNode this.length += 1 } } } public deleteAtIndex(index: number): void { if (index >= 0 && index < this.length && this.length > 0) { if (index === 0) { this.head = this.head.next } else { let pointer: number = 1 let prevNode: LinkedListNode = this.head let currentNode: LinkedListNode = this.head.next // 值得注意的是这里的判断条件使用的是currentNode.next // 这意味着currentNode最远到达当前链表的尾部的节点,而非null // 这是因为prevNode.next = prevNode.next.next, 我们不能取null的next属性 while (pointer !== index && currentNode.next) { prevNode = currentNode currentNode = currentNode.next pointer += 1 } prevNode.next = prevNode.next.next } this.length -= 1 } }}环形链表原题地址, 将环形链表想象成一个跑道, 运动员的速度是肥宅的两倍, 那么经过一段时间后, 运动员必然会超过肥宅一圈。这个时候, 运动员和肥宅必然会相遇。快慢指针的思想就是源于此。/* * 判断链表是否成环 /function hasCycle (head: LinkedListNode): boolean { // 快指针 let flst = head // 慢指针 let slow = head while (flst && flst.next && flst.next.next) { flst = flst.next.next slow = flst.next if (flst === slow) { return true } } return false}环形链表II原题地址, 在环形链表的基础上, 我们需要获取环形链表的入口。同样使用快慢指针实现。但是值的注意的是。链表可能只有部分成环, 这意味着。快慢指针相遇的点, 可能并不是环的入口。慢节点的运动距离为, a + b - c快节点的运动距离为, 2b + a - c快节点的运动距离是慢节点的两倍。可以得出这个公式 2(a + b - c) = 2b + a - c, 化简 2a - 2c = a - c, 可以得出 a = c。相遇的点距离入口的距离, 等于起点距离入口距离function hasCycleEntrance (head: LinkedListNode): LinkedListNode | Boolean { // 快指针 let flst = head // 慢指针 let slow = head while (flst && flst.next && flst.next.next) { flst = flst.next.next slow = flst.next // 快指针移动到入口,并且速度变为1 if (flst === slow) { // 变道起点 flst = head // a 和 c距离是一致的 // 每一次移动一个next,必然会在入口出相遇 while (flst !== slow) { flst = flst.next slow = slow.next } return flst } } return false}相交链表原题地址, 相交链表的解题思路依然是使用快慢指针。思路见下图, 将a链的tail链接到b链head, 如果a与b链相交, 那么就会成环。套用上一题的思路就可以获取到a与b的交点。function hasCross (headA: LinkedListNode, headB: LinkedListNode): LinkedListNode { if (headA && headB) { // 自身相等的情况下 if (headA === headB) { return headA } // a链的tail连上b链的head let lastA: LinkedListNode = headA let lastB: LinkedListNode = headB while (lastA && lastA.next) { lastA = lastA.next } lastA.next = headB let fast: LinkedListNode = headA let slow: LinkedListNode = headA while (fast && fast.next && fast.next.next) { slow = slow.next fast = fast.next.next if (slow === fast) { fast = headA while (slow !== fast) { slow = slow.next fast = fast.next if (slow === fast) { lastA.next = null return slow } } lastA.next = null return fast } } lastA.next = null return null } }删除链表的倒数第N个节点原题地址, 这里我使用的是比较笨的办法, 先计算链表的长度, 获取正序的时n的大小。然后按照删除链表中某一个节点的方法进行删除即可。需要区分删除的是否是第一个。反转链表原题地址, 常见的反转链表的方式就是使用递归或者迭代的方式。反转链表的过程, 如果拆解开来, 可以分为下面几步。从拆解的过程可以看出, 反转链表的过程就是依次将head的后面的节点, 放到链表的头部。1 -> 2 -> 3 -> 4 -> null2 -> 1 -> 3 -> 4 -> null3 -> 2 -> 1 -> 4 -> null4 -> 3 -> 2 -> 1 -> nullconst reverseList = function(head: LinkedListNode): LinkedListNode { let newHead: LinkedListNode = head let current: LinkedListNode = head // current的指针将会向后移动 function reverse () { let a = current.next let b = current.next.next a.next = head current.next = b head = a } while (current && current.next) { reverse() } return head};删除链表中的节点原题地址。我使用的也是笨办法。由于链表头部特殊性,删除头部时需要进行递归(因为在第一次删除头部的节点后, 新的头部也有可能是满足删除条件的节点)。对于其他位置的节点使用常规办法即可。function removeElements (head: LinkedListNode, val: number): LinkedListNode { /* * 删除链表的头部 */ function deleteHead () { head = head.next if (head && head.val === val) { deleteHead() } } if (head) { if (head.val === val) { // 递归删除头部的节点 deleteHead() } if (head && head.next) { let prevNode = head let currentNode = head.next while (currentNode) { // 删除链表中间符合条件的节点 if (currentNode.val === val) { prevNode.next = currentNode.next currentNode = currentNode.next } else { prevNode = prevNode.next currentNode = currentNode.next } } } } return head}奇偶链表原题地址, 对于这道题目我们就需要运用上之前提到的两种套路(同时保留头部的指针以及当前的节点的指针和虚假的头部)function oddEvenList (head: LinkedListNode): LinkedListNode { let oddHead: LinkedListNode = new LinkedListNode(0) let evenHead: LinkedListNode = new LinkedListNode(0) let oddTail: LinkedListNode = oddHead let evenTail: LinkedListNode = evenHead let index: number = 1 while (head) { // 链接不同奇偶两条链 // 这里默认开头是1,所以index从1开始 if (index % 2 !== 0) { oddTail.next = head oddTail = oddTail.next } else { evenTail.next = head evenTail = evenTail.next } head = head.next index += 1 } // 偶数链的结尾是null,因为是尾部 evenTail.next = null // evenHead.next忽略到假头 oddTail.next = evenHead.next // oddHead.next忽略到假头 return oddHead.next}回文链表原题地址, 何为所谓的回文链表, 1 -> 2 -> 1 或者 1 -> 1 亦或则 1 -> 2 -> 2 -> 1 可以被称为回文链表。回文链表如果长度为奇数, 那么除去中间点, 两头的链表应当是在反转后应当是相同的。如果是偶数个, 链表的一半经过反转应该等于前半部分。当然有两种情况需要特殊考虑, 比如链表为 1 或者 1 -> 1 的情况下。在排除了这两种特色情况后, 可以通过快慢指针获取链表的中点(fast的速度是slow的两倍)。反转中点之后的链表后, 然后从头部开始和中点开始对比每一个节点的val。function isPalindrome (head: LinkedListNode): boolean { if (!head) { return true } // 通过快慢指针获取中点 let fast: LinkedListNode = head let slow: LinkedListNode = head // 链表中点 let middle = null // 循环结束后慢节点就是链表的中点 while (fast && fast.next && fast.next.next) { fast = fast.next.next slow = slow.next } // 一个和两个的情况 if (fast === slow) { if (!fast.next) { return true } else if ( fast.val === fast.next.val ) { return true } else { return false } } // middle保持对中点的引用 // slow往后移动 middle = slow // 反转中点以后的链表 function reverse () { let a = slow.next let b = slow.next.next a.next = middle slow.next = b middle = a } while (slow && slow.next) { reverse() } // 从头部和中点开始对比 while (head && middle) { if (head.val !== middle.val) { return false } head = head.next middle = middle.next } return true }合并两个有序链表原题地址, 对于创建一个新的链表使用的思路就是创建一个虚假的头部, 这道题目的解答也是如此。以及同时保留头部的指针以及尾部的指针, 无论是添加节点还是返回链表都会非常方便。function mergeTwoLists (l1: LinkedListNode, l2: LinkedListNode): LinkedListNode { // 头部的引用 let newHead: LinkedListNode = new LinkedListNode(0) // 尾部的引用 let newTail: LinkedListNode = newHead while (l1 && l2) { if (l1.val < l2.val) { newTail.next = l1 l1 = l1.next } else { newTail.next = l2 l2 = l2.next } // 始终指向尾部 newTail = newTail.next } if (!l1) { newTail.next = l2 } if (!l2) { newTail.next = l1 } // 忽略虚假的头部 return newHead.next}链表相加原题地址。生成虚假的头部后, 两个链表两两相加, 注意进位以及保留位即可。如果val不存在, 取0。(2 -> 4 -> 3) + (5 -> 6 -> 7 -> 8)03438765__ 9108function addTwoNumbers (l1: LinkedListNode, l2: LinkedListNode): LinkedListNode { let newHead: LinkedListNode = new LinkedListNode(0) let newTail: LinkedListNode = newHead // carry是进位,8 + 8 = 16 ,进位为1 let carry: number = 0 while (l1 || l2) { let a: number = l1 ? l1.val : 0 let b: number = l2 ? l2.val : 0 // val是保留的位 let val: number = (a + b + carry) % 10 carry = Math.floor((a + b + carry) / 10) let newNode = new LinkedListNode(val) newTail.next = newNode newTail = newTail.next if (l1) { l1 = l1.next } if (l2) { l2 = l2.next } } if (carry !== 0) { // 注意最后一位的进位 let newNode: LinkedListNode = new LinkedListNode(carry) newTail.next = newNode newTail = newTail.next } return newHead.next}旋转链表原题地址, 通过观察可知, 所谓的旋转就是依次将链表尾部的节点移动到链表的头部, 同时可以发现如果旋转的次数等于链表的长度。链表是没有发生改变的。所以通过提前计算出链表的长度, 可以减少旋转的次数。输入: 0-> 1-> 2 -> NULL向右旋转 1 步: 2 -> 0 -> 1 -> NULL向右旋转 2 步: 1 -> 2 -> 0 -> NULL向右旋转 3 步: 0 -> 1 -> 2 -> NULL向右旋转 4 步: 2 -> 0 -> 1 -> NULLvar rotateRight = function(head, k) { if (!head || !head.next) { return head } let length = 0 let c = head // 计算出链表的长度 while (c) { length += 1 c = c.next } // 将链表的尾部移动到链表的头部 // 链表尾部的前一个next指向null function rotate () { let a = head let b = head.next while (b && b.next) { a = b b = b.next } b.next = head head = b a.next = null } // 避免没有必要的选装 let newK = k % length let index = 1 while (index <= newK) { rotate() index += 1 } return head}; ...

March 18, 2019 · 7 min · jiezi

PHP面试常考之数据结构——链表的概念

你好,是我琉忆,PHP程序员面试笔试系列图书的作者。本周(2019.3.18至3.22)的一三五更新的文章如下:周一:PHP面试常考之数据结构——链表的概念周三:PHP面试常考之数据结构——栈和队列周五:PHP面试常考之数据结构——自己整理了一篇“PHP如何实现链表?”的文章,关注公众号:“琉忆编程库”,回复:“链表”,我发给你。一、链表链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。链表有很三种不同的类型:单向链表,双向链表以及循环链表。二、单向链表单向链表包含两个域,一个信息域和一个指针域。这个链接指向列表中的下一个节点,而最后一个节点则指向一个空值。如图:三、双向链表每个节点有两个连接:一个指向前一个节点,(当此“连接”为第一个“连接”时,指向空值或者空列表);而另一个指向下一个节点,(当此“连接”为最后一个“连接”时,指向空值或者空列表)如图:四、循环链表在一个循环链表中,首节点和末节点被连接在一起。这种方式在单向和双向链表中皆可实现。要转换一个循环链表,你开始于任意一个节点然后沿着列表的任一方向直到返回开始的节点。再来看另一种方法,循环链表可以被视为“无头无尾”。这种列表很利于节约数据存储缓存,假定你在一个列表中有一个对象并且希望所有其他对象迭代在一个非特殊的排列下。指向整个列表的指针可以被称作访问指针。自己编写的《PHP程序员面试笔试宝典》和《PHP程序员面试笔试真题解析》书籍,已在各大电商平台销售,两本可以帮助你更快更好的拿到offer的书。更多PHP相关的面试知识、考题可以关注公众号获取:琉忆编程库对本文有什么问题或建议都可以进行留言,我将不断完善追求极致,感谢你们的支持。

March 18, 2019 · 1 min · jiezi

Redis 数据结构之List (链表)

链表的作用首先我们知道,链表提供了高效的节点重排能力、顺序性的访问方式、灵活的增删节点并调整链表的长度。作为一种常用的数据结构,在很多高级的编程语言里都可以看到。实现的方式大同小异。与数组的比较从内存空间来看,数组是一个连续的内存空间属于连续存储存储形式。而链表则是离散存储的存储形式,有一个指针指向一个节点。所以意味着链表在内存中可以是离散的。就从内存存储方式来看,数组是一个连续的空间,查询更快,更加适合使用序列来访问数组元素。而链表更适对线性表长度无法确定,频繁的插入删除操作的动态性比较强的线性表。下图可见,数组和链表的存储形式。数组链表aotherbacotherdbeothernilc节点的定义首先我们看一下节点的定义。prev 记录前一个节点的指针next 记录下一个节点的指针value 记录节点的阵阵的值这样如果你知道第一个节点就能完整的把整个链表循环显示出来。其实目前来看整个链表的结构已经出现了,多个listNode通过prev next 就可以连起来行程一个链表。但是redis中的链表还对listNode 进行了一个封装,增加了一些冗余字段来提高访问的性能,跟上一篇文章说的SDS有相似地方。typedef struct listNode{ //前置节点 struct listNode *prev; //后置节点 struct listNode *next; //节点的值 void *value;}Redis中的list在redis中对listNode再次封装了下,使用了一个list。下面我们看下代码:可以看到跟SDS类似,多了一个表头,表尾,数量和3个函数。这几个属性能够达到什么效果呢?这3个函数可以说是对链表最常用的函数,使用起来十分的方便。链表数量作用跟SDS相似,可以用O(1)的复杂度获取整个链表的长度,表头和表尾可以快速的在表头和表尾添加元素。否则如果要在最后添加需要循环整个链表拿到尾节点,改变尾节点的next添加新节点。在redis中使用链表,几乎都是使用list。typedef struct list{ //链表头 listNode *head; //链表尾 listNode *tail; //链表数量 unsigned long len; //节点复制韩式布 vode *(*dup) (void *ptr); //节点释放 vode *(*free) (void *ptr); //节点对比 vode *(*match) (void *ptr,void *key);}

March 17, 2019 · 1 min · jiezi

【Leetcode】109.有序链表转换二叉搜索树

题目给定一个单链表,其中的元素按升序排序,将其转换为高度平衡的二叉搜索树。本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。示例:给定的有序链表: [-10, -3, 0, 5, 9],一个可能的答案是:[0, -3, 9, -10, null, 5], 它可以表示下面这个高度平衡二叉搜索树: 0 / \ -3 9 / / -10 5题解这道题和上一道类似,改动是把数组改成了链表。链表求中间节点的经典方法是快慢指针,慢指针每次走一步,快指针每次都走两步,这样快指针走到链表结尾的时候,慢指针就指向中间节点。这样就可以把问题转化为递归的子问题进行求解。class Solution { public TreeNode sortedListToBST(ListNode head) { if (head == null) { return null; } return helper(head, null); } public TreeNode helper(ListNode head, ListNode tail) { if (head == null || head == tail) { return null; } ListNode slow = head; ListNode fast = head; while (fast.next != tail && fast.next.next != tail) { fast = fast.next.next; slow = slow.next; } TreeNode current = new TreeNode(slow.val); current.left = helper(head, slow); current.right = helper(slow.next, tail); return current; } }这道题目还有一个比较巧妙的办法是利用BST中序遍历是升序的性质,去求解,具体看代码注释。class Solution { private ListNode node; public TreeNode sortedListToBST(ListNode head) { if (head == null) { return null; } int size = 0; ListNode runner = head; node = head; while (runner != null) { runner = runner.next; size++; } // 先计算有几个节点 return inorderHelper(0, size - 1); } public TreeNode inorderHelper(int start, int end) { if (start > end) { return null; } // 划分左右子树 int mid = start + (end - start) / 2; TreeNode left = inorderHelper(start, mid - 1); // 中序遍历 TreeNode treenode = new TreeNode(node.val); treenode.left = left; node = node.next; TreeNode right = inorderHelper(mid + 1, end); treenode.right = right; return treenode; }}最关键的一个步骤是node = node.next 这步的意思是基于:在BST中任意子树,它的中序遍历的结果如果存在一个链表中,一定是一个升序的,可以一一对应上,所以中序遍历完(这里是构建完)一个节点链表+1。热门阅读【Leetcode】108. 将有序数组转换为二叉搜索树【Leetcode】107. 二叉树的层次遍历 II【Leetcode】105. 从前序与中序遍历序列构造二叉树【Leetcode】106. 从中序与后序遍历序列构造二叉树手撕代码QQ群:805423079, 群密码:1024 ...

March 17, 2019 · 2 min · jiezi

ApacheCN 翻译活动进度公告 2019.3.17

【主页】 apachecn.org【Github】@ApacheCN暂时下线: 社区暂时下线: cwiki 知识库自媒体平台微博:@ApacheCN知乎:@ApacheCNCSDN | 简书 | OSChina | 博客园头条号 | 搜狐号 | bilibili 专栏We are ApacheCN Open Source Organization, not ASF! We are fans of AI, and have no relationship with ASF!合作or侵权,请联系【fonttian】<fonttian@gmail.com> | 请抄送一份到 <apachecn@163.com>seaborn 0.9 中文文档参与方式:https://github.com/apachecn/s…整体进度:https://github.com/apachecn/s…项目仓库:https://github.com/apachecn/s…认领:6/74,翻译:2/74序号章节译者进度1An introduction to seaborn@yiran7324 2Installing and getting started@neolei100%3Visualizing statistical relationships@JNJYan100%4Plotting with categorical data@hold2010 5Visualizing the distribution of a dataset@alohahahaha 6Visualizing linear relationships 7Building structured multi-plot grids@keyianpai 8Controlling figure aesthetics 9Choosing color palettes 10seaborn.relplot 11seaborn.scatterplot 12seaborn.lineplot 13seaborn.catplot 14seaborn.stripplot 15seaborn.swarmplot 16seaborn.boxplot 17seaborn.violinplot 18seaborn.boxenplot 19seaborn.pointplot 20seaborn.barplot 21seaborn.countplot 22seaborn.jointplot 23seaborn.pairplot 24seaborn.distplot 25seaborn.kdeplot 26seaborn.rugplot 27seaborn.lmplot 28seaborn.regplot 29seaborn.residplot 30seaborn.heatmap 31seaborn.clustermap 32seaborn.FacetGrid 33seaborn.FacetGrid.map 34seaborn.FacetGrid.map_dataframe 35seaborn.PairGrid 36seaborn.PairGrid.map 37seaborn.PairGrid.map_diag 38seaborn.PairGrid.map_offdiag 39seaborn.PairGrid.map_lower 40seaborn.PairGrid.map_upper 41seaborn.JointGrid 42seaborn.JointGrid.plot 43seaborn.JointGrid.plot_joint 44seaborn.JointGrid.plot_marginals 45seaborn.set 46seaborn.axes_style 47seaborn.set_style 48seaborn.plotting_context 49seaborn.set_context 50seaborn.set_color_codes 51seaborn.reset_defaults 52seaborn.reset_orig 53seaborn.set_palette 54seaborn.color_palette 55seaborn.husl_palette 56seaborn.hls_palette 57seaborn.cubehelix_palette 58seaborn.dark_palette 59seaborn.light_palette 60seaborn.diverging_palette 61seaborn.blend_palette 62seaborn.xkcd_palette 63seaborn.crayon_palette 64seaborn.mpl_palette 65seaborn.choose_colorbrewer_palette 66seaborn.choose_cubehelix_palette 67seaborn.choose_light_palette 68seaborn.choose_dark_palette 69seaborn.choose_diverging_palette 70seaborn.load_dataset 71seaborn.despine 72seaborn.desaturate 73seaborn.saturate 74seaborn.set_hls_values HBase 3.0 中文参考指南参与方式:https://github.com/apachecn/h…整体进度:https://github.com/apachecn/h…项目仓库:https://github.com/apachecn/h…认领:18/31,翻译:11/31章节译者进度Preface@xixici100%Getting Started@xixici100%Apache HBase Configuration@xixici100%Upgrading@xixici100%The Apache HBase Shell@xixici100%Data Model@Winchester-Yi HBase and Schema Design@RaymondCode100%RegionServer Sizing Rules of Thumb HBase and MapReduce@BridgetLai100%Securing Apache HBase Architecture@RaymondCode In-memory Compaction@mychaow Backup and Restore@mychaow Synchronous Replication@mychaow Apache HBase APIs@xixici100%Apache HBase External APIs@xixici100%Thrift API and Filter Language@xixici100%HBase and Spark@TsingJyujing100%Apache HBase Coprocessors@TsingJyujing Apache HBase Performance Tuning Troubleshooting and Debugging Apache HBase Apache HBase Case Studies Apache HBase Operational Management Building and Developing Apache HBase Unit Testing HBase Applications Protobuf in HBase@TsingJyujing Procedure Framework (Pv2): HBASE-12439 AMv2 Description for Devs ZooKeeper Community Appendix PyTorch 1.0 中文文档参与方式:https://github.com/apachecn/p…整体进度:https://github.com/apachecn/p…项目仓库:https://github.com/apachecn/p…教程部分:认领:37/37,翻译:34/37;文档部分:认领:37/39,翻译:34/39章节贡献者进度教程部分–Deep Learning with PyTorch: A 60 Minute Blitz@bat67100%What is PyTorch?@bat67100%Autograd: Automatic Differentiation@bat67100%Neural Networks@bat67100%Training a Classifier@bat67100%Optional: Data Parallelism@bat67100%Data Loading and Processing Tutorial@yportne13100%Learning PyTorch with Examples@bat67100%Transfer Learning Tutorial@jiangzhonglian100%Deploying a Seq2Seq Model with the Hybrid Frontend@cangyunye100%Saving and Loading Models@bruce1408100%What is torch.nn really?@lhc741100%Finetuning Torchvision Models@ZHHAYO100%Spatial Transformer Networks Tutorial@PEGASUS1993100%Neural Transfer Using PyTorch@bdqfork100%Adversarial Example Generation@cangyunye100%Transfering a Model from PyTorch to Caffe2 and Mobile using ONNX@PEGASUS1993100%Chatbot Tutorial@a625687551100%Generating Names with a Character-Level RNN@hhxx2015100%Classifying Names with a Character-Level RNN@hhxx2015100%Deep Learning for NLP with Pytorch@bruce1408100%Introduction to PyTorch@guobaoyo100%Deep Learning with PyTorch@bdqfork100%Word Embeddings: Encoding Lexical Semantics@sight007100%Sequence Models and Long-Short Term Memory Networks@ETCartman100%Advanced: Making Dynamic Decisions and the Bi-LSTM CRF@JohnJiangLA Translation with a Sequence to Sequence Network and Attention@mengfu188100%DCGAN Tutorial@wangshuai9517100%Reinforcement Learning (DQN) Tutorial@friedhelm739100%Creating Extensions Using numpy and scipy@cangyunye100%Custom C++ and CUDA Extensions@Lotayou Extending TorchScript with Custom C++ Operators@cloudyyyyy Writing Distributed Applications with PyTorch@firdameng100%PyTorch 1.0 Distributed Trainer with Amazon AWS@yportne13100%ONNX Live Tutorial@PEGASUS1993100%Loading a PyTorch Model in C++@talengu100%Using the PyTorch C++ Frontend@solerji100%文档部分–Autograd mechanics@PEGASUS1993100%Broadcasting semantics@PEGASUS1993100%CUDA semantics@jiangzhonglian100%Extending PyTorch@PEGASUS1993100%Frequently Asked Questions@PEGASUS1993100%Multiprocessing best practices@cvley100%Reproducibility@WyattHuang1 Serialization semantics@yuange250100%Windows FAQ@PEGASUS1993100%torch torch.Tensor@hijkzzz100%Tensor Attributes@yuange250100%Type Info@PEGASUS1993100%torch.sparse@hijkzzz100%torch.cuda@bdqfork100%torch.Storage@yuange250100%torch.nn@yuange250 torch.nn.functional@hijkzzz100%torch.nn.init@GeneZC100%torch.optim@qiaokuoyuan Automatic differentiation package - torch.autograd@gfjiangly100%Distributed communication package - torch.distributed@univeryinli100%Probability distributions - torch.distributions@hijkzzz100%Torch Script@keyianpai100%Multiprocessing package - torch.multiprocessing@hijkzzz100%torch.utils.bottleneck@belonHan100%torch.utils.checkpoint@belonHan100%torch.utils.cpp_extension@belonHan100%torch.utils.data@BXuan694100%torch.utils.dlpack@kunwuz100%torch.hub@kunwuz100%torch.utils.model_zoo@BXuan694100%torch.onnx@guobaoyo100%Distributed communication package (deprecated) - torch.distributed.deprecated torchvision Reference@BXuan694100%torchvision.datasets@BXuan694100%torchvision.models@BXuan694100%torchvision.transforms@BXuan694100%torchvision.utils@BXuan694100%AirFlow 中文文档参与方式:https://github.com/apachecn/a…整体进度:https://github.com/apachecn/a…项目仓库:https://github.com/apachecn/a…认领:25/30,翻译:24/30章节贡献者进度1 项目@zhongjiajie100%2 协议-100%3 快速开始@ImPerat0R_100%4 安装@Thinking Chen100%5 教程@ImPerat0R_100%6 操作指南@ImPerat0R_100%7 设置配置选项@ImPerat0R_100%8 初始化数据库后端@ImPerat0R_100%9 使用操作器@ImPerat0R_100%10 管理连接@ImPerat0R_100%11 保护连接@ImPerat0R_100%12 写日志@ImPerat0R_100%13 使用Celery扩大规模@ImPerat0R_100%14 使用Dask扩展@ImPerat0R_100%15 使用Mesos扩展(社区贡献)@ImPerat0R_100%16 使用systemd运行Airflow@ImPerat0R_100%17 使用upstart运行Airflow@ImPerat0R_100%18 使用测试模式配置@ImPerat0R_100%19 UI /截图@ImPerat0R_100%20 概念@ImPerat0R_100%21 数据分析@ImPerat0R_100%22 命令行接口@ImPerat0R_100%23 调度和触发器@Ray100%24 插件@ImPerat0R_100%25 安全 26 时区 27 实验性 Rest API@ImPerat0R_100%28 集成 29 Lineage 30 常见问题@zhongjiajie 31 API 参考 UCB CS61b:Java 中的数据结构参与方式:https://github.com/apachecn/c…整体进度:https://github.com/apachecn/c…项目仓库:https://github.com/apachecn/c…认领:5/12,翻译:1/12标题译者进度一、算法复杂度@leader402二、抽象数据类型@Allenyep100%三、满足规范 四、序列和它们的实现@biubiubiuboomboomboom五、树@biubiubiuboomboomboom六、搜索树 七、哈希 八、排序和选择@Rachel-Hu九、平衡搜索 十、并发和同步 十一、伪随机序列 十二、图 UCB Prob140:面向数据科学的概率论参与方式:https://github.com/apachecn/p…整体进度:https://github.com/apachecn/p…项目仓库:https://github.com/apachecn/p…认领:21/25,翻译:19/25标题译者翻译进度一、基础飞龙100%二、计算几率飞龙100%三、随机变量飞龙100%四、事件之间的关系@biubiubiuboomboomboom100%五、事件集合 >0%六、随机计数@viviwong100%七、泊松化@YAOYI626100%八、期望 50%九、条件(续)@YAOYI626100%十、马尔科夫链喵十八100%十一、马尔科夫链(续)喵十八100%十二、标准差缺只萨摩 100%十三、方差和协方差缺只萨摩 100%十四、中心极限定理喵十八100%十五、连续分布@ThunderboltSmile十六、变换十七、联合密度@Winchester-Yi100%十八、正态和 Gamma 族@Winchester-Yi100%十九、和的分布平淡的天100%二十、估计方法平淡的天100%二十一、Beta 和二项@lvzhetx100%二十二、预测 50%二十三、联合正态随机变量@JUNE951234二十四、简单线性回归@ThomasCai100%二十五、多元回归@lanhaixuan100%OpenCV 4.0 中文文档参与方式:https://github.com/apachecn/o…整体进度:https://github.com/apachecn/o…项目仓库:https://github.com/apachecn/o…认领:51/51,翻译:19/51。章节贡献者进度1. 简介@wstone00111.1 OpenCV-Python教程简介-100%1.2 安装OpenCV—Python-100%2. GUI功能@ranxx2.1 图像入门-100%2.2 视频入门-100%2.3 绘图功能-100%2.4 鼠标作为画笔-100%2.5 作为调色板的跟踪栏-100%3. 核心操作@luxinfeng3.1 图像基本操作-100%3.2 图像的算术运算-100%3.3 性能测量和改进技术-100%4. 图像处理@friedhelm7394.1 更改颜色空间-100%4.2 图像的几何变换-100%4.3 图像阈值-100%4.4 平滑图像-4.5 形态转换-4.6 图像梯度-4.7 Canny边缘检测-4.8 影像金字塔-4.9 轮廓-4.10 直方图-4.11 图像转换-4.12 模板匹配-4.13 霍夫线变换-4.14 霍夫圆变换-4.15 基于分水岭算法的图像分割-基于GrabCut算法的交互式前景提取-5. 特征检测和描述@3lackrush5.1 了解功能-100%5.2 Harris角点检测-5.3 Shi-Tomasi角点检测和追踪的良好特征-5.4 SIFT简介(尺度不变特征变换)-5.5 SURF简介(加速鲁棒特性)-5.6 角点检测的FAST算法-5.7 简介(二进制鲁棒独立基本特征)-5.8 ORB(定向快速和快速旋转)-5.9 特征匹配-5.10 特征匹配+ Homography查找对象-6. 视频分析@xmmmmmovo6.1 Meanshift和Camshift-100%6.2 光流-100%6.3 背景减法-100%7. 相机校准和3D重建@xmmmmmovo 7.1 相机校准-7.2 姿势估计-7.3 极线几何-7.4 立体图像的深度图-8. 机器学习@wstone00118.1 K-最近邻-8.2 支持向量机(SVM)-8.3 K-Means聚类-9. 计算摄影@ranxx9.1 图像去噪-9.2 图像修复-9.3 高动态范围(HDR)-10. 目标检测@jiangzhonglian 10.1 使用Haar Cascades进行人脸检测-100%11. OpenCV-Python绑定@daidai21 11.1 OpenCV-Python绑定如何工作?-100%翻译征集要求:机器学习/数据科学相关或者编程相关原文必须在互联网上开放不能只提供 PDF 格式(我们实在不想把精力都花在排版上)请先搜索有没有人翻译过请回复本文。赞助我们 ...

March 17, 2019 · 3 min · jiezi

Redis 数据结构之String

目的Redis现在是各个系统几乎都在使用的一种分布式高可用的缓存内存中的数据结构存储系统。可以作为数据库、缓存消息中间件、订阅发布系统等。我们都知道redis中有string、sets、sorted sets、hash、list类型。但是这些我们经常使用的数据结构的底层是怎么实现的。今天先记录一下string的结构。主要是参照Redis设计与实现和一些网上的资料总结的一个学习笔记。C语言字符串和SDS在Redis中的使用redis是用C语言实现的,所以在redis中的string有一些是直接使用C语言的字符串。但是在redis中还使用了叫做简单动态字符串(simple dynamic string)。redis中的string主要就是使用这两种string。但是什么时候用哪种呢?具体我们来看一下。C语言string:主要使用在一些无序对字符串进行修改的地方,比如说作为key、打印日志SDS:SDS是redis中默认字符串表示。几乎用于所有需要使用字符串操作的地方,包括AOF缓冲区等模块。我们举一个例子:redis> set aaa “bbb"这个时候aaa使用的是C语言字符串,bbb则是使用SDSRPUSH list “aaa” “bbb” “ccc"list这个key使用的是C语言字符串,aaa bbb ccc则是使用了3个SDS来保存SDS定义SDS的定义其实很简单,一共有3个属性。len、free、bufstruct sdshdr{ //记录buf数组中已经使用的数量 //等于整个字符串目前已经使用的长度(不包括结束字符”\0”) int len; //记录buf数组中未使用的字节数量 int free; //字节数组,用于保存字符串的地方 char buf[]}我们可以看到相比C语言中的字符串,SDS多了两个属性len和free。但就是这两个简单的属性再加上一些扩容策略就可以是的性能有一个很大的提升。接下来我们可以看一下SDS的优势点在哪里。SDS优势点快速获取字符串的长度在SDS的结构中有一个len的属性,如果要获取字符串长度则直接返属性即可。如果C语言字符串,则需要循环字节数组遇到了"0"计算出整个字符串的长度。很明显可以看到SDS使用的复杂度是O(1)。所以说在redis中获取字符串的长度对性能几乎是没有影响的。杜绝缓冲区溢出在C语言字符串中,当你添加字符到一个字符串是s1长度时如果忘记在执行前为s1分配足够多的空间,那么s1的数据将溢出到之后的字符串s2中,导致s2被意外的修改。然而SDS不同,每次对SDS字符串修改之前都会去判断字符串的容量是否足够。如果不够则会扩充SDS的内存大小。所以完全避免了这个错误。减少字符串修改带来的内存分配次数在C语言字符串中,每次对字符串的修改都会影响到内存的分配。如果增长字符串,则会为字符串重新分配一个新的内存空间。如果减少字符串,则会对减少的内存空间做内存回收,否则会引起内存泄漏。那么SDS是如何减少内存分配的呢? 是通过两点空间的预分配当SDS的API对一个SDS进行修改,并且需要扩展空间的时候,不仅会对SDS分配所需要的空间,还会为SDS分配额外的未使用空间。这个未使用空间的长度则记录在free属性中。这个预分配空间的策略根据SDS的长度决定。1 当SDS小于1MB的时候。每次扩容长度则跟len相同。举一个例子如果一个SDS扩容为12个字节,那么 SDS函数将会再添加一个12字节的预分配长度。既 len=12 free=12 buf长度则为25字节 (多出来的1个是结束符"0")2 当SDS大于1MB的时候,每次预分配长度则为1MB。相当于如果这个SDS是10MB,那么每次扩容之后的free的长度回是1MB惰性空间释放当一个SDS字符串缩短操作时,程序并不会马上重新收回多余出来的内容,而是用free字段将这些空余的空间记录下来。当下次如果需要往SDS添加字符,则可以再次使用这些空余空间。当然也不是意味着永远不会被回收,SDS有API来释放这些内存空间。二进制安全开始没有理解什么事二进制安全,网上找了一些资料。简单总结:**二进制安全的意思就是,只关心二进制化的字符串,不关心具体格式,只会严格的按照二进制的数据存取,不会妄图以某种特殊格式解析数据。C语言字符串的字符必须符合某种编码(比如ASCII)并且不能包含空字符,否则空字符之后的字符将会被忽略。二SDS则都是使用二进制处理,不仅可以存放字符串还可以放字节流,比如图片,视频等。因为redis不使用空字符串来判断长度而是用len属性。总结因为redis作为一个数据库存储来使,而且redis是一个单线程,一旦一个操作阻塞了之后之后的所有操作都会被影响。所以对性能的要求特别高,所以redis自己构建了这种字符串。 其实SDS结构十分简单,简单的使用了len,free两个字段配合一些策略,就大幅度的提高了性能和减少了内存重新分配的次数。值得我们学习,可以应用在其他程序设计中。

March 14, 2019 · 1 min · jiezi

T-Tree、T*-Tree的理解、实现与简单的内存数据库应用

章节目录T*-tree的介绍T*-tree节点与C语言实现T*-tree的插入、删除、查找与旋转实现简单的key-value内存数据库参考文献T-tree和T*-tree极为相似,他们的不同主要是T×-tree的节点结构比T-tree多了一个指向successor的指针位,指向successor的指针的存在是的树的寻找和遍历的时间复杂度.注:本文中关于ttree的head file :ttree.h和ttree_defs.h来源于Dan Kruchinin <dkruchinin@acm.org>,Github:dkruchinin/libttreeT*-tree的介绍在计算机科学中,T-tree是一种二叉树,它有一个左子树和一个右子树,由主存储器数据库使用,例如Datablitz,EXtremeDB,MySQL Cluster,Oracle TimesTen和MobileLite。T树是一种平衡的索引树数据结构,针对索引和实际数据都完全保存在内存中的情况进行了优化,就像B树是针对面向块的辅助存储设备(如硬盘)上的存储而优化的索引结构。 T树寻求获得内存树结构(如AVL树)的性能优势,同时避免它们常见的大存储空间开销。T树不保留索引树节点本身内的索引数据字段的副本。 相反,它们利用了这样的事实:实际数据总是与索引一起在主存储器中,因此它们只包含指向实际数据字段的指针。虽然T树以前被广泛用于主存数据库,但最近的研究表明它们在现代硬件上的表现并不比B树好。主要原因是现代的内存和硬盘的速度差异越来越大了,内存访问速度比硬盘访问速度快,并且CPU的核心缓存容量也越来越大。T*-tree的节点结构与C语言实现T树节点通常由指向父节点,左右子节点,有序数据指针数组和一些额外控制数据的指针组成。 具有两个子树的节点称为内部节点(internal nodes),没有子树的节点称为叶子节点(leaf nodes),而仅具有一个子树的节点称为半叶节点(half-leaf nodes)。 如果值在节点的当前最小值和最大值之间,则该节点称为(bounding node)。对于每个内部节点,它的左子树中会有一个叶子节点或半叶子节点中有一个predecessor(称为最大下限-GLB(Greatest Lower Bound)),还在右子树中包含其最大数据值的后继者(LUB-lower upper bound)的节点(包含GLB或LUP的节点或许距离参照内部节点的距离很远,但也有可能恰好相邻。正因为T-tree的每个节点是有序的,并且不同节点之间保证左子树的数据都比节点数据的最小值小,右子树的数据都比节点数据的最大值大,因此B-tree最左边的节点中最左边的数据是整棵树最小的数据,最右边的节点中的最大值是整棵树最大的数据。叶子和半叶节点中的数据元素量在1~最大元素量之间,内部节点将其占用保持在预定义的最小元素量和最大元素数之间。如图:T-tree,T-treenode的插入、删除、旋转和查找代码来自于:Github:dkruchinin/libttree typedef struct ttree_node { struct ttree_node *parent; /< Pointer to node’s parent */ struct ttree_node *successor; /< Pointer to node’s soccussor */ union { struct ttree_node *sides[2]; struct { struct ttree_node *left; /< Pointer to node’s left child */ struct ttree_node *right; /< Pointer to node’s right child */ }; }; union { uint32_t pad; struct { signed min_idx :12; /< Index of minimum item in node’s array */ signed max_idx :12; /< Index of maximum item in node’s array */ signed bfc :4; /< Node’s balance factor */ unsigned node_side :4; /< Node’s side(TNODE_LEFT, TNODE_RIGHT or TNODE_ROOT) / }; };T-tree的插入、删除、查找与旋转插入int ttree_insert(Ttree *ttree, void item){ TtreeCursor cursor; / * If the tree already contains the same key item has and * tree’s wasn’t allowed to hold duplicate keys, signal an error. */ if (ttree_lookup(ttree, ttree_item2key(ttree, item), &cursor) && ttree->keys_are_unique) { return -1; } ttree_insert_at_cursor(&cursor, item); return 0;}void ttree_insert_at_cursor(TtreeCursor *cursor, void *item){ Ttree *ttree = cursor->ttree; TtreeNode *at_node, *n; TtreeCursor tmp_cursor; void key; TTREE_ASSERT(cursor->ttree != NULL); //TTREE_ASSERT(cursor->state == CURSOR_PENDING); key = ttree_item2key(ttree, item); n = at_node = cursor->tnode; if (!ttree->root) { / The root node has to be created. / at_node = allocate_ttree_node(ttree); at_node->keys[first_tnode_idx(ttree)] = key; at_node->min_idx = at_node->max_idx = first_tnode_idx(ttree); ttree->root = at_node; tnode_set_side(at_node, TNODE_ROOT); ttree_cursor_open_on_node(cursor, ttree, at_node, TNODE_SEEK_START); return; } if (cursor->side == TNODE_BOUND) { if (tnode_is_full(ttree, n)) { / * If node is full its max item should be removed and * new key should be inserted into it. Removed key becomes * new insert value that should be put in successor node. */ void tmp = n->keys[n->max_idx–]; increase_tnode_window(ttree, n, &cursor->idx); n->keys[cursor->idx] = key; key = tmp; ttree_cursor_copy(&tmp_cursor, cursor); cursor = &tmp_cursor; / * If current node hasn’t successor and right child * New node have to be created. It’ll become the right child * of the current node. / if (!n->successor || !n->right) { cursor->side = TNODE_RIGHT; cursor->idx = first_tnode_idx(ttree); goto create_new_node; } at_node = n->successor; / * If successor hasn’t any free rooms, new value is inserted * into newly created node that becomes left child of the current * node’s successor. / if (tnode_is_full(ttree, at_node)) { cursor->side = TNODE_LEFT; cursor->idx = first_tnode_idx(ttree); goto create_new_node; } / * If we’re here, then successor has free rooms and key * will be inserted to one of them. */ cursor->idx = at_node->min_idx; cursor->tnode = at_node; } increase_tnode_window(ttree, at_node, &cursor->idx); at_node->keys[cursor->idx] = key; cursor->state = CURSOR_OPENED; return; }create_new_node: n = allocate_ttree_node(ttree); n->keys[cursor->idx] = key; n->min_idx = n->max_idx = cursor->idx; n->parent = at_node; at_node->sides[cursor->side] = n; tnode_set_side(n, cursor->side); cursor->tnode = n; cursor->state = CURSOR_OPENED; fixup_after_insertion(ttree, n, cursor);}删除void *ttree_delete(Ttree *ttree, void *key){ TtreeCursor cursor; void *ret; ret = ttree_lookup(ttree, key, &cursor); if (!ret) { return ret; } ttree_delete_at_cursor(&cursor); return ret;}void *ttree_delete_at_cursor(TtreeCursor *cursor){ Ttree *ttree = cursor->ttree; TtreeNode *tnode, n; void ret; TTREE_ASSERT(cursor->ttree != NULL); TTREE_ASSERT(cursor->state == CURSOR_OPENED); tnode = cursor->tnode; ret = ttree_key2item(ttree, tnode->keys[cursor->idx]); decrease_tnode_window(ttree, tnode, &cursor->idx); cursor->state = CURSOR_CLOSED; if (UNLIKELY(cursor->idx > tnode->max_idx)) { cursor->idx = tnode->max_idx; } / * If after a key was removed, T-tree node contains more than * minimum allowed number of items, the proccess is completed. / if (tnode_num_keys(tnode) > min_tnode_entries(ttree)) { return ret; } if (is_internal_node(tnode)) { int idx; / * If it is an internal node, we have to recover number * of items from it by moving one item from its successor. / n = tnode->successor; idx = tnode->max_idx + 1; increase_tnode_window(ttree, tnode, &idx); tnode->keys[idx] = n->keys[n->min_idx++]; if (UNLIKELY(cursor->idx > tnode->max_idx)) { cursor->idx = tnode->max_idx; } if (!tnode_is_empty(n) && is_leaf_node(n)) { return ret; } / * If we’re here, then successor is either a half-leaf * or an empty leaf. / tnode = n; } if (!is_leaf_node(tnode)) { int items, diff; n = tnode->left ? tnode->left : tnode->right; items = tnode_num_keys(n); / * If half-leaf can not be merged with a leaf, * the proccess is completed. / if (items > (ttree->keys_per_tnode - tnode_num_keys(tnode))) { return ret; } if (tnode_get_side(n) == TNODE_RIGHT) { / * Merge current node with its right leaf. Items from the leaf * are placed after the maximum item in a node. */ diff = (ttree->keys_per_tnode - tnode->max_idx - items) - 1; if (diff < 0) { memcpy(tnode->keys + tnode->min_idx + diff, tnode->keys + tnode->min_idx, sizeof(void *) * tnode_num_keys(tnode)); tnode->min_idx += diff; tnode->max_idx += diff; if (cursor->tnode == tnode) { cursor->idx += diff; } } memcpy(tnode->keys + tnode->max_idx + 1, n->keys + n->min_idx, sizeof(void ) * items); tnode->max_idx += items; } else { / * Merge current node with its left leaf. Items the leaf * are placed before the minimum item in a node. */ diff = tnode->min_idx - items; if (diff < 0) { register int i; for (i = tnode->max_idx; i >= tnode->min_idx; i–) { tnode->keys[i - diff] = tnode->keys[i]; } tnode->min_idx -= diff; tnode->max_idx -= diff; if (cursor->tnode == tnode) { cursor->idx -= diff; } } memcpy(tnode->keys + tnode->min_idx - items, n->keys + n->min_idx, sizeof(void ) * items); tnode->min_idx -= items; } n->min_idx = 1; n->max_idx = 0; tnode = n; } if (!tnode_is_empty(tnode)) { return ret; } / if we’re here, then current node will be removed from the tree. */ n = tnode->parent; if (!n) { ttree->root = NULL; free(tnode); return ret; } n->sides[tnode_get_side(tnode)] = NULL; fixup_after_deletion(ttree, tnode, NULL); free(tnode); return ret;}查找void *ttree_lookup(Ttree *ttree, void *key, TtreeCursor *cursor){ TtreeNode *n, *marked_tn, *target; int side = TNODE_BOUND, cmp_res, idx; void item = NULL; enum ttree_cursor_state st = CURSOR_PENDING; / * Classical T-tree search algorithm is O(log(2N/M) + log(M - 2)) * Where N is total number of items in the tree and M is a number of * items per node. In worst case each node on the path requires 2 * comparison(with its min and max items) plus binary search in the last * node(bound node) excluding its first and last items. * * Here is used another approach that was suggested in * “Tobin J. Lehman , Michael J. Carey, A Study of Index Structures for * Main Memory Database Management Systems”. * It reduces O(log(2N/M) + log(M - 2)) to true O(log(N)). * This algorithm compares the search * key only with minimum item in each node. If search key is greater, * current node is marked for future consideration. / target = n = ttree->root; marked_tn = NULL; idx = first_tnode_idx(ttree); if (!n) { goto out; } while (n) { target = n; cmp_res = ttree->cmp_func(key, tnode_key_min(n)); if (cmp_res < 0) side = TNODE_LEFT; else if (cmp_res > 0) { marked_tn = n; / mark current node for future consideration. / side = TNODE_RIGHT; } else { / ok, key is found, search is completed. / side = TNODE_BOUND; idx = n->min_idx; item = ttree_key2item(ttree, tnode_key_min(n)); st = CURSOR_OPENED; goto out; } n = n->sides[side]; } if (marked_tn) { int c = ttree->cmp_func(key, tnode_key_max(marked_tn)); if (c <= 0) { side = TNODE_BOUND; target = marked_tn; if (!c) { item = ttree_key2item(ttree, tnode_key_max(target)); idx = target->max_idx; st = CURSOR_OPENED; } else { / make internal binary search / struct tnode_lookup tnl; tnl.key = key; tnl.low_bound = target->min_idx + 1; tnl.high_bound = target->max_idx - 1; item = lookup_inside_tnode(ttree, target, &tnl, &idx); st = (item != NULL) ? CURSOR_OPENED : CURSOR_PENDING; } goto out; } } / * If we’re here, item wasn’t found. So the only thing * needs to be done is to determine the position where search key * may be placed to. If target node is not empty, key may be placed * to its min or max positions. */ if (!tnode_is_full(ttree, target)) { side = TNODE_BOUND; idx = ((marked_tn != target) || (cmp_res < 0)) ? target->min_idx : (target->max_idx + 1); st = CURSOR_PENDING; }out: if (cursor) { ttree_cursor_open_on_node(cursor, ttree, target, TNODE_SEEK_START); cursor->side = side; cursor->idx = idx; cursor->state = st; } return item;}旋转static void __rotate_single(TtreeNode **target, int side){ TtreeNode *p, *s; int opside = opposite_side(side); p = *target; TTREE_ASSERT(p != NULL); s = p->sides[side]; TTREE_ASSERT(s != NULL); tnode_set_side(s, tnode_get_side(p)); p->sides[side] = s->sides[opside]; s->sides[opside] = p; tnode_set_side(p, opside); s->parent = p->parent; p->parent = s; if (p->sides[side]) { p->sides[side]->parent = p; tnode_set_side(p->sides[side], side); } if (s->parent) { if (s->parent->sides[side] == p) s->parent->sides[side] = s; else s->parent->sides[opside] = s; } target = s;}/ * There are two cases of single rotation possible: * 1) Right rotation (side = TNODE_LEFT) * [P] [L] * / \ / \ * [L] x1 => x2 [P] * / \ / \ * x2 x3 x3 x1 * * 2) Left rotation (side = TNODE_RIHGT) * [P] [R] * / \ / \ * x1 [R] => [P] x2 * / \ / \ * x3 x2 x1 x3 */static void rotate_single(TtreeNode **target, int side){ TtreeNode *n; __rotate_single(target, side); n = (target)->sides[opposite_side(side)]; / * Recalculate balance factors of nodes after rotation. * Let X was a root node of rotated subtree and Y was its * child. After single rotation Y is new root of subtree and X is its child. * Y node may become either balanced or overweighted to the * same side it was but 1 level less. * X node scales at 1 level down and possibly it has new child, so * its balance should be recalculated too. If it still internal node and * its new parent was not overwaighted to the opposite to X side, * X is overweighted to the opposite to its new parent side, * otherwise it’s balanced. If X is either half-leaf or leaf, * balance racalculation is obvious. */ if (is_internal_node(n)) { n->bfc = (n->parent->bfc != side2bfc(side)) ? side2bfc(side) : 0; } else { n->bfc = !!(n->right) - !!(n->left); } (*target)->bfc += side2bfc(opposite_side(side)); TTREE_ASSERT((abs(n->bfc < 2) && (abs((target)->bfc) < 2)));}/ * There are two possible cases of double rotation: * 1) Left-right rotation: (side == TNODE_LEFT) * [P] [r] * / \ / \ * [L] x1 [L] [P] * / \ => / \ / \ * x2 [r] x2 x4 x3 x1 * / \ * x4 x3 * * 2) Right-left rotation: (side == TNODE_RIGHT) * [P] [l] * / \ / \ * x1 [R] [P] [R] * / \ => / \ / \ * [l] x2 x1 x3 x4 x2 * / \ * x3 x4 */static void rotate_double(TtreeNode **target, int side){ int opside = opposite_side(side); TtreeNode *n = (target)->sides[side]; __rotate_single(&n, opside); / * Balance recalculation is very similar to recalculation after * simple single rotation. */ if (is_internal_node(n->sides[side])) { n->sides[side]->bfc = (n->bfc == side2bfc(opside)) ? side2bfc(side) : 0; } else { n->sides[side]->bfc = !!(n->sides[side]->right) - !!(n->sides[side]->left); } TTREE_ASSERT(abs(n->sides[side]->bfc) < 2); n = n->parent; __rotate_single(target, side); if (is_internal_node(n)) { n->bfc = ((target)->bfc == side2bfc(side)) ? side2bfc(opside) : 0; } else { n->bfc = !!(n->right) - !!(n->left); } / * new root node of subtree is always ideally balanced * after double rotation. */ TTREE_ASSERT(abs(n->bfc) < 2); (*target)->bfc = 0;}static void rebalance(Ttree *ttree, TtreeNode **node, TtreeCursor *cursor){ int lh = left_heavy(*node); int sum = abs((*node)->bfc + (node)->sides[opposite_side(lh)]->bfc); if (sum >= 2) { rotate_single(node, opposite_side(lh)); goto out; } rotate_double(node, opposite_side(lh)); / * T-tree rotation rules difference from AVL rules in only one aspect. * After double rotation is done and a leaf became a new root node of * subtree and both its left and right childs are half-leafs. * If the new root node contains only one item, N - 1 items should * be moved into it from one of its childs. * (N is a number of items in selected child node). */ if ((tnode_num_keys(*node) == 1) && is_half_leaf((*node)->left) && is_half_leaf((*node)->right)) { TtreeNode n; int offs, nkeys; / * If right child contains more items than left, they will be moved * from the right child. Otherwise from the left one. */ if (tnode_num_keys((*node)->right) >= tnode_num_keys((node)->left)) { / * Right child was selected. So first N - 1 items will be copied * and inserted after parent’s first item. */ n = (*node)->right; nkeys = tnode_num_keys(n); (*node)->keys[0] = (*node)->keys[(*node)->min_idx]; offs = 1; (*node)->min_idx = 0; (*node)->max_idx = nkeys - 1; if (!cursor) { goto no_cursor; } else if (cursor->tnode == n) { if (cursor->idx < n->max_idx) { cursor->tnode = *node; cursor->idx = (node)->min_idx + (cursor->idx - n->min_idx + 1); } else { cursor->idx = first_tnode_idx(ttree); } } } else { / * Left child was selected. So its N - 1 items * (starting after the min one) * will be copied and inserted before parent’s single item. / n = (node)->left; nkeys = tnode_num_keys(n); (node)->keys[ttree->keys_per_tnode - 1] = (node)->keys[(node)->min_idx]; (node)->min_idx = offs = ttree->keys_per_tnode - nkeys; (node)->max_idx = ttree->keys_per_tnode - 1; if (!cursor) { goto no_cursor; } else if (cursor->tnode == n) { if (cursor->idx > n->min_idx) { cursor->tnode = node; cursor->idx = (node)->min_idx + (cursor->idx - n->min_idx); } else { cursor->idx = first_tnode_idx(ttree); } } n->max_idx = n->min_idx++; }no_cursor: memcpy((node)->keys + offs, n->keys + n->min_idx, sizeof(void ) * (nkeys - 1)); n->keys[first_tnode_idx(ttree)] = n->keys[n->max_idx]; n->min_idx = n->max_idx = first_tnode_idx(ttree); }out: if (ttree->root->parent) { ttree->root = node; }}实现简单的key-value内存数据库实现简单的key-value内存数据库,用hashtable来链接key-value的关系。key不光插入到ttree中,而且还存到hash-table中。hash_table采用了macro:hash-table(uthash.h)`uthash.h的帮助文档:macro:uthash.h帮助文档hashkey-value对的插入:插入之前先HASH_FIND_INT看看,key-value存不存在,如果不存在则可以插,存在的话不能插入。void add_user(int user_id, char name) { struct my_struct s; HASH_FIND_INT(users, &user_id, s); / id already in the hash? / if (s==NULL) { s = (struct my_struct )malloc(sizeof s); s->id = user_id; HASH_ADD_INT( users, id, s ); / id: name of key field / } strcpy(s->name, name);}解析存有key-value格式的文件找到一个key-value格式的实例:xlarge.delfopen()读取文件,读完之后 fclose()关闭。因为待会儿要用strtok来拆开每一行,所以malloc个file_line FILE * fp; fp = fopen("/home/vory/programing/c/key_value_mmdb/xlarge.del",“r”); file_line = malloc(1000 * sizeof(char)); memset(file_line, 0, 1000 * sizeof(char)); …… fclose(fp); fgets获取每一行: char * buf; buf = malloc(sizeof(input)); memset(buf,0,sizeof(input)); while(fgets(input ,256,fp)){ strcpy(buf,input); …… }strtok切割每一行为若干字符串。strtok将目标字符串的符合delim中的元素全部替换为'0’,strtok用了之后,原来的目标代码就被破坏了,因此,新malloc一个接受复制的字符串,将目标字符串strcpy()之后,对复制的字符串进行strtok()操作。用完之后free()。 strcpy(buf,source_string); token = strtok(buf,delim);// printf("%s\n",token); parameter[parametercount] = malloc(sizeof(input)); strcpy(parameter[parametercount],token); parametercount++; token = strtok(NULL,delim);// printf("%s\n",token); parameter[parametercount] = malloc(sizeof(input)); strcpy(parameter[parametercount],token); 实例的xlarge.del 文件的内容大概是这样:每一行分成两部分,KEY 和VALUE被逗号隔开着。41231234,“Teenage Caveman"3061234,“Banger Sisters, The"18861234,“Hope Floats"29381234,“No Looking Back"1288,“Erotic Confessions: Volume 8"2954,“Normal Life"43901234,“Utomlyonnye solntsem"20801234,“Island of Dr. Moreau, The"3019,“One Hell of a Guy"6712341,“Adventures of Pluto Nash, The"33031234,“Pronto"34701234,“Ripper, The"106612341,“Devotion"39481234,“Starship Troopers"32381234,“Polish Wedding"30551234,“Oscar and Lucinda"42391,“Tomcats"1661123411,“Gojira ni-sen mireniamu"10611234,“Devil in a Blue Dress"61612341,“Bully"102612341,“Defenders: Taking the First, The"1650,“Go Fish"43512341,“Black Rose of Harlem"解析从文件读来的每一行:每一行最多解析为2个参数([KEY] [VALUE])。void parse_file(char * string){ char * buf; char * delim; delim = NULL; delim = “,”; char * token; token = NULL; buf = malloc(1000sizeof(char)); memset(buf,0, 1000sizeof(char)); if (!parf0){ parf0 =malloc(500sizeof(char)); } memset(parf0,0, 500sizeof(char)); if (!parf1){ parf1 =malloc(500sizeof(char)); } memset(parf1,0, 500sizeof(char)); strcpy(buf, string); token = strtok(buf, delim); if(token != NULL) { strcpy(parf0, token); } token = strtok(NULL, delim); if (token != NULL){ strcpy(parf1, token); } free(buf);}strtol将字符型的数据转换成long int型:long int strtol(const char nptr,char endptr,int base);strtol不仅可以识别十进制整数,还可以识别其它进制的整数,取决于base参数,base为10,则识别十进制整数。 all_items[bcount].key = strtol(parf0,NULL ,10); bcount++; hash_user_id = strtol(parf0,NULL ,10); strcpy(hash_name,parf1);解析从命令行输入的命令([COMMANDS] [KEY] [VALUE])从文件读取每一行是 [KEY] [VALUE]的格式,但是从命令行读取是[COMMANDS] [KEY] [VALUE]的格式。我将copy_string,par1,par2,par3定义在了别处。这样就可以解析从stdin输入的句子了,被空格隔开的句子最多分为发出3路给par1,par2,par3。如果stdin输入的句子只包含了一个空格(也就是含有[COMMANDS] [KEY]的结构)则只能被分发为2路给par1和par2.malloc()之后要free(),我在别处free() 了。void parseinput(char string){ char * delim; delim = " “; copy_string = malloc(100sizeof(char)); memset(copy_string,0,100 sizeof(char)); char * token; token = NULL; par1 = malloc(50sizeof(char)); par2 = malloc(50sizeof(char)); par3 = malloc(50sizeof(char)); memset(par1,0,50sizeof(char)); memset(par2,0,50sizeof(char)); memset(par3,0,50sizeof(char)); strcpy(copy_string,string); printf("%s is copystring .\n “,copy_string); printf("%s is string . \n”,string); token = strtok(copy_string,delim); if (token != NULL){ printf("%s is token1 \n”,token); strcpy(par1,token); } token = strtok(NULL,delim); if (token != NULL){ printf("%s is token2 \n”,token); strcpy(par2,token); } token = strtok(NULL,delim); if (token != NULL){ printf("%s is token3 \n”,token); strcpy(par3,token); } free(copy_string);}初始化T-tree#define ttree_init(ttree, num_keys, is_unique, cmpf, data_struct, key_field) _ttree_init(ttree, num_keys, is_unique, cmpf, offsetof(data_struct, key_field))int __ttree_init(Ttree ttree, int num_keys, bool is_unique, ttree_cmp_func_fn cmpf, size_t key_offs);……….. ret = ttree_init(&ttree, 8, false, __cmpfunc, struct item, key); if (ret < 0) { fprintf(stderr, “Failed to initialize T-tree. [ERR=%d]\n”, ret); free(all_items); exit(EXIT_FAILURE); }将读取的每一行插入ttree,并将key-value插入hashtable在一个循环中解析每一行,当真个文件的所有行都读完则跳出循环。 while (fgets(file_line, 1000, fp)) { parse_file(file_line); all_items[bcount].key = strtol(parf0, NULL, 10); hash_name = malloc(500 * sizeof(char)); memset(hash_name, 0, 500 * sizeof(char)); hash_user_id = strtol(parf0, NULL, 10); strcpy(hash_name, parf1); s = find_user(hash_user_id); if (s == NULL) { add_user(hash_user_id, hash_name); } free(hash_name); memset(file_line, 0, 1000 * sizeof(char)); } for (i = 0; i < num_keys; i++) { ret = ttree_insert(&ttree, &all_items[i]); if (ret < 0) { fprintf(stderr, “Failed to insert item %d with key %ld! [ERR=%d]\n”, i, all_items[i].key, ret); free(all_items); exit(EXIT_FAILURE); } } 打印出ttree的所有key for (i = 0; i < num_keys; i++) { printf("%ld “, all_items[i].key); }给ttree的所有key排序从小到大排序,递归实现。 printf("\nSorted keys:\n”); printf(”{ “); tnode = ttree_node_leftmost(ttree.root); while (tnode) { tnode_for_each_index(tnode, i) { printf("%d “, (int ) tnode_key(tnode, i)); } tnode = tnode->successor; } printf(”}\n”);程序结束前free(),释放内存空间ttree_destroy(&ttree);free(all_items);附件&代码github代码所有的代码参考文献[1].Tobin J. Lehman and Michael J. Carey. 1986. A Study of Index Structures for Main Memory Database Management Systems. In Proceedings of the 12th International Conference on Very Large Data Bases (VLDB ‘86), Wesley W. Chu, Georges Gardarin, Setsuo Ohsuga, and Yahiko Kambayashi (Eds.). Morgan Kaufmann Publishers Inc., San Francisco, CA, USA, 294-303.[2].Kong-Rim Choi and Kyung-Chang Kim, “T-tree: a main memory database index structure for real time applications,” Proceedings of 3rd International Workshop on Real-Time Computing Systems and Applications, Seoul, South Korea, 1996, pp. 81-88.doi: 10.1109/RTCSA.1996.554964[3].wikipidia about T-tree[4].An Open-source T-tree Library ...

March 14, 2019 · 13 min · jiezi

考研系列笔记:数据结构邓俊辉

在线课程学堂在线:数据结构(上)(自主模式)学堂在线:数据结构(下)(自主模式)教材与习题集链接: https://pan.baidu.com/s/1Pp0K… 提取码: 98mg目录提纲第一章 绪论(a)计算(b)计算模型(c)大O记号(d)算法分析(e)迭代与递归(xc)动态规划第二章 向量(a)接口与实现(b)可扩充向量(c)无序向量(d1)有序向量:唯一化(d2)有序向量:二分查找(d3)有序向量:Fibonacci查找(d4)有序向量:二分查找(改进)(d5)有序向量:插值查找(e)起泡排序(f)归并排序第三章 列表(a)接口与实现(b)无序列表(c)有序列表(d)选择排序(e)插入排序(xd)习题辅导:LightHouse第四章 栈与队列(a)栈接口与实现(c1)栈应用:进制转换(c2)栈应用:括号匹配(c3)栈应用:栈混洗(c4)栈应用:中缀表达式求值(c5)栈应用:逆波兰表达式(d)队列接口与实现第五章 二叉树(a)树(b)树的表示(c)二叉树(d)二叉树实现(e1)先序遍历(e2)中序遍历(e4)层次遍历(e5)重构第六章 图(a)概述(b1)邻接矩阵(c)广度优先搜索(d)深度优先搜索第七章 二叉搜索树(a)概述(b1)BST:查找(b2)BST:插入(b3)BST:删除(c)平衡与等价(d1)AVL树:重平衡(d2)AVL树:插入(d3)AVL树:删除(d4)AVL树:(3+4)-重构第八章 高级搜索树(a1)伸展树:逐层伸展(a2)伸展树:双层伸展(a3)伸展树:算法实现(b1)B-树:动机(b2)B-树:结构(b3)B-树:查找(b4)B-树: 插入(b5)B-树: 删除(xa1)红黑树:动机(xa2)红黑树:结构(xa3)红黑树:插入(xa4)红黑树:删除第九章 词典(b)散列:原理(c)散列:散列函数(d1)散列:排解冲突(1) (d2)散列:排解冲突(2) (e)桶/计数排序第十章 优先级队列(a1)需求与动机(a2)基本实现(b1)完全二叉堆:结构(b2)完全二叉堆:插入与上滤(b3)完全二叉堆:删除与下滤(b4)完全二叉堆:批量建堆(c)堆排序(xa1)左式堆:结构(xa2)左式堆:合并(xa3)左式堆:插入与删除第十一章 串(a)ADT(b1)串匹配(b2)蛮力匹配(c1)KMP算法:从记忆力到预知力(c2)KMP算法:查询表(c3)KMP算法:理解next[]表(c4)KMP算法:构造next[]表(c5)KMP算法:分摊分析(c6)KMP算法:再改进(d1)BM_BC算法:以终为始(d2)BM_BC算法:坏字符(d3)BM_BC算法:构造bc[](d4)BM_BC算法:性能分析(e1)BM_GS算法:好后缀(e2)BM_GS算法:构造gs表(e3)BM_GS算法:综合性能(f1)Karp-Rabin算法:串即是数(f2)Karp-Rabin算法:散列第十二章 排序(a1)快速排序:算法A(a2)快速排序:性能分析(a4)快速排序:变种(b1)选取:众数(b3)选取:通用算法(c1)希尔排序:Shell序列(c3)希尔排序:更佳的序列

March 12, 2019 · 1 min · jiezi

备战2019计算机912考研系列笔记绪论

计算机912考研系列笔记本笔记记录从2019年3月份到12月份,准备计算机912考研相关内容与912考研进度历程。一方面是给后人提供一些参考,一方面方便自己消化咀嚼知识。912与408区别balabalatop5学校分析balabala复习(学习)计划四大专业课:数据结构操作系统计算机组成原理计算机网络

March 11, 2019 · 1 min · jiezi

小型Redis完成! | 自己实现Redis源代码(4)

缘起近期在阅读《Redis设计与实现》一书,我发现如果不动手实践,显然是无法真正理解书上奇形怪状的数据结构的。所以为了锻炼自己的数据结构与算法能力,我参照其中一些数据结构的API,对诸如动态字符串SDS,双端链表list,字典dict及其内嵌的哈希表dictht等数据结构进行了实现。当然,为了让他们有用武之地,我在这基础上构建了一个小型Redis,作为自己的学习记录。当然现在的项目功能还不够完善,后期我会慢慢将其完善,恳请批评指教!github链接实现流程如今本项目还只是实现了key-value的存储功能,其他诸如切换数据库、数据持久化等功能将会在后期慢慢实现。项目实现流程如下:基本数据结构的构建客户端服务端的交互基本命令的实现原理分析1.基本数据结构的构建下面主要介绍数据库结构redisDb的构建,其他数据结构可以参见我的系列文章:动态字符串SDS的实现 | 自己实现Redis源代码(1)双端链表list的实现 | 自己实现Redis源代码(2)字典与哈希表 | 自己实现Redis源代码(3)数据库redisDb的基本结构如下:2. 客户端服务端的交互客户端与服务端的结构如下两者通过建立网络连接,进行数据交互,完成通信过程。这里的网络连接的建立是通过套接字socket建立的。在Redis的单机应用中,一个服务端redisServer进程可以处理多个客户端的请求。对多个客户端的处理部分我们通过创建线程来完成。每次检测到有一个客户端进行连接,便为其创建一个工作线程,在其中执行客户端与服务端的通信操作。服务端含有一个数据库数组,记录保存在服务端的所有数据库,默认数据库数量为16。客户端含有一个数据库指针,指向其当前正在使用的目标数据库,方便其进行切换数据库操作。3. 基本命令的实现这里主要讲解get/set命令的实现。服务端结构到客户端输入的命令,需要进行一下操作:1)判断是否为查看类命令如,查看帮助文档,查看版本信息等;2)对命令进行分割3)判断命令关键字如,命令为set pig 12,切割后我们可以得到[“set”,“pig”,“12”],通过对关键字set的判断,可以得知其为set命令。4)检查命令长度5)执行相关命令测试结果

March 10, 2019 · 1 min · jiezi

Leetcode讲解视频(持续更新中...)

【Leetcode】146.LRU缓存机制【Leetcode】108.将有序数组转换为二叉搜索树【Leetcode】107.二叉树的层次遍历【Leetcode】106. 从中序与后序遍历序列构造二叉树【Leetcode】105. 从前序与中序遍历序列构造二叉树【Leetcode】101.镜像二叉树【Leetcode】100.相同的树

March 10, 2019 · 1 min · jiezi

ApacheCN 翻译活动进度公告 2019.3.10

【主页】 apachecn.org【Github】@ApacheCN暂时下线: 社区暂时下线: cwiki 知识库自媒体平台微博:@ApacheCN知乎:@ApacheCNCSDN简书OSChina博客园We are ApacheCN Open Source Organization, not ASF! We are fans of AI, and have no relationship with ASF!合作or侵权,请联系【fonttian】fonttian@gmail.com | 请抄送一份到 apachecn@163.comPyTorch 1.0 中文文档参与方式:https://github.com/apachecn/p…整体进度:https://github.com/apachecn/p…项目仓库:https://github.com/apachecn/p…教程部分:认领:37/37,翻译:32/37;文档部分:认领:37/39,翻译:34/39章节贡献者进度教程部分–Deep Learning with PyTorch: A 60 Minute Blitz@bat67100%What is PyTorch?@bat67100%Autograd: Automatic Differentiation@bat67100%Neural Networks@bat67100%Training a Classifier@bat67100%Optional: Data Parallelism@bat67100%Data Loading and Processing Tutorial@yportne13100%Learning PyTorch with Examples@bat67100%Transfer Learning Tutorial@jiangzhonglian100%Deploying a Seq2Seq Model with the Hybrid Frontend@cangyunye100%Saving and Loading Models@bruce1408 What is torch.nn really?@lhc741100%Finetuning Torchvision Models@ZHHAYO100%Spatial Transformer Networks Tutorial@PEGASUS1993100%Neural Transfer Using PyTorch@bdqfork100%Adversarial Example Generation@cangyunye100%Transfering a Model from PyTorch to Caffe2 and Mobile using ONNX@PEGASUS1993100%Chatbot Tutorial@a625687551100%Generating Names with a Character-Level RNN@hhxx2015100%Classifying Names with a Character-Level RNN@hhxx2015100%Deep Learning for NLP with Pytorch@bruce1408 Introduction to PyTorch@guobaoyo100%Deep Learning with PyTorch@bdqfork100%Word Embeddings: Encoding Lexical Semantics@sight007100%Sequence Models and Long-Short Term Memory Networks@ETCartman100%Advanced: Making Dynamic Decisions and the Bi-LSTM CRF@JohnJiangLA Translation with a Sequence to Sequence Network and Attention@mengfu188100%DCGAN Tutorial@wangshuai9517100%Reinforcement Learning (DQN) Tutorial@friedhelm739100%Creating Extensions Using numpy and scipy@cangyunye100%Custom C++ and CUDA Extensions@Lotayou Extending TorchScript with Custom C++ Operators@cloudyyyyy Writing Distributed Applications with PyTorch@firdameng100%PyTorch 1.0 Distributed Trainer with Amazon AWS@yportne13100%ONNX Live Tutorial@PEGASUS1993100%Loading a PyTorch Model in C++@talengu100%Using the PyTorch C++ Frontend@solerji100%文档部分–Autograd mechanics@PEGASUS1993100%Broadcasting semantics@PEGASUS1993100%CUDA semantics@jiangzhonglian100%Extending PyTorch@PEGASUS1993100%Frequently Asked Questions@PEGASUS1993100%Multiprocessing best practices@cvley100%Reproducibility@WyattHuang1 Serialization semantics@yuange250100%Windows FAQ@PEGASUS1993100%torch torch.Tensor@hijkzzz100%Tensor Attributes@yuange250100%Type Info@PEGASUS1993100%torch.sparse@hijkzzz100%torch.cuda@bdqfork100%torch.Storage@yuange250100%torch.nn@yuange250 torch.nn.functional@hijkzzz100%torch.nn.init@GeneZC100%torch.optim@qiaokuoyuan Automatic differentiation package - torch.autograd@gfjiangly100%Distributed communication package - torch.distributed@univeryinli100%Probability distributions - torch.distributions@hijkzzz100%Torch Script@keyianpai100%Multiprocessing package - torch.multiprocessing@hijkzzz100%torch.utils.bottleneck@belonHan100%torch.utils.checkpoint@belonHan100%torch.utils.cpp_extension@belonHan100%torch.utils.data@BXuan694100%torch.utils.dlpack@kunwuz100%torch.hub@kunwuz100%torch.utils.model_zoo@BXuan694100%torch.onnx@guobaoyo100%Distributed communication package (deprecated) - torch.distributed.deprecated torchvision Reference@BXuan694100%torchvision.datasets@BXuan694100%torchvision.models@BXuan694100%torchvision.transforms@BXuan694100%torchvision.utils@BXuan694100%HBase 3.0 中文参考指南参与方式:https://github.com/apachecn/h…整体进度:https://github.com/apachecn/h…项目仓库:https://github.com/apachecn/h…认领:13/31,翻译:9/31章节译者进度Preface@xixici100%Getting Started@xixici100%Apache HBase Configuration@xixici100%Upgrading@xixici100%The Apache HBase Shell@xixici100%Data Model@Winchester-Yi HBase and Schema Design@RaymondCode100%RegionServer Sizing Rules of Thumb HBase and MapReduce@BridgetLai Securing Apache HBase Architecture@RaymondCode In-memory Compaction Backup and Restore Synchronous Replication Apache HBase APIs@xixici100%Apache HBase External APIs@xixici100%Thrift API and Filter Language@xixici100%HBase and Spark@TsingJyujing Apache HBase Coprocessors Apache HBase Performance Tuning Troubleshooting and Debugging Apache HBase Apache HBase Case Studies Apache HBase Operational Management Building and Developing Apache HBase Unit Testing HBase Applications Protobuf in HBase Procedure Framework (Pv2): HBASE-12439 AMv2 Description for Devs ZooKeeper Community Appendix AirFlow 中文文档参与方式:https://github.com/apachecn/a…整体进度:https://github.com/apachecn/a…项目仓库:https://github.com/apachecn/a…认领:25/30,翻译:24/30。章节贡献者进度1 项目@zhongjiajie100%2 协议-100%3 快速开始@ImPerat0R_100%4 安装@Thinking Chen100%5 教程@ImPerat0R_100%6 操作指南@ImPerat0R_100%7 设置配置选项@ImPerat0R_100%8 初始化数据库后端@ImPerat0R_100%9 使用操作器@ImPerat0R_100%10 管理连接@ImPerat0R_100%11 保护连接@ImPerat0R_100%12 写日志@ImPerat0R_100%13 使用Celery扩大规模@ImPerat0R_100%14 使用Dask扩展@ImPerat0R_100%15 使用Mesos扩展(社区贡献)@ImPerat0R_100%16 使用systemd运行Airflow@ImPerat0R_100%17 使用upstart运行Airflow@ImPerat0R_100%18 使用测试模式配置@ImPerat0R_100%19 UI /截图@ImPerat0R_100%20 概念@ImPerat0R_100%21 数据分析@ImPerat0R_100%22 命令行接口@ImPerat0R_100%23 调度和触发器@Ray100%24 插件@ImPerat0R_100%25 安全 26 时区 27 实验性 Rest API@ImPerat0R_100%28 集成 29 Lineage 30 常见问题@zhongjiajie 31 API 参考 OpenCV 4.0 中文文档参与方式:https://github.com/apachecn/o…整体进度:https://github.com/apachecn/o…项目仓库:https://github.com/apachecn/o…认领:47/51,翻译:17/51。章节贡献者进度1. 简介@wstone00111.1 OpenCV-Python教程简介-100%1.2 安装OpenCV—Python-2. GUI功能@ranxx2.1 图像入门-100%2.2 视频入门-100%2.3 绘图功能-100%2.4 鼠标作为画笔-100%2.5 作为调色板的跟踪栏-100%3. 核心操作@luxinfeng3.1 图像基本操作-100%3.2 图像的算术运算-100%3.3 性能测量和改进技术-100%4. 图像处理@friedhelm7394.1 更改颜色空间-100%4.2 图像的几何变换-100%4.3 图像阈值-4.4 平滑图像-4.5 形态转换-4.6 图像梯度-4.7 Canny边缘检测-4.8 影像金字塔-4.9 轮廓-4.10 直方图-4.11 图像转换-4.12 模板匹配-4.13 霍夫线变换-4.14 霍夫圆变换-4.15 基于分水岭算法的图像分割-基于GrabCut算法的交互式前景提取-5. 特征检测和描述@3lackrush5.1 了解功能-100%5.2 Harris角点检测-5.3 Shi-Tomasi角点检测和追踪的良好特征-5.4 SIFT简介(尺度不变特征变换)-5.5 SURF简介(加速鲁棒特性)-5.6 角点检测的FAST算法-5.7 简介(二进制鲁棒独立基本特征)-5.8 ORB(定向快速和快速旋转)-5.9 特征匹配-5.10 特征匹配+ Homography查找对象-6. 视频分析@xmmmmmovo6.1 Meanshift和Camshift-100%6.2 光流-100%6.3 背景减法-100%7. 相机校准和3D重建 7.1 相机校准 7.2 姿势估计 7.3 极线几何 7.4 立体图像的深度图 8. 机器学习@wstone00118.1 K-最近邻-8.2 支持向量机(SVM)-8.3 K-Means聚类-9. 计算摄影@ranxx9.1 图像去噪-9.2 图像修复-9.3 高动态范围(HDR)-10. 目标检测@jiangzhonglian 10.1 使用Haar Cascades进行人脸检测-100%11. OpenCV-Python绑定@daidai21 11.1 OpenCV-Python绑定如何工作?-100%UCB CS61b:Java 中的数据结构参与方式:https://github.com/apachecn/c…整体进度:https://github.com/apachecn/c…项目仓库:https://github.com/apachecn/c…认领:2/12,翻译:1/12。标题译者进度一、算法复杂度@leader402二、抽象数据类型@Allenyep100%三、满足规范 四、序列和它们的实现 五、树 六、搜索树 七、哈希 八、排序和选择 九、平衡搜索 十、并发和同步 十一、伪随机序列 十二、图 UCB Prob140:面向数据科学的概率论参与方式:https://github.com/apachecn/p…整体进度:https://github.com/apachecn/p…项目仓库:https://github.com/apachecn/p…认领:23/25,翻译:19/25。标题译者翻译进度一、基础飞龙100%二、计算几率飞龙100%三、随机变量飞龙100%四、事件之间的关系@biubiubiuboomboomboom100%五、事件集合@PEGASUS1993>0%六、随机计数@viviwong100%七、泊松化@YAOYI626100%八、期望@PEGASUS199350%九、条件(续)@YAOYI626100%十、马尔科夫链喵十八100%十一、马尔科夫链(续)喵十八100%十二、标准差缺只萨摩 100%十三、方差和协方差缺只萨摩 100%十四、中心极限定理喵十八100%十五、连续分布@ThunderboltSmile十六、变换十七、联合密度@Winchester-Yi100%十八、正态和 Gamma 族@Winchester-Yi100%十九、和的分布平淡的天100%二十、估计方法平淡的天100%二十一、Beta 和二项@lvzhetx100%二十二、预测 50%二十三、联合正态随机变量@JUNE951234二十四、简单线性回归@ThomasCai100%二十五、多元回归@lanhaixuan100%翻译征集要求:机器学习/数据科学相关或者编程相关原文必须在互联网上开放不能只提供 PDF 格式(我们实在不想把精力都花在排版上)请先搜索有没有人翻译过请回复本文。赞助我们 ...

March 10, 2019 · 3 min · jiezi

并查集小结

并查集小结并查集(Union-find Sets)是一种非常精巧而实用的数据结构,它主要用于处理一些不相交集合的合并问题。一些常见的用途有求连通子图、求最小生成树的 Kruskal 算法和求最近公共祖先(Least Common Ancestors, LCA)等。下面我们通过分析HDUOJ的一题例题,让我们对并查集有进一步的了解HDUOJ 1272 小希的迷宫Problem Description上次Gardon的迷宫城堡小希玩了很久(见Problem B),现在她也想设计一个迷宫让Gardon来走。但是她设计迷宫的思路不一样,首先她认为所有的通道都应该是双向连通的,就是说如果有一个通道连通了房间A和B,那么既可以通过它从房间A走到房间B,也可以通过它从房间B走到房间A,为了提高难度,小希希望任意两个房间有且仅有一条路径可以相通(除非走了回头路)。小希现在把她的设计图给你,让你帮忙判断她的设计图是否符合她的设计思路。比如下面的例子,前两个是符合条件的,但是最后一个却有两种方法从5到达8。Input输入包含多组数据,每组数据是一个以0 0结尾的整数对列表,表示了一条通道连接的两个房间的编号。房间的编号至少为1,且不超过100000。每两组数据之间有一个空行。 整个文件以两个-1结尾。Output 对于输入的每一组数据,输出仅包括一行。如果该迷宫符合小希的思路,那么输出”Yes”,否则输出”No”。Sample Input6 8 5 3 5 2 6 4 5 6 0 08 1 7 3 6 2 8 9 7 5 7 4 7 8 7 6 0 03 8 6 8 6 4 5 3 5 6 5 2 0 0-1 -1Sample OutputYes Yes No并查集的基本操作有个:findRoot(x):找到元素 x 所在的集合的代表,该操作也可以用于判断两个元素是否位于同一个集合,只要将它们各自的代表比较一下就可以了。unionRoot(x, y):把元素 x 和元素 y 所在的集合合并,要求 x 和 y 所在的集合不相交,如果相交则不合并。find_res(n):判断有多少个连通分量。代码块int Tree[N] ,Node[N]; //Node[N]==1表示存在该节点int flag=1;void initial(){ for(int i=1;i<N;i++){ Tree[i]=i; }}int findRoot(int x){ if(Tree[x]==x) return x; else{ int tmp=findRoot(Tree[x]); Tree[x]=tmp; return tmp; }}void unionRoot(int a,int b){ int x=findRoot(a); int y=findRoot(b); if(x!=y){ Tree[x]=y; }else{ flag=0; }}int find_res(int n){ int res=0; for(int i=1;i<=N;i++){ if(Tree[i]==i && Node[i]) res++; } return res;}int main(){ int a,b; while(scanf("%d%d",&a,&b)!=EOF && a!=-1 && b!=-1){ int n=1; initial(); while(n){ int x,y; scanf("%d%d",&x,&y); if(x!=0 && y!=0){ unionRoot(x,y); Node[x]=1; Node[y]=1; }else{ break; } n++; } if(flag==0){ printf(“NO\n”); } int res=find_res(n); if(res<=1){ printf(“YES\n”); }else{ printf(“NO\n”); } } return 0;}参考资料:http://www.cnblogs.com/cyjb/p… ...

March 9, 2019 · 1 min · jiezi

字典与哈希表 | 自己实现Redis源代码(3)

通过对《Redis设计与实现》一书的学习,我打算动手自己实现一份“Redis源代码”作为自己的学习记录。对Redis感兴趣的同学可以查看我的另一篇文章 造个轮子 | 自己动手写一个Redis。本章介绍的是Redis源代码中的字典及其内部哈希表的实现。字典dict的实现dict的API(1)创建一个新的字典dict dictCreate(dictType type,int hashSize);(2)根据key寻找其在hashTable中对应的结点dictEntry lookup(dict d,void *key);(3)将给定的键值对添加到字典里面(如果键已经存在于字典,那么用新值取代原有的值)bool dictInsert(dict d, void key, void *val);(4)返回给定的键的值void dictFetchValue(dict d, void *key);(5)从字典中删除给定键所对应的键值对void dictDelete(dict d, void key);(6)释放给定字典,以及字典中包含的所有键值对void dictRelease(dict *d);头文件#ifndef __DICT_H#define __DICT_H//哈希表的结点使用dictEntry结构来表示//每个dictEntry结构都保存着一个key-valuetypedef struct dictEntry{ //键 void *key; //值 void *value; //指向下个哈希表结点,形成链表——避免键冲突 struct dictEntry *next;}dictEntry;//保存一组用于操作特定类型键值对的函数typedef struct dictType { //计算哈希值的函数 unsigned int (*hashFunction)(void *key,int size); //复制键的函数 void *(*keyDup)(void *key); //复制值的函数 void *(*valDup)(void *obj); //对比键的函数 int (*keyCompare)(void *key1, void *key2); //销毁键的函数 void (*keyDestructor)(void *key); //销毁值的函数 void (*valDestructor)(void *obj);} dictType;//哈希表typedef struct dictht { //哈希表数组 dictEntry **table; //哈希表大小 int size; //该哈希表已有结点的数量 int used;} dictht;//字典//其实字典就是对普通的哈希表再做一层封装//增加了一些属性typedef struct dict { //类型特定函数 dictType *type; //哈希表 dictht *ht;} dict;//创建一个新的字典//需要传入哈希表的大小dict *dictCreate(dictType type,int hashSize);//根据key寻找其在hashTable中对应的结点dictEntry lookup(dict *d,void *key);//将给定的键值对添加到字典里面//将给定的键值对添加到字典里面,如果键已经存在于字典,//那么用新值取代原有的值bool dictInsert(dict d, void key, void val);//返回给定的键的值void dictFetchValue(dict d, void key);//从字典中删除给定键所对应的键值对void dictDelete(dict d, void key);//释放给定字典,以及字典中包含的所有键值对void dictRelease(dict d);#endifdict API的实现#include <stdio.h>#include <stdlib.h>#include <string.h>#include “dict.h”//哈希表的大小#define HASHSIZE 10//定义对哈希表进行相关操作的函数集//计算哈希值的函数unsigned int myHashFunction(void key,int size){ char charkey=(char)key; unsigned int hash=0; for(;charkey;++charkey){ hash=hash33+charkey; } return hash%size;}//复制键的函数void myKeyDup(void key){ return key;}//复制值的函数void myValDup(void obj){ return obj;}//对比键的函数int myKeyCompare(void key1, void key2){ charcharkey1=(char)key1; charcharkey2=(char)key2; return strcmp(charkey1,charkey2);}//销毁键的函数void myKeyDestructor(void key){ //free(key);}//销毁值的函数void myValDestructor(void obj){ //free(obj);}//创建一个新的字典dict dictCreate(dictType type,int hashSize){ dict d=(dict)malloc(sizeof(dict)); //对hashTable进行相关操作的特定函数集 if(type==NULL){ printf(“PIG Redis WARNING : Type is NULL.\n”); } d->type=type; //哈希表初始化 d->ht=(dictht)malloc(sizeof(dictht)); d->ht->size=hashSize; d->ht->used=0; d->ht->table=(dictEntry)malloc(sizeof(dictEntry)hashSize); //全部结点都设为NULL for(int i=0;i<hashSize;i++){ d->ht->table[i]=NULL; } return d;}//根据key寻找其在hashTable中对应的结点dictEntry lookup(dict d,void key){ dictEntry node; //该key在hashTable中对应的下标 unsigned int index; index=d->type->hashFunction(key,d->ht->size); for(node=d->ht->table[index];node;node=node->next){ if(!(d->type->keyCompare(key,node->key))){ return node; } } return NULL;}//将给定的键值对添加到字典里面bool dictInsert(dict d, void key, void val){ unsigned int index; dictEntry node; if(!(node=lookup(d,key))){ index=d->type->hashFunction(key,d->ht->size); node=(dictEntry)malloc(sizeof(dictEntry)); if(!node)return false; node->key=d->type->keyDup(key); node->next=d->ht->table[index]; d->ht->table[index]=node; } //若存在,直接修改其对应的value值 node->value=d->type->valDup(val); return true;}//返回给定的键的值void dictFetchValue(dict d, void key){ unsigned int index; dictEntry node; //找不到这个结点 if(!(node=lookup(d,key))){ return NULL; } return node->value;}//从字典中删除给定键所对应的键值对void dictDelete(dict d, void key){ dictEntry node; dictEntry temp; //该key在hashTable中对应的下标 unsigned int index; index=d->type->hashFunction(key,d->ht->size); node=d->ht->table[index]; //key相同 if(!(d->type->keyCompare(key,node->key))){ d->ht->table[index]=node->next; d->type->keyDestructor(node->key); d->type->valDestructor(node->value); free(node); return; } temp=node; node=node->next; while(node){ if(!(d->type->keyCompare(key,node->key))){ temp->next=node->next; d->type->keyDestructor(node->key); d->type->valDestructor(node->value); free(node); return; } temp=node; node=node->next; } return;}//释放给定字典,以及字典中包含的所有键值对void dictRelease(dict d){ dictEntry node; dictEntry temp; for(int i=0;i<d->ht->size;i++){ node=d->ht->table[i]; //printf("%d\n",i); while(node){ char t=(char)node->value; //printf("%s\n",t); temp=node; node=node->next; d->type->keyDestructor(temp->key); d->type->valDestructor(temp->value); free(temp); } } free(d->ht); free(d->type); free(d);}/int main(){ dictTypetype=(dictType)malloc(sizeof(dictType)); type->hashFunction=myHashFunction; type->keyDup=myKeyDup; type->valDup=myValDup; type->keyCompare=myKeyCompare; type->keyDestructor=myKeyDestructor; type->valDestructor=myValDestructor; dict d=dictCreate(type,HASHSIZE); charkey1=“sss”; charvalue1=“111”; bool result=dictInsert(d,key1,value1); if(result){ printf(“insert1 success\n”); }else{ printf(“insert1 fail\n”); } charkey2=“3sd”; charvalue2=“ddd”; result=dictInsert(d,key2,value2); if(result){ printf(“insert2 success\n”); }else{ printf(“insert2 fail\n”); } charkey3=“ddds”; charvalue3=“1ss”; result=dictInsert(d,key3,value3); if(result){ printf(“insert3 success\n”); }else{ printf(“insert3 fail\n”); } char value4=(char)dictFetchValue(d,key3); printf("—%s\n",value4); dictDelete(d,key3); value4=(char)dictFetchValue(d,key3); printf("—%s\n",value4); dictRelease(d); system(“pause”); return 0;}/ ...

March 9, 2019 · 2 min · jiezi

【Leetcode】106. 从中序与后序遍历序列构造二叉树2

题目根据一棵树的中序遍历与后序遍历构造二叉树。注意:你可以假设树中没有重复的元素。例如,给出中序遍历 inorder = [9,3,15,20,7]后序遍历 postorder = [9,15,7,20,3]返回如下的二叉树: 3 / \ 9 20 / \ 15 7题解根据前序和中序可以构造一颗二叉树,根据中序和后续也可以构建一颗二叉树。反正必须要有中序才能构建,因为没有中序,你没办法确定树的形状。比如先序和后序是不能构建唯一的一颗二叉树的。例如:先序为:[1, 2]后序为:[2, 1]可以构建如下 1 / 2 1 \ 2 这个面试官也会问的。回到这个题目。那回到这个题目, 其实思路比较好想到,就是如何划分子问题,然后递归的构建左子树和右子树。inorder = [9,3,15,20,7]postorder = [9,15,7,20,3]因为后序后遍历根节点,后续最后一个节点为整棵树的根节点,可以确定根节点为3;再根据中序得到:leftInOrder = [9]RightInOrder = [15, 20 ,7]又由于中序和先序的数组大小应该相同的,所以,LeftPostOrder = [9]RightPostOrder = [15, 7, 20]至此,划分为子问题:leftInOrder = [9]LeftPostOrder = [9]构建左子树。RightPreOrder = [20, 15, 7]RightPostOrder = [15, 7, 20]构建右子树。class Solution { public TreeNode buildTree(int[] inorder, int[] postorder) { return helper(inorder, postorder, postorder.length - 1, 0, inorder.length - 1); } public TreeNode helper(int[] inorder, int[] postorder, int postEnd, int inStart, int inEnd) { if (inStart > inEnd) { return null; } int currentVal = postorder[postEnd]; TreeNode current = new TreeNode(currentVal); int inIndex = 0; for (int i = inStart; i <= inEnd; i++) { if (inorder[i] == currentVal) { inIndex = i; } } TreeNode left = helper(inorder, postorder, postEnd - (inEnd- inIndex) - 1, inStart, inIndex - 1); TreeNode right = helper(inorder, postorder, postEnd - 1, inIndex + 1, inEnd); current.left = left; current.right = right; return current; }}热门阅读技术文章汇总【Leetcode】103. 二叉树的锯齿形层次遍历【Leetcode】102. 二叉树的层次遍历【Leetcode】101. 对称二叉树【Leetcode】100. 相同的树【Leetcode】98. 验证二叉搜索树手撕代码QQ群:805423079, 群密码:1024 ...

March 6, 2019 · 1 min · jiezi

动态字符串SDS的实现 | 自己实现Redis源代码(1)

通过对《Redis设计与实现》一书的学习,我打算动手自己实现一份“Redis源代码”作为自己的学习记录。对Redis感兴趣的同学可以查看我的另一篇文章 造个轮子 | 自己动手写一个Redis。本章介绍的是Redis源代码中的动态字符串SDS的实现。动态字符串SDS的实现SDS的API(1)创建一个包含给定c字符串的sdssds sdsnew(char *);(2)为sds(也就是buf数组)分配指定空间sds sdsnewlen(sds,int);(3)创建一个不包含任何内容的空字符串sds sdsempty(void);(4)释放给定的sdsvoid sdsfree(sds);(5)创建一个给定sds的副本sds sdsdup(sds);(6)清空sds保存的字符串内容sds sdsclear(sds);(7)将给定c字符串拼接到另一个sds字符串的末尾sds sdscat(sds,char *);(8)将给定sds字符串拼接到另一个sds字符串的末尾sds sdscatsds(sds,sds);(9)将给定的c字符串复制到sds里面,覆盖原有的字符串sds sdscpy(sds,char *);(10)保留sds给定区间内的数据sds sdsrange(sds,int,int);(11)从sds中移除所有在c字符串中出现过的字符sds sdstrim(sds,const char );(12)对比两个sds字符串是否相同bool sdscmp(sds,sds);头文件#ifndef SDS_H#define SDS_H//实现Redis中的动态字符串//SDS:simple dynamic stringtypedef struct sdshdr{ //记录buf数组中已使用字节的数量 //等于SDS所保存字符串的长度,不 //包括最后的’\0’; int len; //记录buf数组中未使用字节的数量 int free; //字节数组,用于保存字符串,以 //’\0’结束 char buf;}*sds;//返回sds已使用空间的字节数:lenstatic inline int sdslen(const sds sh){ return sh->len;}//返回sds未使用空间的字节数:freestatic inline int sdsavail(const sds sh){ return sh->free;}//创建一个包含给定c字符串的sdssds sdsnew(char *);//为sds(也就是buf数组)分配指定空间/lensds sdsnewlen(sds,int);//创建一个不包含任何内容的空字符串sds sdsempty(void);//释放给定的sdsvoid sdsfree(sds);//创建一个给定sds的副本sds sdsdup(sds);//清空sds保存的字符串内容sds sdsclear(sds);//将给定c字符串拼接到另一个sds字符串的末尾sds sdscat(sds,char );//将给定sds字符串拼接到另一个sds字符串的末尾sds sdscatsds(sds,sds);//将给定的c字符串复制到sds里面,覆盖原有的字符串sds sdscpy(sds,char );//保留sds给定区间内的数据,不在区间内的数据会被覆盖或清除//s = sdsnew(“Hello World”);//sdsrange(s,1,-1); => “ello World"sds sdsrange(sds,int,int);//接受一个sds和一个c字符串作为参数,从sds中移除所有在c字符串中出现过的字符//s = sdsnew(“AA…AA.a.aa.aHelloWorld :::”);//s = sdstrim(s,“A. :”);//printf("%s\n”, s);//Output will be just “Hello World”.//大小写不敏感sds sdstrim(sds,const char );//对比两个sds字符串是否相同bool sdscmp(sds,sds);#endifSDS API的实现#include <stdio.h>#include <stdlib.h>#include <string.h>#include “sds.h”//创建一个包含给定c字符串的sdssds sdsnew(char init){ sds sh=(sds)malloc(sizeof(struct sdshdr)); sh->len=strlen(init); sh->free=0; sh->buf=(char)malloc(sizeof(char)(sh->len+1)); //将字符串内容进行复制 int i; for(i=0;i<sh->len;i++){ (sh->buf)[i]=init[i]; } (sh->buf)[i]=’\0’; return sh;}//为sds(也就是buf数组)分配指定空间/lensds sdsnewlen(sds sh,int len){ int i; sh->free=len-1-sh->len; //保存之前的buf内容 char str=(char )malloc(sizeof(char)(sh->len+1)); for(i=0; i<(sh->len); i++){ str[i]=sh->buf[i]; } str[i]=’\0’; //sh->buf=(char)realloc(sh->buf,len); sh->buf=(char)malloc(sizeof(char)len); for(i=0; i<(sh->len); i++){ sh->buf[i]=str[i]; } sh->buf[i]=’\0’; free(str); return sh;}//创建一个不包含任何内容的空字符串sds sdsempty(void){ sds sh=(sds)malloc(sizeof(struct sdshdr)); sh->len=0; sh->free=0; sh->buf=(char)malloc(sizeof(char)); sh->buf[0]=’\0’; return sh;}//释放给定的sdsvoid sdsfree(sds sh){ (sh)->free=0; (sh)->len=0; free((sh)->buf); free(sh);}//创建一个给定sds的副本sds sdsdup(sds sh01){ sds sh02=(sds)malloc(sizeof(struct sdshdr)); sh02->free=sh01->free; sh02->len=sh01->len; sh02->buf=(char)malloc(sizeof(char)(sh02->free+sh02->len+1)); int i; for(i=0;i<sh01->len;i++){ sh02->buf[i]=sh01->buf[i]; } sh02->buf[i]=’\0’; return sh02;}//清空sds保存的字符串内容sds sdsclear(sds sh){ int total=sh->len+sh->free+1; sh->len=0; sh->free=total-1; sh->buf[0]=’\0’; return sh;}//将给定c字符串拼接到另一个sds字符串的末尾//先检查sds的空间是否满足修改所需的要求,如//果不满足则自动将sds空间扩展至执行修改所需//要的大小,然后在执行实际的修改操作——防止//缓冲区溢出//扩展空间的原则:拼接后的字符串是n个字节,则//再给其分配n个字节的未使用空间,buf数组的实际长度为n+n+1//当n超过1MB的时候,则为其分配1MB的未使用空间//两个字符串cat,中间使用空格隔开sds sdscat(sds sh,char str){ int newlen=strlen(str); int newfree; //剩余的空间不够cat操作 if(sh->free<=newlen){ //超出部分的空间 newfree=newlen-sh->free; if(newfree<1024){ newfree=newfree+newfree+1+sh->len+sh->free; sh=sdsnewlen(sh,newfree); }else{ newfree=newfree+1024+1+sh->len+sh->free; sh=sdsnewlen(sh,newfree); } } int i; //执行cat操作 sh->buf[sh->len]=’ ‘; for(i=0;i<newlen;i++){ sh->buf[sh->len+i+1]=str[i]; } sh->buf[sh->len+i+1]=’\0’; sh->len+=(newlen+1); sh->free-=newlen; return sh;}//将给定sds字符串拼接到另一个sds字符串的末尾sds sdscatsds(sds sh,sds str){ int newlen=str->len; int newfree; //剩余的空间不够cat操作 if(sh->free<=newlen){ //超出部分的空间 newfree=newlen-sh->free; if(newfree<1024){ newfree=newfree+newfree+1+sh->len+sh->free; sh=sdsnewlen(sh,newfree); }else{ newfree=newfree+1024+1+sh->len+sh->free; sh=sdsnewlen(sh,newfree); } } int i; //执行cat操作 sh->buf[sh->len]=’ ‘; for(i=0;i<newlen;i++){ sh->buf[sh->len+i+1]=str->buf[i]; } sh->buf[sh->len+i+1]=’\0’; sh->len+=(newlen+1); sh->free-=newlen; return sh;}//将给定的c字符串复制到sds里面,覆盖原有的字符串//需要先检查sds sdscpy(sds sh,char str){ //新来的长度 int len=strlen(str); //需要使用到的新空间长度 int newlen=len-sh->len; int total; //剩余的空间不够了需要重新分配,在copy if(newlen>=sh->free){ //新空间长度大于1M,就只多分配newlen+1M+1 //总的空间是len+newlen+1M+1 if(newlen>=1024){ total=len+newlen+1024+1; //copy后使用到的len,就是新字符串的长度 sh->len=len; //空闲的空间长度 //sh->free=total-len-1; //sh->buf=(char)realloc(sh->buf,total); sh=sdsnewlen(sh,total); //分配newlen+newlen+1 }else{ total=len+newlen+newlen+1; sh->len=len; //sh->free=total-len-1; //sh->buf=(char)realloc(sh->buf,total); sh=sdsnewlen(sh,total); } if(sh->buf==NULL){ printf(“PIG Redis ERROR : Realloc failed.\n”); } }else{ //剩余的空间够,不需要分配 //原来拥有的总空间 total=sh->len+sh->free; sh->len=len; sh->free=total-sh->len; } //开始copy int i; for(i=0;i<len;i++){ (sh->buf)[i]=str[i]; } sh->buf[i]=’\0’; return sh;}//保留sds给定区间内的数据,不在区间内的数据会被覆盖或清除//s = sdsnew(“Hello World”);//sdsrange(s,1,-1); => “ello World"sds sdsrange(sds sh,int start,int end){ int newlen=end-start+1; char str=(char)malloc(sizeof(char)(sh->len+1)); //sh1->free=sh->len-sh1->len; int i,j; for(i=start,j=0;i<=end;i++,j++){ str[j]=sh->buf[i]; } str[j]=’\0’; sh->buf=(char)malloc(sizeof(char)(sh->len+1)); sh->free=sh->len-newlen; sh->len=newlen; for(i=0;i<strlen(str);i++){ sh->buf[i]=str[i]; } sh->buf[i]=’\0’; free(str); return sh;}//接受一个sds和一个c字符串作为参数,从sds中移除所有在c字符串中出现过的字符//s = sdsnew(“AA…AA.a.aa.aHelloWorld :::”);//s = sdstrim(s,“A. :”);//printf("%s\n”, s);//Output will be just “Hello World”.//截断操作需要通过内存重分配来释放字符串中不再使用的空间,否则会造成内存泄漏//大小写不敏感//使用惰性空间释放优化字符串的缩短操作,执行缩短操作的时候,不立即使用内存重分//配来回收缩短后多出来的字节,而是使用free属性记录这些字节,等待将来使用sds sdstrim(sds s,const char chstr);//对比两个sds字符串是否相同bool sdscmp(sds sh1,sds sh2){ if(sh1->len!=sh2->len){ return false; } for(int i=0;i<sh1->len;i++){ if(sh1->buf[i]!=sh2->buf[i]){ return false; } } return true;}int main(){ printf(“sdsnew(‘sss’)\n”); sds sh=sdsnew(“sss”); printf("%s\n",sh->buf); printf("%d\n",sh->len); printf("%d\n",sh->free); printf(“sdscat(sh,‘www’)\n”); sh=sdscat(sh,“www”); printf("%s\n",sh->buf); /for(int i=0;i<sh->len;i++){ printf("%c",sh->buf[i]); }/ printf("%d\n",sh->len); printf("%d\n",sh->free); sds sh1=sdsnew(“qqqq”); sh=sdscatsds(sh,sh1); printf("%s\n",sh->buf); printf("%d\n",sh->len); printf("%d\n",sh->free); sh=sdsrange(sh,1,5); printf("%s\n",sh->buf); printf("%d\n",sh->len); printf("%d\n",sh->free); sds sh3=sdsnew(“qqqq”); sds sh4=sdsnew(“qqqq”); if(sdscmp(sh3,sh4)){ printf(“same\n”); }else{ printf(“no same\n”); }/ printf(“sdscpy(sh,‘wwww’)\n”); sh=sdscpy(sh,“wwww”); printf("%s\n",sh->buf); printf("%d\n",sh->len); printf("%d\n",sh->free); printf(“sdsnewlen(sh,12)\n”); sh=sdsnewlen(sh,12); printf("%s\n",sh->buf); printf("%d\n",sh->len); printf("%d\n",sh->free); printf(“sdsdup(sh)\n”); sds sh1=sdsdup(sh); printf("%s\n",sh1->buf); printf("%d\n",sh1->len); printf("%d\n",sh1->free); printf(“sdsclear(sh1)\n”); sh1=sdsclear(sh1); printf("%s\n",sh1->buf); printf("%d\n",sh1->len); printf("%d\n",sh1->free);/ sdsfree(&sh); sdsfree(&sh1); //sdsfree(&sh2); sdsfree(&sh3); sdsfree(&sh4); system(“pause”); return 0;} ...

March 5, 2019 · 2 min · jiezi

【Leetcode】106. 从中序与后序遍历序列构造二叉树

题目根据一棵树的中序遍历与后序遍历构造二叉树。注意:你可以假设树中没有重复的元素。例如,给出中序遍历 inorder = [9,3,15,20,7]后序遍历 postorder = [9,15,7,20,3]返回如下的二叉树: 3 / \ 9 20 / \ 15 7题解根据前序和中序可以构造一颗二叉树,根据中序和后续也可以构建一颗二叉树。反正必须要有中序才能构建,因为没有中序,你没办法确定树的形状。比如先序和后序是不能构建唯一的一颗二叉树的。例如:先序为:[1, 2]后序为:[2, 1]可以构建如下 1 / 2 1 \ 2 这个面试官也会问的。回到这个题目。那回到这个题目, 其实思路比较好想到,就是如何划分子问题,然后递归的构建左子树和右子树。inorder = [9,3,15,20,7]postorder = [9,15,7,20,3]因为后序后遍历根节点,后续最后一个节点为整棵树的根节点,可以确定根节点为3;再根据中序得到:leftInOrder = [9]RightInOrder = [15, 20 ,7]又由于中序和先序的数组大小应该相同的,所以,LeftPostOrder = [9]RightPostOrder = [15, 7, 20]至此,划分为子问题:leftInOrder = [9]LeftPostOrder = [9]构建左子树。RightPreOrder = [20, 15, 7]RightPostOrder = [15, 7, 20]构建右子树。class Solution { public TreeNode buildTree(int[] inorder, int[] postorder) { return helper(inorder, postorder, postorder.length - 1, 0, inorder.length - 1); } public TreeNode helper(int[] inorder, int[] postorder, int postEnd, int inStart, int inEnd) { if (inStart > inEnd) { return null; } int currentVal = postorder[postEnd]; TreeNode current = new TreeNode(currentVal); int inIndex = 0; for (int i = inStart; i <= inEnd; i++) { if (inorder[i] == currentVal) { inIndex = i; } } TreeNode left = helper(inorder, postorder, postEnd - (inEnd- inIndex) - 1, inStart, inIndex - 1); TreeNode right = helper(inorder, postorder, postEnd - 1, inIndex + 1, inEnd); current.left = left; current.right = right; return current; }}热门阅读技术文章汇总【Leetcode】103. 二叉树的锯齿形层次遍历【Leetcode】102. 二叉树的层次遍历【Leetcode】101. 对称二叉树【Leetcode】100. 相同的树【Leetcode】98. 验证二叉搜索树 ...

March 5, 2019 · 1 min · jiezi

【Leetcode】105. 从前序与中序遍历序列构造二叉树

题目根据一棵树的前序遍历与中序遍历构造二叉树。注意:你可以假设树中没有重复的元素。例如,给出前序遍历 preorder = [3,9,20,15,7]中序遍历 inorder = [9,3,15,20,7]返回如下的二叉树: 3 / \ 9 20 / \ 15 7题解根据前序和中序可以构造一颗二叉树,根据中序和后续也可以构建一颗二叉树。反正必须要有中序才能构建,因为没有中序,你没办法确定树的形状。比如先序和后序是不能构建唯一的一颗二叉树的。例如:先序为:[1, 2]后序为:[2, 1]可以构建如下 1 / 2 1 \ 2 这个面试官也会问的。那回到这个题目, 其实思路比较好想到,就是如何划分子问题,然后递归的构建左子树和右子树。preorder = [3,9,20,15,7]inorder = [9,3,15,20,7]因为先序先遍历根节点,可以确定根节点为3;再根据中序得到:leftInOrder = [9]RightInOrder = [15, 20 ,7]又由于中序和先序的数组大小应该相同的,所以,LeftPreOrder = [9]RightPreOrder = [20, 15, 7]至此,划分为子问题:leftInOrder = [9]LeftPreOrder = [9]构建左子树。RightPreOrder = [20, 15, 7]RightInOrder = [15, 20 ,7]构建右子树。/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */class Solution { public TreeNode buildTree(int[] preorder, int[] inorder) { return helper(preorder, inorder, 0, 0, inorder.length - 1); } public TreeNode helper(int[] preorder, int[] inorder, int preStart, int inStart, int inEnd) { if (inStart > inEnd) { return null; } int currentVal = preorder[preStart]; TreeNode current = new TreeNode(currentVal); int inIndex = 0; for (int i = inStart; i <= inEnd; i++) { if (inorder[i] == currentVal) { inIndex = i; } } TreeNode left = helper(preorder, inorder, preStart + 1, inStart, inIndex - 1); TreeNode right = helper(preorder, inorder, preStart + inIndex - inStart + 1, inIndex + 1, inEnd); current.left = left; current.right = right; return current; }}热门阅读技术文章汇总【Leetcode】103. 二叉树的锯齿形层次遍历【Leetcode】102. 二叉树的层次遍历【Leetcode】101. 对称二叉树【Leetcode】100. 相同的树【Leetcode】98. 验证二叉搜索树手撕代码QQ群:805423079, 群密码:1024 ...

March 4, 2019 · 1 min · jiezi

PAT A1098 堆排序

完整的堆排序内容;其中给出了一个思路,就是对于插入排序,使用sort函数会快很多。。。这也算模拟经典排序的一种取巧方式#include<iostream>#include<stdlib.h>#include<stdio.h>#include<vector>#include<algorithm>using namespace std;using std::vector;const int maxn=110;int n;int aim_quence[maxn];int heap[maxn];int input[maxn];int flag=0;bool isSame(int a[],int b[]){ for(int i=1;i<=n;i++){ if(a[i]!=b[i]) return false; } return true;}void insert_sort(){ for(int i=2;i<=n;i++){ sort(input+1,input+i+1); if(flag!=0) break; if(isSame(input,aim_quence)){ flag=1; } }}void downAdujst(int low,int high){ //插入时进行向下调整 int i=low; int j=2i; while(j<=high){ //cout<<11<<endl; if(j+1<=high&&heap[j]<heap[j+1]){ j=j+1; } if(heap[j]>heap[i]){ swap(heap[j],heap[i]); i=j; j=i2; }else{ break; } }}void heapSort(){ for(int i=n;i>1;i–){ swap(heap[i],heap[1]); downAdujst(1,i-1); if(flag!=0) break; if(isSame(heap,aim_quence)){ flag=2; } }}void create(){ for(int i=n/2;i>=1;i–){ downAdujst(i,n); }}int main(){ scanf("%d",&n); for(int i=1;i<=n;i++){ scanf("%d",&input[i]); heap[i]=input[i]; } for(int i=1;i<=n;i++){ scanf("%d",&aim_quence[i]); } insert_sort(); create(); heapSort(); if(flag==1){ printf(“Insertion Sort\n”); for(int i=1;i<=n;i++){ if(i==1) printf("%d",input[i]); else printf(" %d",input[i]); } }else if(flag==2){ printf(“Heap Sort\n”); for(int i=1;i<=n;i++){ if(i==1) printf("%d",heap[i]); else printf(" %d",heap[i]); } } system(“pause”); return 0;} ...

March 1, 2019 · 1 min · jiezi

【Leetcode】104. 二叉树的最大深度

题目给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。说明: 叶子节点是指没有子节点的节点。示例:给定二叉树 [3,9,20,null,null,15,7], 3 / \ 9 20 / \ 15 7返回它的最大深度 3 。题解求最大深度,和深度相关,我们很容易想到用层序遍历。每遍历一层,就深度加1, 怎么记录是第几层我们之前的文章中讲过了。/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */class Solution { public int maxDepth(TreeNode root) { if (root == null) { return 0; } int depth = 0; LinkedList<TreeNode> queue = new LinkedList<>(); queue.add(root); while (!queue.isEmpty()) { int size = queue.size(); while (size > 0) { TreeNode current = queue.poll(); if (current.left != null) { queue.add(current.left); } if (current.right != null) { queue.add(current.right); } size–; } depth++; } return depth; }}这道题用递归解代码比较简单.递归的结束条件: 当节点为叶子节点的时候.递归的子问题: 当前最大深度 = 左右子树最大深度的较大者 + 1代码实现就很简单了。class Solution { public int maxDepth(TreeNode root) { if (root == null) { return 0; } return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; }}热门阅读技术文章汇总【Leetcode】103. 二叉树的锯齿形层次遍历【Leetcode】102. 二叉树的层次遍历【Leetcode】101. 对称二叉树【Leetcode】100. 相同的树【Leetcode】98. 验证二叉搜索树 ...

March 1, 2019 · 1 min · jiezi

【Leetcode】103. 二叉树的锯齿形层次遍历

题目给定一个二叉树,返回其节点值的锯齿形层次遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。例如:给定二叉树 [3,9,20,null,null,15,7], 3 / \ 9 20 / \ 15 7返回锯齿形层次遍历如下:[ [3], [20,9], [15,7]]题解这道题要求用z字型,就是要求知道深度。因为知道深度我们就可以根据深度的奇偶来判断如何打印。首先相到的就是层序遍历,然后记录是第几层。层序遍历用队列的代码我们已经很熟悉了。/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } /class Solution { public List<List<Integer>> zigzagLevelOrder(TreeNode root) { List<List<Integer>> res = new LinkedList<>(); if (root == null) { return res; } LinkedList<TreeNode> queue = new LinkedList<>(); queue.add(root); int depth = 0; while (!queue.isEmpty()) { int size = queue.size(); LinkedList<Integer> currentRes = new LinkedList<>(); // 当前层一直出队. while (size > 0) { TreeNode current = queue.poll(); TreeNode left = current.left; TreeNode right = current.right; if (left != null) { queue.add(left); } if (right != null) { queue.add(right); } // 奇数层,从头添加; 偶数层从尾部添加. if (depth % 2 != 0) { currentRes.add(0, current.val); } else { currentRes.add(current.val); } size–; } // 把当前层遍历的结果加入到结果中. res.add(currentRes); depth++; } return res; }}同之前一样,我们想一想有没有递归的解法.我们可以采用先序遍历的方式,先遍历节点,然后递归的遍历左子树和右子树。稍作改动的是,需要在遍历左子树和右子树的时候带上深度的信息,才能知道是加在列表头还是列表尾部。递归的结束条件就是遇到空节点。/* * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */class Solution { public List<List<Integer>> zigzagLevelOrder(TreeNode root) { List<List<Integer>> res = new LinkedList<>(); if (root == null) { return res; } helper(res, root, 0); return res; } public void helper(List<List<Integer>> res,TreeNode root, int depth) { if (root == null) { return; } // 注意这里new List, 说明当前节点递归进入了新的层. if (res.size() <= depth) { res.add(new LinkedList<>()); } if (depth % 2 != 0) { res.get(depth).add(0, root.val); } else { res.get(depth).add(root.val); } helper(res, root.left, depth + 1); helper(res, root.right, depth + 1); }}热门阅读技术文章汇总【Leetcode】101. 对称二叉树【Leetcode】100. 相同的树【Leetcode】98. 验证二叉搜索树 ...

February 27, 2019 · 2 min · jiezi

【Leetcode】102. 二叉树的层次遍历

题目给定一个二叉树,返回其按层次遍历的节点值。 (即逐层地,从左到右访问所有节点)。例如:给定二叉树: [3,9,20,null,null,15,7], 3 / \ 9 20 / \ 15 7返回其层次遍历结果:[ [3], [9,20], [15,7]]题解我们数据结构的书上教的层序遍历,就是利用一个队列,不断的把左子树和右子树入队。但是这个题目还要要求按照层输出。所以关键的问题是: 如何确定是在同一层的。我们很自然的想到:如果在入队之前,把上一层所有的节点出队,那么出队的这些节点就是上一层的列表。由于队列是先进先出的数据结构,所以这个列表是从左到右的。/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } /class Solution { public List<List<Integer>> levelOrder(TreeNode root) { List<List<Integer>> res = new LinkedList<>(); if (root == null) { return res; } LinkedList<TreeNode> queue = new LinkedList<>(); queue.add(root); while (!queue.isEmpty()) { int size = queue.size(); List<Integer> currentRes = new LinkedList<>(); // 当前队列的大小就是上一层的节点个数, 依次出队 while (size > 0) { TreeNode current = queue.poll(); if (current == null) { continue; } currentRes.add(current.val); // 左子树和右子树入队. if (current.left != null) { queue.add(current.left); } if (current.right != null) { queue.add(current.right); } size–; } res.add(currentRes); } return res; }}这道题可不可以用非递归来解呢?递归的子问题:遍历当前节点, 对于当前层, 遍历左子树的下一层层,遍历右子树的下一层递归结束条件: 当前层,当前子树节点是null/* * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } /class Solution { public List<List<Integer>> levelOrder(TreeNode root) { List<List<Integer>> res = new LinkedList<>(); if (root == null) { return res; } levelOrderHelper(res, root, 0); return res; } /* * @param depth 二叉树的深度 */ private void levelOrderHelper(List<List<Integer>> res, TreeNode root, int depth) { if (root == null) { return; } if (res.size() <= depth) { // 当前层的第一个节点,需要new 一个list来存当前层. res.add(new LinkedList<>()); } // depth 层,把当前节点加入 res.get(depth).add(root.val); // 递归的遍历下一层. levelOrderHelper(res, root.left, depth + 1); levelOrderHelper(res, root.right, depth + 1); }}热门阅读技术文章汇总【Leetcode】101. 对称二叉树【Leetcode】100. 相同的树【Leetcode】98. 验证二叉搜索树 ...

February 26, 2019 · 2 min · jiezi

ApacheCN 翻译活动进度公告 2019.2.25

【主页】 apachecn.org【Github】@ApacheCN暂时下线: 社区暂时下线: cwiki 知识库自媒体平台微博:@ApacheCN知乎:@ApacheCNCSDN简书OSChina博客园我们不是 Apache 的官方组织/机构/团体,只是 Apache 技术栈(以及 AI)的爱好者!合作or侵权,请联系【fonttian】fonttian@gmail.com | 请抄送一份到 apachecn@163.comPyTorch 1.0 中文文档参与方式:https://github.com/apachecn/p…整体进度:https://github.com/apachecn/p…项目仓库:https://github.com/apachecn/p…教程部分:认领:36/37,翻译:29/37;文档部分:认领:34/39,翻译:23/39章节贡献者进度教程部分–Deep Learning with PyTorch: A 60 Minute Blitz@bat67100%What is PyTorch?@bat67100%Autograd: Automatic Differentiation@bat67100%Neural Networks@bat67100%Training a Classifier@bat67100%Optional: Data Parallelism@bat67100%Data Loading and Processing Tutorial@yportne13100%Learning PyTorch with Examples@bat67100%Transfer Learning Tutorial@jiangzhonglian100%Deploying a Seq2Seq Model with the Hybrid Frontend@cangyunye100%Saving and Loading Models@sfyumi What is torch.nn really?@lhc741100%Finetuning Torchvision Models@ZHHAYO100%Spatial Transformer Networks Tutorial@PEGASUS1993100%Neural Transfer Using PyTorch@bdqfork100%Adversarial Example Generation@cangyunye100%Transfering a Model from PyTorch to Caffe2 and Mobile using ONNX@PEGASUS1993100%Chatbot Tutorial@a625687551100%Generating Names with a Character-Level RNN@hhxx2015100%Classifying Names with a Character-Level RNN@hhxx2015100%Deep Learning for NLP with Pytorch@BreezeHavana Introduction to PyTorch@guobaoyo100%Deep Learning with PyTorch@bdqfork100%Word Embeddings: Encoding Lexical Semantics@sight007100%Sequence Models and Long-Short Term Memory Networks@ETCartman100%Advanced: Making Dynamic Decisions and the Bi-LSTM CRF@JohnJiangLA Translation with a Sequence to Sequence Network and Attention@mengfu188100%DCGAN Tutorial@wangshuai9517 Reinforcement Learning (DQN) Tutorial@BreezeHavana Creating Extensions Using numpy and scipy@cangyunye100%Custom C++ and CUDA Extensions@Lotayou Extending TorchScript with Custom C++ Operators Writing Distributed Applications with PyTorch@firdameng PyTorch 1.0 Distributed Trainer with Amazon AWS@yportne13100%ONNX Live Tutorial@PEGASUS1993100%Loading a PyTorch Model in C++@talengu100%Using the PyTorch C++ Frontend@solerji100%文档部分–Autograd mechanics@PEGASUS1993100%Broadcasting semantics@PEGASUS1993100%CUDA semantics@jiangzhonglian100%Extending PyTorch@PEGASUS1993 Frequently Asked Questions@PEGASUS1993100%Multiprocessing best practices@cvley100%Reproducibility@WyattHuang1 Serialization semantics@yuange250100%Windows FAQ@PEGASUS1993100%torch@ZHHAYO torch.Tensor@hijkzzz100%Tensor Attributes@yuange250100%Type Info@PEGASUS1993100%torch.sparse@hijkzzz100%torch.cuda@bdqfork100%torch.Storage@yuange250100%torch.nn@yuange250 torch.nn.functional@hijkzzz100%torch.nn.init@GeneZC100%torch.optim@qiaokuoyuan Automatic differentiation package - torch.autograd@gfjiangly Distributed communication package - torch.distributed Probability distributions - torch.distributions@hijkzzz Torch Script Multiprocessing package - torch.multiprocessing@hijkzzz100%torch.utils.bottleneck@belonHan torch.utils.checkpoint@belonHan torch.utils.cpp_extension@belonHan torch.utils.data@BXuan694 torch.utils.dlpack torch.hub torch.utils.model_zoo@BXuan694100%torch.onnx@guobaoyo100%Distributed communication package (deprecated) - torch.distributed.deprecated torchvision Reference@BXuan694100%torchvision.datasets@BXuan694100%torchvision.models@BXuan694100%torchvision.transforms@BXuan694100%torchvision.utils@BXuan694100%HBase 3.0 中文参考指南参与方式:https://github.com/apachecn/h…整体进度:https://github.com/apachecn/h…项目仓库:https://github.com/apachecn/h…认领:3/31,翻译:1/31章节译者进度Preface Getting Started Apache HBase Configuration Upgrading The Apache HBase Shell Data Model@Winchester-Yi HBase and Schema Design@RaymondCode100%RegionServer Sizing Rules of Thumb HBase and MapReduce Securing Apache HBase Architecture In-memory Compaction Backup and Restore Synchronous Replication Apache HBase APIs Apache HBase External APIs Thrift API and Filter Language HBase and Spark@TsingJyujing Apache HBase Coprocessors Apache HBase Performance Tuning Troubleshooting and Debugging Apache HBase Apache HBase Case Studies Apache HBase Operational Management Building and Developing Apache HBase Unit Testing HBase Applications Protobuf in HBase Procedure Framework (Pv2): HBASE-12439 AMv2 Description for Devs ZooKeeper Community Appendix AirFlow 中文文档参与方式:https://github.com/apachecn/a…整体进度:https://github.com/apachecn/a…项目仓库:https://github.com/apachecn/a…认领:24/30,翻译:24/30。章节贡献者进度1 项目@zhongjiajie100%2 协议-100%3 快速开始@ImPerat0R_100%4 安装@Thinking Chen100%5 教程@ImPerat0R_100%6 操作指南@ImPerat0R_100%7 设置配置选项@ImPerat0R_100%8 初始化数据库后端@ImPerat0R_100%9 使用操作器@ImPerat0R_100%10 管理连接@ImPerat0R_100%11 保护连接@ImPerat0R_100%12 写日志@ImPerat0R_100%13 使用Celery扩大规模@ImPerat0R_100%14 使用Dask扩展@ImPerat0R_100%15 使用Mesos扩展(社区贡献)@ImPerat0R_100%16 使用systemd运行Airflow@ImPerat0R_100%17 使用upstart运行Airflow@ImPerat0R_100%18 使用测试模式配置@ImPerat0R_100%19 UI /截图@ImPerat0R_100%20 概念@ImPerat0R_100%21 数据分析@ImPerat0R_100%22 命令行接口@ImPerat0R_100%23 调度和触发器@Ray100%24 插件@ImPerat0R_100%25 安全 26 时区 27 实验性 Rest API@ImPerat0R_100%28 集成 29 Lineage 30 常见问题 31 API 参考 OpenCV 4.0 中文文档参与方式:https://github.com/apachecn/o…整体进度:https://github.com/apachecn/o…项目仓库:https://github.com/apachecn/o…认领:0/51,翻译:0/51。章节贡献者进度1. 简介 1.1 OpenCV-Python教程简介 1.2 安装OpenCV—Python 2. GUI功能 2.1 图像入门 2.2 视频入门 2.3 绘图功能 2.4 鼠标作为画笔 2.5 作为调色板的跟踪栏 3. 核心操作 3.1 图像基本操作 3.2 图像的算术运算 3.3 性能测量和改进技术 4. 图像处理 4.1 更改颜色空间 4.2 图像的几何变换 4.3 图像阈值 4.4 平滑图像 4.5 形态转换 4.6 图像梯度 4.7 Canny边缘检测 4.8 影像金字塔 4.9 轮廓 4.10 直方图 4.11 图像转换 4.12 模板匹配 4.13 霍夫线变换 4.14 霍夫圆变换 4.15 基于分水岭算法的图像分割 基于GrabCut算法的交互式前景提取 5. 特征检测和描述 5.1 了解功能 5.2 Harris角点检测 5.3 Shi-Tomasi角点检测和追踪的良好特征 5.4 SIFT简介(尺度不变特征变换) 5.5 SURF简介(加速鲁棒特性) 5.6 角点检测的FAST算法 5.7 简介(二进制鲁棒独立基本特征) 5.8 ORB(定向快速和快速旋转) 5.9 特征匹配 5.10 特征匹配+ Homography查找对象 6. 视频分析 6.1 Meanshift和Camshift 6.2 光流 6.3 背景减法 7. 相机校准和3D重建 7.1 相机校准 7.2 姿势估计 7.3 极线几何 7.4 立体图像的深度图 8. 机器学习 8.1 K-最近邻 8.2 支持向量机(SVM) 8.3 K-Means聚类 9. 计算摄影 9.1 图像去噪 9.2 图像修复 9.3 高动态范围(HDR) 10. 目标检测 10.1 使用Haar Cascades进行人脸检测 11. OpenCV-Python绑定 11.1 OpenCV-Python绑定如何工作? UCB CS61b:Java 中的数据结构参与方式:https://github.com/apachecn/c…整体进度:https://github.com/apachecn/c…项目仓库:https://github.com/apachecn/c…认领:0/12,翻译:0/12。标题译者进度一、算法复杂度 二、抽象数据类型 三、满足规范 四、序列和它们的实现 五、树 六、搜索树 七、哈希 八、排序和选择 九、平衡搜索 十、并发和同步 十一、伪随机序列 十二、图 UCB Prob140:面向数据科学的概率论参与方式:https://github.com/apachecn/p…整体进度:https://github.com/apachecn/p…项目仓库:https://github.com/apachecn/p…认领:23/25,翻译:19/25。标题译者翻译进度一、基础飞龙100%二、计算几率飞龙100%三、随机变量飞龙100%四、事件之间的关系@biubiubiuboomboomboom100%五、事件集合@PEGASUS1993>0%六、随机计数@viviwong100%七、泊松化@YAOYI626100%八、期望@PEGASUS199350%九、条件(续)@YAOYI626100%十、马尔科夫链喵十八100%十一、马尔科夫链(续)喵十八100%十二、标准差缺只萨摩 100%十三、方差和协方差缺只萨摩 100%十四、中心极限定理喵十八100%十五、连续分布@ThunderboltSmile十六、变换十七、联合密度@Winchester-Yi100%十八、正态和 Gamma 族@Winchester-Yi100%十九、和的分布平淡的天100%二十、估计方法平淡的天100%二十一、Beta 和二项@lvzhetx100%二十二、预测@lvzhetx50%二十三、联合正态随机变量二十四、简单线性回归@ThomasCai100%二十五、多元回归@lanhaixuan100%翻译征集要求:机器学习/数据科学相关或者编程相关原文必须在互联网上开放不能只提供 PDF 格式(我们实在不想把精力都花在排版上)请先搜索有没有人翻译过请回复本文。赞助我们 ...

February 25, 2019 · 3 min · jiezi

【Leetcode】101. 对称二叉树

题目给定一个二叉树,检查它是否是镜像对称的。例如,二叉树 [1,2,2,3,4,4,3] 是对称的。 1 / \ 2 2 / \ / \3 4 4 3但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的: 1 / \ 2 2 \ \ 3 3题解还记得我们上几次说过,二叉树的题目,大多数可以用递归解决。而递归主要确定两点:递归的子问题是什么;递归的结束条件是什么这个题这两点都不难找到,直接看代码吧。/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */class Solution { public boolean isSymmetric(TreeNode root) { return root == null || isSymmetricHelp(root.left, root.right); } private boolean isSymmetricHelp(TreeNode left, TreeNode right) { if (left == null || right == null) return left == right; if (left.val != right.val) return false; return isSymmetricHelp(left.left, right.right) && isSymmetricHelp(left.right, right.left); }}这个题目还要求用非递归的方式去解.一种很直观的想法就是利用层序遍历,看它是不是对称的.入队顺序依次是, 左子树的左儿子,右子树的右儿子左子树的右儿子,右子树左右儿子。这样出队的时候两两检查是不是对称。public boolean isSymmetric(TreeNode root) { Queue<TreeNode> q = new LinkedList<TreeNode>(); if(root == null) return true; q.add(root.left); q.add(root.right); while(!q.isEmpty()){ TreeNode left = q.poll(); TreeNode right = q.poll(); // 叶子节点. if(left== null&& right == null) continue; // 其中一个为null 肯定不是 if(left == null ^ right == null) return false; // 值不相同 if(left.val != right.val) return false; q.add(left.left); q.add(right.right); q.add(left.right); q.add(right.left); } return true; }热门阅读技术文章汇总【Leetcode】100. 相同的树【Leetcode】98. 验证二叉搜索树【Leetcode】95~96 不同的二叉搜索树 ...

February 25, 2019 · 1 min · jiezi

JS数据结构学习:链表

在存储多个元素时,我们最常用的数据结构可能是数组,究其原因可能是数组访问方便,可以直接通过[]访问,但是数组也存在一定的缺点,数组的大小是固定,数组在执行插入或者删除的时候成本很高。链表存储的是有序的元素集合,和数组不同的是,链表中的元素在内存并不是连续放置,每个元素由一个存储元素本身的节点和一个指向下一个元素的引用组成,结构如下图所示:和数组相比,链表的优势在于:添加或者删除元素不需要移动其他元素,劣势在与链表相对于数组结构更复杂,需要一个指向下一个元素的指针,在访问链表中的某个元素也需要从头迭代,而不是像数组一样直接访问链表的创建首先让我们来看一下链表的大概骨架,需要定义一些什么属性:function LinkedList() { let Node = function (element) { this.element = element this.next = null } let length = 0 let head = null this.append = function (element) { } this.insert = function (position, element) { } this.removeAt = function (position) { } this.remove = function (element) { } this.indexOf = function (element) { } this.isEmpty = function () { } this.size = function () { } this.getHead = function () { } this.toString = function () { } this.print = function () { }}首先LinkedList里面需要定义一个Node辅助类,表示要加入链表的项,包含一个element属性和一个指向下一个节点的指针next,接下来定义了一个指针的头head和指针的长度length,然后就是最重要的类里面的方法,接下来就让我们一起来看这些方法的职责和实现append(向链表尾部追加元素)链表在向尾部追加元素的时候有两种情况:链表为空,添加的是第一个元素,或者链表不为空,追加元素,下面来看具体实现:this.append = function (element) { let node = new Node(element), current if (head === null) { head = node // 链表为空时,插入 } else { current = node while (current.next) { // 循环链表,直到最后一项 current = current.next } current.next = node // 找到最后一项,将新增元素连接 } length++ }现在让我们先来看第一个种情况,链表为空时,插入就直接是头,因此直接将node赋值给head就行,第二种情况,当链表有元素时,就需要先循环链表,找到最后一项,然后将最后一项的next指针指向noderemoveAt(移除元素)现在让我们来看看怎么从指定位置移除元素,移除元素也有两个场景,第一种是移除第一个元素,第二种是移除第一个以外的任意一个元素,来看具体实现:this.removeAt = function (position) { if (position > -1 && position < length) { // 判断边界 let previous, index = 0, current = head if (position === 0) { // 移除第一项 head = current.next } else { while (index++ < position) { previous = current current = current.next } previous.next = current.next } length– return current.element } else { return null } }接下来一起来分析一下上面的代码,首先判断要删除的位置是不是有效的位置,然后来看第一种情况,当移除的元素是第一项是时,此时直接将head指向第二个元素就行了,第二种情况就会稍微复杂一点,首先需要一个index控制递增,previous记录前一个位置,移除当前元素,就是将前一个元素的next指向下一个元素,来看一个示意图:因此在while循环中始终用previous记录上一个位置元素,current记录下一个元素,跳出循环时,上一个元素的next指针指向当前元素的next指针指向的元素,就将当前元素移出链表insert(任意位置插入)接下来来看在任意位置插入的insert方法,这个方法同样需要考虑两种情况,插入位置在头部和插入位置不在头部,下面来看一下具体实现:this.insert = function (position, element) { if (position > -1 && position <= length) { let node = new Node(element),previous, index = 0, current = head if (position === 0) { node.next = current head = node } else { while (index++ < position) { previous = current current = current.next } previous.next = node node.next = current } length++ return true } else { return false } }先来看第一种情况,链表起点添加一个元素,将node.next指向current,然后再将node的引用赋值给head,这样就再链表的起点添加了一个元素,第二种情况,在其他位置插入一个元素,previous是插入元素的前一个元素,current为插入元素的后一个元素,想要插入一个元素,就需要将前一个元素的next指向要插入的元素,要插入元素的next指向下一个元素,来看示意图:如上图所示:将新项node插入到previous和current之间,需要将previous.next指向node,node.next指向current,这样就在链表中插入了一个新的项toStringtoString方法会把LinkedList对象转换成一个字符串,下面来看具体实现:this.toString = function () { let current = head, string = ’’ while (current) { string += current.element + (current.next ? ’n’ : ‘’) current = current.next } return string }循环遍历所有元素,以head为起点,当存在下一个元素时,就将其拼接到字符串中,直到next为nullindexOfindexOf方法返回对应元素的位置,存在就返回对应的索引,不存在返回-1,来看具体的实现:this.indexOf = function (element) { let current = head, index = 0 while (current) { if (current.element === element) { return index } index++ current = current.next } return -1 }遍历链表,当前元素的值与目标值一致时返回元素的位置index,遍历完链表还没找到则返回-1remove、isEmpty、size、getHead由于这几个方法实现比较简单,直接来看具体实现:this.remove = function (element) { // 移除指定元素 let index = this.indexOf(element) return this.removeAt(index) }this.isEmpty = function () { // 判断链表是否为空 return length === 0 }this.size = function () { // 获取链表长度 return length }this.getHead = function () { // 获取链表头 return head }总结这篇文章主要对链表做了简单介绍,对链表的简单实现。如果有错误或不严谨的地方,欢迎批评指正,如果喜欢,欢迎点赞。 ...

February 23, 2019 · 2 min · jiezi

【Leetcode】60. 第k个排列

题目给出集合 [1,2,3,…,n],其所有元素共有 n! 种排列。按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下:“123"“132"“213"“231"“312"“321"给定 n 和 k,返回第 k 个排列。说明:给定 n 的范围是 [1, 9]。给定 k 的范围是[1, n!]。示例 1:输入: n = 3, k = 3输出: “213"示例 2:输入: n = 4, k = 9输出: “2314"题解这道题还蛮有意思的,我首先一看,这不是backtrack的经典题目吗? backtrack的剪枝可以参看相关文章中有详细的step-by-step的流程.从小到大把数排好;用backtrack的方法遍历,每次遍历到一个全排列那么结果,count+1;遍历到n就return输出由于用的是backtrack,后面 count > k的情况都直接return掉;然后用java写了一个版本,还没有剪枝就ac啦.class Solution {int count = 0; List<Integer> finalRes; public String getPermutation(int n, int k) { int[] nums = new int[n]; for (int i = 0; i < n; i++) { nums[i] = i + 1; } //第几个解. List<Integer> resTemp = new ArrayList<>(); boolean[] haveSeen = new boolean[n]; backtrack(nums, k, resTemp, haveSeen); StringBuilder res = new StringBuilder(); for (Integer i : finalRes) { res.append(i); } return res.toString(); } public void backtrack(int[] nums, int k, List<Integer> tempIndex, boolean[] haveSeen) { if (tempIndex.size() == nums.length) { count++; } if (count == k && tempIndex.size() == nums.length) { finalRes = new ArrayList<>(tempIndex); return; } else if (count < k && tempIndex.size() == nums.length) { tempIndex = new ArrayList<>(); } for (int i = 0; i < nums.length; i++) { if (haveSeen[i]) { continue; } tempIndex.add(nums[i]); haveSeen[i] = true; backtrack(nums, k, tempIndex, haveSeen); haveSeen[i] = false; tempIndex.remove(tempIndex.size() - 1); } }}由于前几天后台有同学反馈,希望给出java版本的题解。所以又动手写了一个python版本.class Solution: def getPermutation(self, n, k): "”” :type n: int :type k: int :rtype: str "”” global count global finalRes count = 0 finalRes = [] def backtrack(nums, k, resTemp, haveSeen): global count global finalRes if count > k: return if len(resTemp) == len(nums): count += 1 if count == k and len(resTemp) == len(nums): finalRes = [str() for _ in resTemp] return elif count < k and len(resTemp) == len(nums): resTemp = [] for i in range(0, len(nums)): if count > k: break if haveSeen[i]: continue resTemp.append(nums[i]) haveSeen[i] = True backtrack(nums, k, resTemp, haveSeen) haveSeen[i] = False resTemp.pop() backtrack([ + 1 for _ in range(0, n)], k, [], [False for _ in range(0, n)]) return “".join(finalRes)后来这个版本提交的时候我以为可以洗洗睡啦.结果,卧槽,居然换一种语言就超时啦~~这倒是个意外.难道我的python写的有性能问题,不应该啊,不是:人生苦短,我用python我就继续想剪枝,还能怎么剪枝?剪枝是啥,不就是跳过某些步骤吗?那哪些步骤可以跳过呢.4的全排列是: 1 + {2,3,4}全排列2 + {1,3,4}全排列3 + {1,2,4}全排列4 + {1,2,3}全排列似乎可以转化成子问题啊.由于题目只要求出第几个,我们再看看个数的规律1 + {2,3,4}全排列(3!个)2 + {1,3,4}全排列(3!个)3 + {1,2,4}全排列(3!个)4 + {1,2,3}全排列(3!个)这就很好了呀~具体来说是:n 个数字有 n!种全排列,每种数字开头的全排列有 (n - 1)!种。所以用 k / (n - 1)! 就可以得到第 k 个全排列是以第几个数字开头的。用 k % (n - 1)! 就可以得到第 k 个全排列是某个数字开头的全排列中的第几个。数学之美啊,有木有。然后就快速实现了python的code AC了.class Solution: def getPermutation(self, n, k): nums = [str(_ + 1) for _ in range(0, n)] if k == 1: return “".join(nums) fact = 1 for i in range(2, n): fact *= i round = n - 1 k -= 1 finalRes = [] while round >= 0: index = int(k / fact) k %= fact finalRes.append(nums[index]) nums.remove(nums[index]) if round > 0: fact /= round round -= 1 return “".join(finalRes)每日英文pulicity (n.) 曝光度,知名度enhanace the ~ of yourself 提高自己的知名度publication (n.) 刊物,发表Publicize (v.) 宣传issue (v.) 发表People’s Republic Of Chinain publicRepublicans (n.)共和主义者mass (n.)群众 (v.)聚集 (a.集中的)masses of = manycivilian (a.)平民civil law 民法相关阅读46.全排列47. 全排列 II ...

February 22, 2019 · 3 min · jiezi

Tensorflow源码解析3 -- TensorFlow核心对象 - Graph

1 Graph概述计算图Graph是TensorFlow的核心对象,TensorFlow的运行流程基本都是围绕它进行的。包括图的构建、传递、剪枝、按worker分裂、按设备二次分裂、执行、注销等。因此理解计算图Graph对掌握TensorFlow运行尤为关键。2 默认Graph默认图替换之前讲解Session的时候就说过,一个Session只能run一个Graph,但一个Graph可以运行在多个Session中。常见情况是,session会运行全局唯一的隐式的默认的Graph,operation也是注册到这个Graph中。也可以显示创建Graph,并调用as_default()使他替换默认Graph。在该上下文管理器中创建的op都会注册到这个graph中。退出上下文管理器后,则恢复原来的默认graph。一般情况下,我们不用显式创建Graph,使用系统创建的那个默认Graph即可。print tf.get_default_graph()with tf.Graph().as_default() as g: print tf.get_default_graph() is g print tf.get_default_graph()print tf.get_default_graph()输出如下<tensorflow.python.framework.ops.Graph object at 0x106329fd0>True<tensorflow.python.framework.ops.Graph object at 0x18205cc0d0><tensorflow.python.framework.ops.Graph object at 0x10d025fd0>由此可见,在上下文管理器中,当前线程的默认图被替换了,而退出上下文管理后,则恢复为了原来的默认图。默认图管理默认graph和默认session一样,也是线程作用域的。当前线程中,永远都有且仅有一个graph为默认图。TensorFlow同样通过栈来管理线程的默认graph。@tf_export(“Graph”)class Graph(object): # 替换线程默认图 def as_default(self): return _default_graph_stack.get_controller(self) # 栈式管理,push pop @tf_contextlib.contextmanager def get_controller(self, default): try: context.context_stack.push(default.building_function, default.as_default) finally: context.context_stack.pop()替换默认图采用了堆栈的管理方式,通过push pop操作进行管理。获取默认图的操作如下,通过默认graph栈_default_graph_stack来获取。@tf_export(“get_default_graph”)def get_default_graph(): return _default_graph_stack.get_default()下面来看_default_graph_stack的创建_default_graph_stack = _DefaultGraphStack()class _DefaultGraphStack(_DefaultStack): def init(self): # 调用父类来创建 super(_DefaultGraphStack, self).init() self._global_default_graph = None class _DefaultStack(threading.local): def init(self): super(_DefaultStack, self).init() self._enforce_nesting = True # 和默认session栈一样,本质上也是一个list self.stack = []_default_graph_stack的创建如上所示,最终和默认session栈一样,本质上也是一个list。3 前端Graph数据结构Graph数据结构理解一个对象,先从它的数据结构开始。我们先来看Python前端中,Graph的数据结构。Graph主要的成员变量是Operation和Tensor。Operation是Graph的节点,它代表了运算算子。Tensor是Graph的边,它代表了运算数据。@tf_export(“Graph”)class Graph(object): def init(self): # 加线程锁,使得注册op时,不会有其他线程注册op到graph中,从而保证共享graph是线程安全的 self._lock = threading.Lock() # op相关数据。 # 为graph的每个op分配一个id,通过id可以快速索引到相关op。故创建了_nodes_by_id字典 self._nodes_by_id = dict() # GUARDED_BY(self._lock) self._next_id_counter = 0 # GUARDED_BY(self._lock) # 同时也可以通过name来快速索引op,故创建了_nodes_by_name字典 self._nodes_by_name = dict() # GUARDED_BY(self.lock) self.version = 0 # GUARDED_BY(self.lock) # tensor相关数据。 # 处理tensor的placeholder self.handle_feeders = {} # 处理tensor的read操作 self.handle_readers = {} # 处理tensor的move操作 self.handle_movers = {} # 处理tensor的delete操作 self.handle_deleters = {}下面看graph如何添加op的,以及保证线程安全的。 def add_op(self, op): # graph被设置为final后,就是只读的了,不能添加op了。 self.check_not_finalized() # 保证共享graph的线程安全 with self.lock: # 将op以id和name分别构建字典,添加到_nodes_by_id和_nodes_by_name字典中,方便后续快速索引 self.nodes_by_id[op.id] = op self.nodes_by_name[op.name] = op self.version = max(self.version, op.id)GraphKeys 图分组每个Operation节点都有一个特定的标签,从而实现节点的分类。相同标签的节点归为一类,放到同一个Collection中。标签是一个唯一的GraphKey,GraphKey被定义在类GraphKeys中,如下@tf_export(“GraphKeys”)class GraphKeys(object): GLOBAL_VARIABLES = “variables” QUEUE_RUNNERS = “queue_runners” SAVERS = “savers” WEIGHTS = “weights” BIASES = “biases” ACTIVATIONS = “activations” UPDATE_OPS = “update_ops” LOSSES = “losses” TRAIN_OP = “train_op” # 省略其他name_scope 节点命名空间使用name_scope对graph中的节点进行层次化管理,上下层之间通过斜杠分隔。# graph节点命名空间g = tf.get_default_graph()with g.name_scope(“scope1”): c = tf.constant(“hello, world”, name=“c”) print c.op.name with g.name_scope(“scope2”): c = tf.constant(“hello, world”, name=“c”) print c.op.name输出如下scope1/cscope1/scope2/c # 内层的scope会继承外层的,类似于栈,形成层次化管理4 后端Graph数据结构Graph先来看graph.h文件中的Graph类的定义,只看关键代码 class Graph { private: // 所有已知的op计算函数的注册表 FunctionLibraryDefinition ops; // GraphDef版本号 const std::unique_ptr<VersionDef> versions; // 节点node列表,通过id来访问 std::vector<Node*> nodes; // node个数 int64 num_nodes = 0; // 边edge列表,通过id来访问 std::vector<Edge*> edges; // graph中非空edge的数目 int num_edges = 0; // 已分配了内存,但还没使用的node和edge std::vector<Node*> free_nodes; std::vector<Edge*> free_edges; }后端中的Graph主要成员也是节点node和边edge。节点node为计算算子Operation,边为算子所需要的数据,或者代表节点间的依赖关系。这一点和Python中的定义相似。边Edge的持有它的源节点和目标节点的指针,从而将两个节点连接起来。下面看Edge类的定义。Edgeclass Edge { private: Edge() {} friend class EdgeSetTest; friend class Graph; // 源节点, 边的数据就来源于源节点的计算。源节点是边的生产者 Node* src; // 目标节点,边的数据提供给目标节点进行计算。目标节点是边的消费者 Node* dst; // 边id,也就是边的标识符 int id; // 表示当前边为源节点的第src_output_条边。源节点可能会有多条输出边 int src_output; // 表示当前边为目标节点的第dst_input_条边。目标节点可能会有多条输入边。 int dst_input;};Edge既可以承载tensor数据,提供给节点Operation进行运算,也可以用来表示节点之间有依赖关系。对于表示节点依赖的边,其src_output, dst_input_均为-1,此时边不承载任何数据。下面来看Node类的定义。Nodeclass Node { public: // NodeDef,节点算子Operation的信息,比如op分配到哪个设备上了,op的名字等,运行时有可能变化。 const NodeDef& def() const; // OpDef, 节点算子Operation的元数据,不会变的。比如Operation的入参列表,出参列表等 const OpDef& op_def() const; private: // 输入边,传递数据给节点。可能有多条 EdgeSet in_edges; // 输出边,节点计算后得到的数据。可能有多条 EdgeSet out_edges;}节点Node中包含的主要数据有输入边和输出边的集合,从而能够由Node找到跟他关联的所有边。Node中还包含NodeDef和OpDef两个成员。NodeDef表示节点算子的信息,运行时可能会变,创建Node时会new一个NodeDef对象。OpDef表示节点算子的元信息,运行时不会变,创建Node时不需要new OpDef,只需要从OpDef仓库中取出即可。因为元信息是确定的,比如Operation的入参个数等。由Node和Edge,即可以组成图Graph,通过任何节点和任何边,都可以遍历完整图。Graph执行计算时,按照拓扑结构,依次执行每个Node的op计算,最终即可得到输出结果。入度为0的节点,也就是依赖数据已经准备好的节点,可以并发执行,从而提高运行效率。系统中存在默认的Graph,初始化Graph时,会添加一个Source节点和Sink节点。Source表示Graph的起始节点,Sink为终止节点。Source的id为0,Sink的id为1,其他节点id均大于1.5 Graph运行时生命周期Graph是TensorFlow的核心对象,TensorFlow的运行均是围绕Graph进行的。运行时Graph大致经过了以下阶段图构建:client端用户将创建的节点注册到Graph中,一般不需要显示创建Graph,使用系统创建的默认的即可。图发送:client通过session.run()执行运行时,将构建好的整图序列化为GraphDef后,传递给master图剪枝:master先反序列化拿到Graph,然后根据session.run()传递的fetches和feeds列表,反向遍历全图full graph,实施剪枝,得到最小依赖子图。图分裂:master将最小子图分裂为多个Graph Partition,并注册到多个worker上。一个worker对应一个Graph Partition。图二次分裂:worker根据当前可用硬件资源,如CPU GPU,将Graph Partition按照op算子设备约束规范(例如tf.device(’/cpu:0’),二次分裂到不同设备上。每个计算设备对应一个Graph Partition。图运行:对于每一个计算设备,worker依照op在kernel中的实现,完成op的运算。设备间数据通信可以使用send/recv节点,而worker间通信,则使用GRPC或RDMA协议。这些阶段根据TensorFlow运行时的不同,会进行不同的处理。运行时有两种,本地运行时和分布式运行时。故Graph生命周期到后面分析本地运行时和分布式运行时的时候,再详细讲解。本文作者:扬易阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 22, 2019 · 2 min · jiezi

PAT A1094

水题,没有什么好说的#include<iostream>#include<stdlib.h>#include<stdio.h>#include<vector>#include<queue>using namespace std;using std::vector;using std::queue;const int maxn=110;vector<int> mem[maxn];void bfs(int x,int& level,int& num){ queue<int>q; q.push(x); int l=0; while(q.size()!=0){ l++; int len=q.size(); for(int i=0;i<len;i++){ int a=q.front(); q.pop(); for(int j=0;j<mem[a].size();j++){ q.push(mem[a][j]); } } if(len>num){ num=len; level=l; } }}int main(){ int n,m; int a,b; int k; scanf("%d%d",&n,&m); for(int i=0;i<m;i++){ scanf("%d",&a); scanf("%d",&k); for(int i=0;i<k;i++){ scanf("%d",&b); mem[a].push_back(b); } } //01根节点; int num=0; int level=0; bfs(1,level,num); printf("%d %d",num,level); system(“pause”);}

February 21, 2019 · 1 min · jiezi

PAT A1142

题目大意:给出一个无向图,并且给出一个点集,要求内部每个点两两直接相连;并且后面要判定是不是最大的满足这个点集的集合;#include<iostream>#include<stdlib.h>#include<stdio.h>#include<vector>using namespace std;using std::vector;const int maxn=210;int mem[maxn][maxn]={0};int main(){ int n,m; int a,b,c; scanf("%d%d",&n,&m); for(int i=0;i<m;i++){ scanf("%d",&a); scanf("%d",&b); mem[a][b]=mem[b][a]=1; } int query_num; scanf("%d",&query_num); for(int ii=0;ii<query_num;ii++){ scanf("%d",&a); bool flag=true; vector<int>query; bool vis[maxn]={false}; for(int j=0;j<a;j++){ scanf("%d",&c); query.push_back(c); vis[c]=true; } //元素输入完毕; for(int i=0;i<query.size()-1;i++){ for(int j=i+1;j<query.size();j++){ if(mem[query[i]][query[j]]==0) flag=false; } } //判断输入元素是不是集合; if(!flag){ cout<<“Not a Clique”<<endl; continue; } int f=false; for(int i=1;i<=n;i++){ if(!vis[i]){ //该节点不在集合i; //cout<<“point:"<<i<<endl; for(int j=0;j<query.size();j++){ if(mem[query[j]][i]==0) break; if(j==query.size()-1) f=true; } } } if(f){ cout<<“Not Maximal”<<endl; }else{ cout<<“Yes”<<endl; } } system(“pause”); return 0;} ...

February 21, 2019 · 1 min · jiezi

PAT A1119 前序遍历后续遍历不唯一生成

之前仅仅接触过如何通过二叉树的中序+先序/后序序列生成唯一二叉树,这一次见到了这个新的题型;这里先梳理一个概念,之所以会生成树不唯一,一定是有一个叶子,无论其在父节点的左右子节点,都可能生成相同的先序和后序遍历序列;所以这个时候,思路就很清晰,我们判别一个序列是否唯一,条件就是是否有一个节点只有一个子节点;大致的序列分割和先序和后续相同,这个后面专门开一个blog进行总结;#include<iostream>#include<stdlib.h>#include<stdio.h>#include<vector>#include<set>#include<algorithm>using namespace std;using std::vector;using std::set;vector<int>in,pre,post;bool uniq=true;void chargement(int prl,int prr,int pol,int por){ if(prl==prr){ in.push_back(pre[prl]); return ; } if(pre[prl]==post[por]){ int i=prl+1; while(i<=prr&&pre[i]!=post[por-1]) i++; if(i-prl>1) chargement(prl+1,i-1,pol,pol+(i-prl-1)-1); else uniq=false; in.push_back(post[por]); chargement(i,prr,pol+(i-prl-1),por-1); }}int main(){ int n; scanf("%d",&n); pre.resize(n); post.resize(n); for(int i=0;i<n;i++) scanf("%d",&pre[i]); for(int i=0;i<n;i++) scanf("%d",&post[i]); chargement(0,n-1,0,n-1); printf("%s\n%d", uniq == true ? “Yes” : “No”, in[0]); for (int i = 1; i < in.size(); i++) printf(" %d", in[i]); printf("\n"); system(“pause”); return 0;}

February 21, 2019 · 1 min · jiezi

消息通知系统模型设计

本篇主要明确消息通知系统的概念和具体实现,包括数据库设计、技术方案、逻辑关系分析等。消息通知系统是一个比较复杂的系统,这里主要分析站内消息如何设计和实现。我们常见的消息推送渠道有以下几种:设备推送站内推送短信推送邮箱推送我们常见的站内通知有以下几种类别:公告 Announcement提醒 Remind资源订阅提醒「我关注的资源有更新、评论等事件时通知我」资源发布提醒「我发布的资源有评论、收藏等事件时通知我」系统提醒「平台会根据一些算法、规则等可能会对你的资源做一些事情,这时你会收到系统通知」私信 Mailbox以上三种消息有各自特点,实现也各不相同,其中「提醒」类通知是最复杂的,下面会详细讲。数据模型设计公告公告是指平台发送一条含有具体内容的消息,站内所有用户都能收到这条消息。方案一:【适合活跃用户在5万左右】 公告表「notify_announce」 表结构如下:id: {type: ‘integer’, primaryKey: true, autoIncrement:true} //公告编号;senderID: {type: ‘string’, required: true} //发送者编号,通常为系统管理员;title: {type: ‘string’, required: true} //公告标题;content: {type: ’text’, required: true} //公告内容;createdAt: {type: ’timestamp’, required: true} //发送时间;用户公告表「notify_announce_user」 表结构如下:id: {type: ‘integer’, primaryKey: true, autoIncrement:true} //用户公告编号;announceID: {type: ‘integer’} //公告编号;recipientID: {type: ‘string’, required: true} //接收用户编号;createdAt:{type: ’timestamp’, required: true} //拉取公告时间;state: {type: ‘integer’, required: true} //状态,已读|未读;readAt:{type: ’timestamp’, required: true} //阅读时间;平台发布一则公告之后,当用户登录的时候去拉取站内公告并插入notify_announce_user表,这样那些很久都没登陆的用户就没必要插入了。「首次拉取,根据用户的注册时间;否则根据notify_announce_user.createdAt即上一次拉取的时间节点获取公告」方案二:【适合活跃用户在百万-千万左右】 和方案一雷同,只是需要把notify_announce_user表进行哈希分表,需事先生成表:notify_announce_<hash(uid)>。 用户公告表「notify_announce_<hash(uid)>」 表结构如下:id: {type: ‘integer’, primaryKey: true, autoIncrement:true} //用户公告编号;announceID: {type: ‘integer’} //公告编号;recipientID: {type: ‘string’, required: true} //接收用户编号;createdAt:{type: ’timestamp’, required: true} //拉取公告时间;state: {type: ‘integer’, required: true} //状态,已读|未读;readAt:{type: ’timestamp’, required: true} //阅读时间;提醒提醒是指「我的资源」或「我关注的资源」有新的动态产生时通知我。提醒的内容无非就是: 「someone do something in someone’s something」 「谁对一样属于谁的事物做了什么操作」 常见的提醒消息例子,如: XXX 关注了你 - 「这则属于资源发布提醒」 XXX 喜欢了你的文章 《消息通知系统模型设计》 - 「这则属于资源发布提醒」 你喜欢的文章《消息通知系统模型设计》有新的评论 - 「这则属于资源订阅提醒」 你的文章《消息通知系统模型设计》已被加入专题 《系统设计》 - 「这则属于系统提醒」 小明赞同了你的回答 XXXXXXXXX -「这则属于资源发布提醒」最后一个例子中包含了消息的生产者(小明),消息记录的行为(赞同),行为的对象(你的回答内容)分析提醒类消息的句子结构: someone = 动作发起者,标记为「sender」 do something = 对资源的操作,如:评论、喜欢、关注都属于一个动作,标记为「action」 something = 被作用对象,如:一篇文章,文章的评论等,标记为「object」 someone’s = 动作的目标对象或目标资源的所有者,标记为「objectOwner」总结:sender 和 objectOwner 就是网站的用户,object 就是网站资源,可能是一篇文章,一条文章的评论等等。action 就是动作,可以是赞、评论、收藏、关注、捐款等等。提醒设置提醒通常是可以在「设置-通知」里自定义配置的,用户可以选择性地「订阅」接收和不接收某类通知。呈现在界面上是这样的:通知设置我发布的 publish 文章 被 评论 是/否 通知我 被 收藏 是/否 通知我 被 点赞 是/否 通知我 被 喜欢 是/否 通知我 被 捐款 是/否 通知我我订阅的 follow 文章 有 更新 是/否 通知我 被 评论 是/否 通知我订阅一般系统默认是订阅了所有通知的。系统在给用户推送消息的时候必须查询通知「订阅」模块,以获取某一事件提醒消息应该推送到哪些用户。也就是说「事件」和「用户」之间有一个订阅关系。那么接下来我们分析下「订阅」有哪些关键元素: 比如我发布了一篇文章,那么我会订阅文章《XXX》的评论动作,所以文章《XXX》每被人评论了,就需要发送一则提醒告知我。分析得出以下关键元素:订阅者「subscriber」订阅的对象「object」订阅的动作「action」订阅对象和订阅者的关系「objectRelationship」什么是订阅的目标关系呢? 拿知乎来说,比如我喜欢了一篇文章,我希望我订阅这篇文章的更新、评论动作。那这篇文章和我什么关系?不是所属关系,只是喜欢。objectRelationship = 我发布的,对应着 actions = [评论,收藏]objectRelationship = 我喜欢的,对应着 actions = [更新,评论]讲了那么多,现在来构建「提醒」的数据结构该吧! 提醒表「notify_remind」 表结构如下:id: {type: ‘integer’, primaryKey: true, autoIncrement:true} //主键;remindID: {type: ‘string’, required: true} //通知提醒编号;senderID: {type: ‘string’, required: true} //操作者的ID,三个0代表是系统发送的;senderName: {type: ‘string’, required: true} //操作者用户名;senderAction: {type: ‘string’, required: true} //操作者的动作,如:赞了、评论了、喜欢了、捐款了、收藏了;objectID: {type: ‘string’, required: true}, //目标对象ID;object: {type: ‘string’, required: false}, //目标对象内容或简介,比如:文章标题;objectType: {type: ‘string’, required: true} //被操作对象类型,如:人、文章、活动、视频等;recipientID: {type: ‘string’} //消息接收者;可能是对象的所有者或订阅者;message: {type: ’text’, required: true} //消息内容,由提醒模版生成,需要提前定义;createdAt:{type: ’timestamp’, required: true} //创建时间;status:{type: ‘integer’, required: false} //是否阅读,默认未读;readAt:{type: ’timestamp’, required: false} //阅读时间;假如:特朗普关注了金正恩,以下字段的值是这样的senderID = 特朗普的IDsenderName = 特朗普senderAction = 关注objectID = 金正恩的IDobject = 金正恩objectType = 人recipientID = 金正恩的IDmessage = 特朗普关注了金正恩这种情况objectID 和 recipientID是一样的。这里需要特别说下消息模版,模版由「对象」、「动作」和「对象关系」构成唯一性。通知提醒订阅表「notify_remind_subscribe」 表结构如下:id: {type: ‘integer’, primaryKey: true, autoIncrement:true} //订阅ID;userID: {type: ‘string’, required: true},//用户ID,对应 notify_remind 中的 recipientID;objectType: {type: ‘string’, required: true} //资源对象类型,如:文章、评论、视频、活动、用户;action: {type: ‘string’, required: true} //资源订阅动作,多个动作逗号分隔如: comment,like,post,update etc.objectRelationship: {type: ‘string’, required: true} //用户与资源的关系,用户发布的published,用户关注的followed;createdAt:{type: ’timestamp’, required: true} //创建时间;特别说下「objectRelationship」字段的作用,这个字段用来区分「消息模版」,为什么呢?因为同一个「资源对象」和「动作」会有两类订阅者,一类是该资源的Owner,另一类是该资源的Subscriber,这两类人收到的通知消息内容应该是不一样的。聚合假如我在抖音上发布了一个短视频,在我不在线的时候,被评论了1000遍,当我一上线的时候,应该是收到一千条消息,类似于:「* 评论了你的文章《XXX》」? 还是应该收到一条信息:「有1000个人评论了你的文章《XXX》」? 当然是后者更好些,要尽可能少的骚扰用户。消息推送是不是感觉有点晕了,还是先上一张消息通知的推送流程图吧: 订阅表一共有两张噢,一张是「通知订阅表」、另一张是用户对资源的「对象订阅表」。 具体实现就不多讲了,配合这张图,理解上面讲的应该不会有问题了。私信通常私信有这么几种需求:点到点:用户发给用户的站内信,系统发给用户的站内信。「1:1」点到多:系统发给多个用户的站内信,接收对象较少,而且接收对象无特殊共性。「1:N」点到面:系统发给用户组的站内信,接收对象同属于某用户组之类的共同属性。「1:N」点到全部:系统发给全站用户的站内信,接收对象为全部用户,通常为系统通知。「1:N」这里主要讲「点到点」的站内信。私信表「notify_mailbox」表结构如下:id: {type: ‘integer’, primaryKey: true, autoIncrement:true} //编号;dialogueID: {type: ‘string’, required: true} //对话编号; senderID: {type: ‘string’, required: true} //发送者编号;recipientID: {type: ‘string’, required: true} //接收者编号;messageID: {type: ‘integer’, required: true} //私信内容ID;createdAt:{type: ’timestamp’, required: true} //发送时间;state: {type: ‘integer’, required: true} //状态,已读|未读;readAt:{type: ’timestamp’, required: true} //阅读时间;Inbox私信列表select * from notify_inbox where recipientID=“uid” order by createdAt desc对话列表select * from notify_inbox where dialogueID=“XXXXXXXXXXXX” and (recipientID=“uid” or senderID=“uid”) order by createdAt asc私信回复时,回复的是dialogueIDOutbox私信列表select * from notify_inbox where senderID=“uid” order by createdAt desc对话列表select * from notify_inbox where dialogueID=“XXXXXXXXXXXX” and (senderID=“uid” or recipientID=“uid”) order by createdAt asc私信内容表「notify_inbox_message」表结构如下:id: {type: ‘integer’, primaryKey: true, autoIncrement:true} //编号;senderID: {type: ‘string’, required: true} //发送者编号;content: {type: ‘string’, required: true} //私信内容; createdAt:{type: ’timestamp’, required: true}参考消息系统设计与实现 通知系统设计 ...

February 21, 2019 · 2 min · jiezi

PAT A1122

本身在图论不是一道难题;需要注意这个环判断的几个隐藏点:1.首位相同;2.每个节点只能访问一次;这里借鉴一位大神的操作,其实两种情况可以分两种判别方式,分开判别;这里再次说一下set查重贼好用,这里可以看是否出现重复节点;#include<iostream>#include<stdlib.h>#include<stdio.h>#include<vector>#include<set>using namespace std;using std::vector;using std::set;const int maxn=210;int main(){ int n,m,cnt,k,a[maxn][maxn]={0}; cin>>n>>m; for(int i=0;i<m;i++){ int t1,t2; scanf("%d%d",&t1,&t2); a[t1][t2]=a[t2][t1]=1; } cin>>cnt; while(cnt–){ cin>>k; vector<int> v(k); set<int> s; int f1=1; int f2=1; for(int i=0;i<k;i++){ scanf("%d",&v[i]); s.insert(v[i]); } if(s.size()!=n||k-1!=n||v[0]!=v[k-1]) f1=0; for(int i=0;i<k-1;i++) if(a[v[i]][v[i+1]]==0) f2=false; printf("%s",f1&&f2?“YES\n”:“NO\n”); } system(“pause”); return 0;}

February 21, 2019 · 1 min · jiezi

PAT A1125

题目没读懂,刚开始都不知道他在说些什么。。。简而言之就是给出一堆绳子,练成一条;没两两段绳子练成环,串在一起,之后仍旧视为一串绳子,下次跟别的单独绳子连接的时候仍需对折;所以可以看出,越长的绳子在前面越对折越吃亏,所以应该进行排序,长的绳子放在后面进行连接;#include<iostream>#include<stdlib.h>#include<stdio.h>#include<vector>#include<set>#include<algorithm>using namespace std;using std::vector;using std::set;const int maxn=10100;int lope[maxn]={0};int main(){ int n; scanf("%d",&n); for(int i=0;i<n;i++){ scanf("%d",&lope[i]); } sort(lope,lope+n); int line=lope[0]; for(int i=1;i<n;i++){ line+=lope[i]; line/=2; } printf("%d",line); system(“pause”); return 0;}

February 21, 2019 · 1 min · jiezi

PAT A1121

水题,但是通过这一题需要改进一点;对于hash赋初值的时候一定要谨慎一点,不要随便赋值0,视输入的数据而定;#include<iostream>#include<stdlib.h>#include<stdio.h>#include<vector>#include<set>using namespace std;using std::vector;using std::set;const int maxn=100100;int mem[maxn]={-1};bool vis[maxn]={false};vector<int>output;set<int>out;int main(){ int n; int a,b; scanf("%d",&n); for(int i=0;i<n;i++){ scanf("%d%d",&a,&b); mem[a]=b; mem[b]=a; } scanf("%d",&n); bool flag=true; for(int i=0;i<n;i++){ scanf("%d",&a); vis[a]=true; } for(int i=0;i<maxn;i++){ if(vis[i]){ if(mem[i]==-1){ //如果是没结婚 out.insert(i); }else{ //看看另一个成员来没来 if(!vis[mem[i]]) out.insert(i); } } } printf("%d\n",out.size()); bool f=true; for(set<int>::iterator it=out.begin();it!=out.end();it++){ if(f){ printf("%05d",*it); f=false; } else printf(" %05d",*it); } system(“pause”); return 0;}

February 21, 2019 · 1 min · jiezi

【Leetcode】75.颜色分类

作者: 码蹄疾毕业于哈尔滨工业大学。 小米广告第三代广告引擎的设计者、开发者; 负责小米应用商店、日历、开屏广告业务线研发; 主导小米广告引擎多个模块重构; 关注推荐、搜索、广告领域相关知识;题目给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。注意:不能使用代码库中的排序函数来解决这道题。示例:输入: [2,0,2,1,1,0]输出: [0,0,1,1,2,2]进阶:一个直观的解决方案是使用计数排序的两趟扫描算法。首先,迭代计算出0、1 和 2 元素的个数,然后按照0、1、2的排序,重写当前数组。你能想出一个仅使用常数空间的一趟扫描算法吗?题解首先这个题目你肯定不能用数数的方法啊,如果你用了相当于说:“面试官傻逼!”那怎么来解这种问题呢?我给大家说说一个不是那么绝对的经验,涉及到数组和链表的题目,先想想双指针法可不可以用。这个题目用三个指针:index 表示当前遍历的元素p1 记录最后一个0的位置p2 记录最开始一个2的位置然后从左到右便利,调整index、p1、p2元素java版本 public void sortColors(int[] nums) { // 1-pass int p1 = 0, p2 = nums.length - 1, index = 0; while (index <= p2) { if (nums[index] == 0) { nums[index] = nums[p1]; nums[p1] = 0; p1++; } if (nums[index] == 2) { nums[index] = nums[p2]; nums[p2] = 2; p2–; index–; } index++; } }python版本class Solution: def sortColors(self, nums): """ :type nums: List[int] :rtype: void Do not return anything, modify nums in-place instead. """ p1 = 0 p2 = len(nums) - 1 index = 0 while index <= p2: if nums[index] == 0: nums[index] = nums[p1] nums[p1] = 0 p1 += 1 if nums[index] == 2: nums[index] = nums[p2] nums[p2] = 2 p2 -= 1 index -= 1 index += 1热门文章【Leetcode】74. 搜索二维矩阵【Leetcode】73.矩阵置零【Leetcode】72.编辑距离 ...

February 21, 2019 · 1 min · jiezi

PAT A1060 科学记数法经典例题(全string库解决)

挺操蛋的一道题,我他妈的都服了。。。出这道题我怕是毙了首先题目里就有几个坑:1.可能有前导零,比如说000.00012.可能有零,比如说000.0000000哎,思路感觉最重要,对于字符串处理一定要有思路,知道先干嘛,后干嘛;首先就要去除前导零,把他变成一个纯净的浮点数;去除前导零,我们就可以进行分类讨论,因为必定会出现两种情况:1.第一位是小数点,此时该数为小数;2.第一位是数字,此时该数为大于零的数;对于第一种情况,我们应该注意e和位数的关系;例如.0002,其e一定是符号位到第一位不为零的数字的距离,也就是0.210^-3。所以对于第一种情况,只需要寻找第一位不为零的数字,过一位e–,从而使得得到纯净小数的时候,也能得到指数;注意:两种情况都是不含小数点的数,“0.”后面输出的时候再加对于第二种情况,我们应该先寻找小数点;这里e的记录方式和第一种情况类似,每过一位,e++;比如44.2,我们应该是0.44210^2,而此时e过了两位;当我们找到小数点之后,就应该删除小数点,使我们得到纯净的连续数字;对于以上两种情况,我们都得到了纯净的连续数字,也就是非小数,不包含小数点的数字,接下来就是对保留位数进行判断;对于一种情况,就是通过以上步骤,00.00,最后得到的序列为空,此时e=0,作为零的特殊情况;后续就是对精度计算,并且对不足位进行补0操作;例如现在得到了12,我们要求精度位4位,所以先建立一个空字符串s,遍历的同时对精度位进行计算,当s=“12"时,还差两个精度位,所以补两个0,输出s=“1200"最后进行比较的时候就是对指数和保留部分比较,后续输出的时候再s前加上“0.”输出就行;代码如下所示:#include<iostream>#include<stdlib.h>#include<stdio.h>#include<vector>#include<string>using namespace std;using std::vector;int n;string deal(string s,int& e){ int k=0; while(s.length()>0&&s[0]==‘0’){ s.erase(s.begin()); //去除前导零; } if(s[0]==’.’){ //小于零的数字 s.erase(s.begin()); while(s.length()>0&&s[0]==‘0’){ s.erase(s.begin()); //找到首位非零元素 e–; } }else{ while(k<s.length()&&s[k]!=’.’){ k++; //寻找小数点 e++; } if(k<s.length()){ //说明有小数点,进行删除 s.erase(s.begin()+k); } } if(s.length()==0){ e=0;//取出前导零为0说明实际为0 } int num=0; k=0; string res; while(num<n){ if(k<s.length()) //如果有数字存在 res+=s[k++]; else res+=‘0’; num++; } return res;}int main(){ string s1,s2,s3,s4; cin>>n>>s1>>s2; int e1=0,e2=0; s3=deal(s1,e1); s4=deal(s2,e2); if(s3==s4&&e1==e2){ cout<<“YES 0."<<s3<<"*10^"<<e1<<endl; }else{ cout<<“NO 0."<<s3<<"*10^"<<e1<<” 0."<<s4<<"*10^"<<e2<<endl; } system(“pause”); return 0;} ...

February 21, 2019 · 1 min · jiezi

PAT A1063

这里面还是用到了set去重,还是要多掌握stl的用法;这里注意一个巧妙地处理;由于我们寻找的时两个集合a,b的不重复元素,和重复元素;首先记录b的集合元素,在b中挨个寻找a的元素;如果在b中没有找到,则说明是不重复元素,找到了说明时重复元素,所以分开记录#include<stdio.h>#include<stdlib.h>#include<set>#include<vector>using namespace std;using std::vector;using std::set;int main(){ int n,m,k,temp,a,b; scanf("%d",&n); vector<set<int> > v(n); for(int i=0;i<n;i++){ scanf("%d",&m); set<int>s; for(int j=0;j<m;j++){ scanf("%d",&temp); s.insert(temp); } v[i]=s; } scanf("%d",&k); for(int i=0;i<k;i++){ scanf("%d %d",&a,&b); int nc=0,nt=v[b-1].size(); for(set<int>::iterator it=v[a-1].begin();it!=v[a-1].end();it++){ if(v[b-1].find(it)==v[b-1].end()){ //如果在b中没有找到a的元素 nt++; }else{ nc++; } } double ans=(double)nc/nt100; printf("%.1f%%\n",ans); } system(“pause”); return 0;}

February 21, 2019 · 1 min · jiezi

PAT A1116

hash数组能少开就少开,而且还可以利用set进行去重管理#include <iostream>#include <set>#include <cmath>using namespace std;int ran[10000];bool isprime(int a) { if(a <= 1) return false; int Sqrt = sqrt((double)a); for(int i = 2; i <= Sqrt; i++) { if(a % i == 0) return false; } return true;}int main() { int n, k; scanf("%d", &n); for(int i = 0; i < n; i++) { int id; scanf("%d", &id); ran[id] = i + 1; } scanf("%d", &k); set<int> ss; for(int i = 0; i < k; i++) { int id; scanf("%d", &id); printf("%04d: “, id); if(ran[id] == 0) { printf(“Are you kidding?\n”); continue; } if(ss.find(id) == ss.end()) { ss.insert(id); } else { printf(“Checked\n”); continue; } if(ran[id] == 1) { printf(“Mystery Award\n”); }else if(isprime(ran[id])) { printf(“Minion\n”); }else { printf(“Chocolate\n”); } } return 0;} ...

February 20, 2019 · 1 min · jiezi

PAT A1104

如果采用单纯的暴力枚举,会出现超时问题,可以跟绝递推来找出来规律;代码如下:#include<iostream>#include<stdlib.h>#include<stdio.h>#include<cstring>#include<math.h>using namespace std;const int maxn=100100;double data[maxn];int mem[maxn];int main(){ int n; double sum=0.0; scanf("%d",&n); for(int i=0;i<n;i++){ scanf("%lf",&data[i]); sum+=data[i](n-i)(i+1); } printf("%.2lf",sum); system(“pause”); return 0;}

February 20, 2019 · 1 min · jiezi

【Leetcode】100. 相同的树

题目给定两个二叉树,编写一个函数来检验它们是否相同。如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。示例 1:输入: 1 1 / \ / \ 2 3 2 3 [1,2,3], [1,2,3]输出: true示例 2:输入: 1 1 / \ 2 2 [1,2], [1,null,2]输出: false示例 3::输入: 1 1 / \ / \ 2 1 1 2 [1,2,1], [1,1,2]输出: false题解大多数的二叉树题目都是用递归可以解的。所以当拿到二叉树的题目的时候,我们首先就是看看能拆解成哪些子问题。这个问题的子问题很简单,就是左子树,右子树都相等的二叉树是相同的二叉树。/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } /class Solution { public boolean isSameTree(TreeNode p, TreeNode q) { if (p == null && q == null) { return true; } else if (p == null || q == null) { return false; } if (p.val == q.val) { return isSameTree(p.left, q.left) && isSameTree(p.right, q.right); } else { return false; } }}那如果用非递归解怎么解呢?如果遇到二叉树的问题,没思路还有第二招,就是想想看是不是遍历的变种:先序遍历中序遍历后序遍历层次遍历我们可以用队列,一起进行层序遍历,同时比较左右两颗树:/* * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */class Solution { public boolean isSameTree(TreeNode p, TreeNode q) { if (p == null && q == null) { return true; } else if (p == null || q == null) { return false; } LinkedList<TreeNode> queue = new LinkedList<>(); queue.add(p); queue.add(q); while(!queue.isEmpty()) { TreeNode left = queue.poll(); TreeNode right = queue.poll(); if (left == null && right == null) { // 都是空的,遍历到叶子节点了 continue; } else if (left == null || right == null) { // 有一个为null return false; } else if (left.val != right.val) { // 不相等. return false; } // 左子树入队 queue.add(left.left); queue.add(right.left); // 右子树入队 queue.add(left.right); queue.add(right.right); } return true; }}其实我们本质上就是要比较左右两棵树,也没必要非要是队列,其实stack也是可以的,大同小异。所以并不是你记住哪种数据结构,关键是你能理解后,灵活应用. public boolean isSameTree(TreeNode p, TreeNode q) { if (p == null && q == null) { return true; } else if (p == null || q == null) { return false; } Stack<TreeNode> stack = new Stack<>(); stack.push(p); stack.push(q); while(!stack.isEmpty()) { TreeNode left = stack.pop(); TreeNode right = stack.pop(); if (left == null && right == null) { continue; } else if (left == null || right == null) { return false; } else if (left.val != right.val) { return false; } stack.push(left.left); stack.push(right.left); stack.push(left.right); stack.push(right.right); } return true; }相关阅读技术文章汇总【Leetcode】98. 验证二叉搜索树【Leetcode】95~96 不同的二叉搜索树 ...

February 20, 2019 · 2 min · jiezi

PAT A1059 质因子分解

这里需要注意一下,long int就是int,一样的。。。#include<iostream>#include<stdlib.h>#include<stdio.h>#include<vector>#include<math.h>using namespace std;using std::vector;const int maxn=100010;struct factor{ int x; int cnt;}fac[10];bool is_prime(int n){ if(n==1) return false; int sqr=(int)sqrt(1.0n); for(int i=2;i<=sqr;i++){ if(n%i==0) return false; } return true;}int prime[maxn],pNum=0;void Find_Prime(){ for(int i=1;i<maxn;i++){ if(is_prime(i)==true){ prime[pNum++]=i; } }}int main(){ Find_Prime(); int n,num=0; scanf("%d",&n); if(n==1) printf(“1=1”); else{ printf("%d=",n); int sqr=(int)sqrt(1.0n); for(int i=0;i<pNum&&prime[i]<=sqr;i++){ if(n%prime[i]==0){ fac[num].x=prime[i]; fac[num].cnt=0; while(n%prime[i]==0){ fac[num].cnt++; n/=prime[i]; } num++; } if(n==1) break; } if(n!=1){ fac[num].x=n; fac[num++].cnt=1; } for(int i=0;i<num;i++){ if(i>0) printf("*"); printf("%d",fac[i].x); if(fac[i].cnt>1){ printf("^%d",fac[i].cnt); } } } system(“pause”); return 0;} ...

February 19, 2019 · 1 min · jiezi

[ JavaScript ] 数据结构与算法 —— 链表

本篇主要有三部分什么是链表链表的实现链表的变种源码地址:https://github.com/yhtx1997/S…另外,今天2019年2月18日上午发现 2048-vue 版,代码版本不对,且最新版本遗失,无奈只得重新修复了下 2048-vue地址: https://github.com/yhtx1997/S…什么是链表链表存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的。每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成。 相对于传统的数组,链表的一个好处在于,添加或移除元素的时候不需要移动其他元素。然而,链表需要使用指针,因此实现链表时需要额外注意。数组的另一个细节是可以直接访问任何位置的任何元素,而要想访问链表中间的一个元素,需要从起点(表头)开始迭代列表直到找到所需的元素。 如下图: 注:其中 00 06 10 12 18 为假定在内存中的地址我将已经做好的链表存入数据,然后在控制台打印出来是这样的:它看起来就像是这样的,一层套一层其实应该是下面这样,类似于栓狗的铁链链表的实现链表功能添加元素获取指定位置元素在指定位置插入元素移除指定位置的元素返回指定元素的位置移除指定元素是否为空长度获取表头清空链表转换为字符串输出// 链表元素class Node { constructor(element) { this.element = element; // 元素 this.next = undefined; // 指向下一个元素 }}class LinkedList { // 构造函数声明一些全局变量 constructor(){ this.count = 0; // 长度 this.head = undefined; // 第一个元素 } // 添加元素 push(element) { } // 获取指定位置元素 getElementAt(index) { } // 在指定位置插入元素 insert(element, index) { } // 移除指定位置的元素 removeAt(index) { } // 返回指定元素的位置 indexOf(element) { } // 移除指定元素 remove(element) { } // 是否为空 isEmpty() { } // 长度 size() { } // 获取表头 getHead() { } // 清空链表 clear() { } // 转换为字符串输出 toString() { }}代码实现class LinkedList { // 构造函数声明一些全局变量 constructor(){ this.count = 0; // 长度 this.head = undefined; // 第一个元素 } // 添加元素 push(element) { const node = new Node(element); if (this.head === undefined) { this.head = node; } else { let current = this.head; while (current.next !== undefined) { current = current.next; } current.next = node; } this.count++; } // 获取指定位置元素 getElementAt(index) { // 判断不是空链表 if (this.isEmpty() || index > this.count || index < 0) { // 非空才能继续处理 // 判断不大于最大长度,不小于最小长度(0) return undefined; } // 循环找到元素 let current = this.head; for (let i = 0; i < index; i++){ current = current.next; } return current;// 返回找到的元素 } // 在指定位置插入元素 insert(element, index) { // 创建一个元素 let current = new Node(element); // 首先确定是不是在首位置插入 if (index === 0){ current.next = this.head; this.head = current; } else { // 找到指定位置前一个元素 let previous = this.getElementAt(index - 1); // 将前一个元素的 next 赋值给插入元素的 next current.next = previous.next; // 将插入元素的 node 赋值给前一个元素的 next previous.next = current; } this.count++; } // 移除指定位置的元素 removeAt(index) { let current = this.head; if (index === 0){ this.head = current.next; } else { // 找到这个元素和这个元素之前的元素 let previous = this.getElementAt(index - 1); current = previous.next; // 将这个元素的 next 赋值给这个元素之前元素的 next previous.next = current.next; } this.count–; // 返回要移除的元素 return current.element; } // 返回指定元素的位置 indexOf(element) { // 从头开始找 let current = this.head; // 不超过最大长度 for (let i = 0; i < this.size() && current != null; i++){ if (current.element === element){ // 找到相等的就返回下标 return i; } current = current.next; } return -1; } // 移除指定元素 remove(element) { // 获取指定元素位置 let index = this.indexOf(element); // 移除指定位置元素 return this.removeAt(index); } // 是否为空 isEmpty() { return this.size() === 0; } // 长度 size() { return this.count; } // 获取表头 getHead() { return this.head; } // 清空链表 clear() { this.head = undefined; this.count = 0; } // 转换为字符串输出 toString() { if (this.head == null) { return ‘’; } let objString = ${this.head.element}; let current = this.head.next; for (let i = 1; i < this.size() && current != null; i++) { objString = ${objString},${current.element}; current = current.next; } return objString; }}let a = new LinkedList();a.push(‘a’);a.push(‘b’);a.push(‘c’);a.push(’d’);a.push(’e’);a.push(‘f’);a.push(‘h’);a.push(‘i’);a.push(‘j’);a.push(‘k’);a.push(’l’);a.push(’m’);a.push(’n’);a.push(‘o’);a.push(‘p’);a.push(‘q’);a.remove(‘a’);a.insert(‘a’,1);console.log(a);插入元素图解:现在有狗链两节,我要在中间加一节先把两节分开,然后把前边的尾部与要加的头部相连,然后把要加的尾部与后边的头部相连 0 连 xx , xx 连 1链表的变种双向链表我们已经知道链表的每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成,双向链表除了这个基本特性,每个元素还包含一个指向前一个元素的引用,如图所示:循环链表循环链表就是链表的最后一个指向下一个元素的引用指向了第一个元素,使其成为循环链表双向循环链表双向循环链表就是双向链表的第一个元素指向前一个的引用指向了最后一个元素,而最后一个元素指向下一个元素的引用指向了第一个元素,如图所示: ...

February 19, 2019 · 3 min · jiezi

PAT A1096 质因子分解

这道题没有用常规的从小到大分解质因子,而是要连续的质因子;其实思路很简单,从2~sqrt(n)进行枚举,使得n%temp是否能够取余为0,temp为连续质因子的乘积。在每次迭代中记录最长的质因子序列和相应的起始值;#include<iostream>#include<stdlib.h>#include<stdio.h>#include<vector>#include<math.h>using namespace std;using std::vector;typedef long long LL;int main(){ LL n; scanf("%lld",&n); LL sqr=(LL)sqrt(1.0n); LL ansI=0,ansLen=0; for(LL i=2;i<=sqr;i++){ LL temp=1,j=i; while(1){ temp=j; if(n%temp!=0) break; if(j-i+1>ansLen){ ansI=i; ansLen=j-i+1; } j++; } } if(ansLen==0){ printf(“1\n%lld”,n); }else{ printf("%lld\n",ansLen); for(LL i=0;i<ansLen;i++){ printf("%lld",ansI+i); if(i<ansLen-1){ printf("*"); } } } system(“pause”); return 0;}

February 19, 2019 · 1 min · jiezi

关于大数问题的个人理解

大数问题也不是第一次接触过,但是只是零零碎碎的做过几道题,并没有很系统的整理过,并且自己的处理上多多少少存在很多瑕疵,所以这里做一个整理;一、大数的存储:相应的,大数存储应该将每一位存储在数组之中,但是需要注意的时,数组存储时从0开始,所以大数存储应该时数字的逆向存储;之前自己时正向存储,所以会在计算进位上造成麻烦,逆向存储可以很好的存储和管理进位问题;struct bign{int d[1000];int len;bign(){ memset(d,0,sizeof(d)); len=0;}};这里使用了构造函数,来保证结构体创立的时候能够第一时间初始化;对于初始化读入数字的时候,我们也需要对数字进行逆序处理;struct bign{ int d[1000]; int len; bign(){ memset(d,0,sizeof(d)); len=0; }};所以对于不同的两个数字,我们比较的方式可以有如下定义:如果长度不一样,则长的大;如果长度相同,则从高位开始逐个比较(也就是反向枚举),从而找出数字大的;int compare(bign a,bign b){ if(a.len>b.len) return 1; else if(a.len<b.len){ for(int i=a.len-1;i>=0;i–){ if(a.d[i]>b.d[i]) return 1; else if(a.d[i]<b.d[i]) return -1; } return 0; }}二、加法运算:对于加法,我们一定要注意进位问题,并且当一正一负的时候可以直接做减法运算,都为负数加和后加上符号即可;如下所示bign add(bign a,bign b){ bign c; int carry=0;//carry位进位位 for(int i=0;i<a.len||i<b.len;i++){ int temp=a.d[i]+b.d[i]+carry;//一定要加上上一位的进位; c.d[c.len++]=temp%10; carry=temp/10;//计算进位 } if(carry!=0){ c.d[len++]=carry; } return c;}值得注意的使上面的循环操作,由于最短数字的后面都为0,所以相当于直接加上长的哪一位本位的元素,再加上进位(当短的遍历完之后carry恒为0)后面的if判断意为如果两个数字序列长度相等,则应该在新开辟更高一位存储进位;三、减法运算:减法运算和加法运算大致相同,从序列低位开始,但是需要注意的使拥有借位思想;并且,当被减数小于减数的时候,应该交换位置进行相减,并且只需要输出一个负号即可;bign sub(bign a,bign b){ bign c; for(int i=0;i<a.len||i<b.len;i++){ if(a.d[i]<b.d[i]){ //需要借位; a.d[i+1]–; a.d[i]+=10;//当前位加十 } c.d[c.len++]=a.d[i]-b.d[i]; } while(c.len-1>=1&&c.d[c.len-1]==0){ c.len–; }}注意点有两处:1.这里的借位是相邻高位-1,本位+10相减的操作;2.由于可能有高位相减=0,但是len任然计数的可能性存在,所以要从高位开始,除去高位的前导0;四、高精度和低精度乘法运算:这里所谓的高精度就是采用数组保存的大数,而低精度就是使用int保存的数字;假设,数组保存的数有三位,int保存的的数字有二位,和普通小学数学乘法不同的是,我们采用的是int保留的两位,逐个对数组保存的三位进行乘积,这样就会进行三次运算,而不是手写运算的两次;bign multi(bign a,int b){ bign c; int carry=0; for(int i=0;i<a.len;i++){ int temp=a.d[i]b+carry;//个位进行结果 carry=temp/10; } while(carry!=0){ c.d[c.len++]=carry%10; carry/=10 } return c;}其实和加法运算类似;需要注意的是当计算完毕,但是carry不为0的时候,最后操作相当于将carry逐位送入高位;五、高精度和低精度除法:和手写除法类似,当该位不够除的时候,保留该位,置商为0,和下一位构成一个数,看是否够除,所以本质看来,代码里应该是对余数来进行除法操作;bign divide(bign a,int b,int&r){ //r为余数 bign c; c.len=a.len;//和被除数一一对应 for(int i=a.len-1;i>=0;i–){ r=r10+a.d[i]; if(r<b) c.d[i]=0; else{ c.d[i]=r/b; r=r%b; } } while(c.len-1>=1&&c.d[c.len-1]==0){ c.len–; } return c;}这里需要注意的是高位仍然需要除去0操作;在包括0的情况下,商的位数应该和被除数的位数一一对应,这也就是我们的边界判别条件之一; ...

February 19, 2019 · 1 min · jiezi

关于Hash散列的集中查重方式

这里总结一下Hash散列当出现不能插入位置的几种位移和计算方式,以免遗忘和出现不知道都在讲些神马;当我们key1和key2冲突的时候,主要有三种方式进行冲突解决;先来说两种开放定址法,所谓开放定址法就是重新计算了hash值;1.线性探查法:当我们插入key的位置,产生冲突之后,加1,查看该位置是否可以使用。如果不可以使用,再次+1,重复到找到位置,或者查完没有满足的位置,并且在这个途中,可以越过尾部,从hash序列头部进行枚举。但是该方法有一个缺点,就是容易造成元素扎堆;2.平方探查法:插入时,当H(key)的位置被占时,将检查下列位置:H(key)+1^2,H(key)-1^2,H(key)+2^2,H(key)-2^2…。如果在这个途中H(key)+k^2超过了表长,则进行取模操作;如果H(key)-k^2<0时,则进行((H(key)-k^2)%Tsize+Tsize)%Tsize,从而保证索引为正;这两个方向的操作称为正向和负向操作,为了避免计算麻烦,往往可以采用正向操作;注意一点,寻找的次数k在[0,Tsize]内,当k超过这个范围,必不可能插入,停止计算;第三中时拉链法则:3.拉链法:拉链法不计算新的hash值,而是在重复Hash值的地方构建一个单链表,从而将所有重复的节点连接起来,查询的时候遍历该链表中的所有元素;

February 18, 2019 · 1 min · jiezi

PAT A1078

这道题牵扯到了hash散列中的集中查询方式,随后做一个总结,对于素数方面,没有神马难度;#include<iostream>#include<stdlib.h>#include<stdio.h>#include<vector>#include<math.h>using namespace std;using std::vector;const int maxn=10010;bool vis[maxn]={false};bool isPrime(int x){ if(x<=1) return false; int sqr=sqrt(x1.0); for(int i=2;i<=sqr;i++){ if(x%i==0) return false; } return true;}int main(){ vector<char> v; int m,n; int num; scanf("%d%d",&m,&n); while(!isPrime(m)){ m++; } for(int i=0;i<n;i++){ scanf("%d",&num); int M=num%m; if(!vis[M]){ vis[M]=true; if(i==0) printf("%d",M); else printf(" %d",M); }else{ int step; for(step=1;step<m;step++){ M=(num+stepstep)%m; if(!vis[M]){ vis[M]=true; if(i==0) printf("%d",M); else printf(" %d",M); break; } } if(step>=m){ if(i>0) printf(" “); printf(”-"); } } } for(int i=0;i<v.size();i++){ if(i==0) printf("%c",v[i]); else printf(" %c",v[i]); } system(“pause”); return 0;} ...

February 18, 2019 · 1 min · jiezi

PAT A1015

可以说是常规下的素数判断;唯一要注意的是循环读入这个点:该句子其实就是读取到结尾结束,在文件中标识的就是缓冲区读取完毕。这里可以用来判断神马时候输入完毕;这里还是在判别函数里要注意两点:第一:注意,0,1不是素数;第二:判别区间应该是2~sqrt(n),闭区间#include<iostream>#include<string>#include<stdlib.h>#include<stdio.h>#include<math.h>using namespace std;bool isPrime(int n){ if(n<=1) return false; int sqr=(int)sqrt(1.0n); for(int i=2;i<=sqr;i++){ if(n%i==0) return false; } return true;}int d[111];int main(){ int n,radix; while(scanf("%d",&n)!=EOF){ //意思就是从缓冲区里面一直读取 if(n<0) break; scanf("%d",&radix); if(isPrime(n)==false){ printf(“No\n”); }else{ int index=0; while(n!=0){ d[index++]=n%radix; n=n/radix; } for(int i=0;i<index;i++){ n=nradix+d[i]; } if(isPrime(n)==true) printf(“Yes\n”); else printf(“No\n”); } } system(“pause”); return 0;}

February 18, 2019 · 1 min · jiezi

PAT A1088 分数四则运算

和之前那题一样,只不过是四册运算:#include<iostream>#include<stdio.h>#include<stdlib.h>using namespace std;typedef long long ll;struct node{ ll up; ll down;};int gcd(ll a,ll b){ if(b==0) return a; return gcd(b,a%b);}node reflesh(node a){ if(a.down<0){ a.down=-a.down; a.up=-a.up; } if(a.up==0){ a.down=1; }else{ int d; if(abs(a.up)>a.down){ d=gcd(abs(a.up),a.down); }else{ d=gcd(a.down,abs(a.up)); } a.up/=d; a.down/=d; } return a;}node add(node a,node b){ node result; result.down=a.downb.down; result.up=a.upb.down+b.upa.down; return reflesh(result);}node minl(node a,node b){ node result; result.down=a.downb.down; result.up=a.upb.down-b.upa.down; return reflesh(result);}node doub(node a,node b){ node result; result.down=a.downb.down; result.up=a.upb.up; return reflesh(result);}node mult(node a,node b){ node result; result.down=a.downb.up; result.up=a.upb.down; return reflesh(result);}void output(node result){ result=reflesh(result); if(result.up<0) printf("("); if(result.down==1){ printf("%lld",result.up); }else if(abs(result.up)>result.down){ printf("%lld %lld/%lld",result.up/result.down,abs(result.up)%result.down,result.down); }else{ printf("%lld/%lld",result.up,result.down); } if(result.up<0) printf(")");}int main(){ node a,b; scanf("%lld/%lld %lld/%lld",&a.up,&a.down,&b.up,&b.down); output(a); printf(" + “); output(b); printf(” = “); output(add(a,b)); printf("\n”); output(a); printf(" - “); output(b); printf(” = “); output(minl(a,b)); printf("\n”); output(a); printf(" * “); output(b); printf(” = “); output(doub(a,b)); printf("\n”); output(a); printf(" / “); output(b); printf(” = “); if(b.up==0) printf(“Inf”); else output(mult(a,b)); system(“pause”); return 0;} ...

February 18, 2019 · 1 min · jiezi

PAT A1081 分数计算

分数计算,之前做过相关的总结,这里不再赘述代码如下:#include<iostream>#include<stdlib.h>#include<stdio.h>#include<string>using namespace std;const int maxn=110;typedef long long LL;struct node{ LL up; LL down;};LL str2num(string s){ LL sum=0; for(int i=0;i<s.size();i++){ sum=sum10+(s[i]-‘0’); } return sum;}int gcd(int a,int b){ if(b==0) return a; return gcd(b,a%b);}node reflesh(node a){ if(a.down<0){ a.up=-a.up; a.down=-a.down; } if(a.up==0){ a.down=1; }else{ int d; if(abs(a.up)>abs(a.down)){ d=gcd(abs(a.up),abs(a.down)); }else{ d=gcd(abs(a.down),abs(a.up)); } a.down/=d; a.up/=d; } return a;}node add(node a,node b){ node result; result.down=a.downb.down; result.up=a.upb.down+a.downb.up; return reflesh(result);}void output(node a){ //reflesh(a); if(a.down==1) printf("%lld\n",a.up); else if(abs(a.up)>a.down){ printf("%lld %lld/%lld\n",a.up/a.down,abs(a.up)%a.down,a.down); }else{ printf("%lld/%lld\n",a.up,a.down); }}int main(){ int N; node f,s; s.down=1; s.up=0; scanf("%d",&N); for(int i=0;i<N;i++){ scanf("%lld/%lld",&f.up,&f.down); s=add(s,f); } output(s); system(“pause”); return 0;} ...

February 18, 2019 · 1 min · jiezi