JS面试之数据结构与算法 (5)

序列文章JS面试之函数(1)JS面试之对象(2)JS面试之数组的几个不low操作(3)JS面试之http0.9~3.0对比分析(4)前言数据结构是计算机存储、组织数据的方式,算法是系统描述解决问题的策略。了解基本的数据结构和算法可以提高代码的性能和质量。也是程序猿进阶的一个重要技能。手撸代码实现栈,队列,链表,字典,二叉树,动态规划和贪心算法1.数据结构篇1.1 栈栈的特点:先进后出class Stack { constructor() { this.items = []; } // 入栈 push(element) { this.items.push(element); } // 出栈 pop() { return this.items.pop(); } // 末位 get peek() { return this.items[this.items.length - 1]; } // 是否为空栈 get isEmpty() { return !this.items.length; } // 长度 get size() { return this.items.length; } // 清空栈 clear() { this.items = []; } } // 实例化一个栈 const stack = new Stack(); console.log(stack.isEmpty); // true // 添加元素 stack.push(5); stack.push(8); // 读取属性再添加 console.log(stack.peek); // 8 stack.push(11); console.log(stack.size); // 3 console.log(stack.isEmpty); // false1.2 队列队列:先进先出 class Queue { constructor(items) { this.items = items || []; } enqueue(element) { this.items.push(element); } dequeue() { return this.items.shift(); } front() { return this.items[0]; } clear() { this.items = []; } get size() { return this.items.length; } get isEmpty() { return !this.items.length; } print() { console.log(this.items.toString()); } } const queue = new Queue(); console.log(queue.isEmpty); // true queue.enqueue(“John”); queue.enqueue(“Jack”); queue.enqueue(“Camila”); console.log(queue.size); // 3 console.log(queue.isEmpty); // false queue.dequeue(); queue.dequeue(); 1.3 链表链表:存贮有序元素的集合, 但是不同于数组,每个元素是一个存贮元素本身的节点和指向下一个元素引用组成 要想访问链表中间的元素,需要从起点开始遍历找到所需元素 class Node { constructor(element) { this.element = element; this.next = null; } } // 链表 class LinkedList { constructor() { this.head = null; this.length = 0; } // 追加元素 append(element) { const node = new Node(element); let current = null; if (this.head === null) { this.head = node; } else { current = this.head; while (current.next) { current = current.next; } current.next = node; } this.length++; } // 任意位置插入元素 insert(position, element) { if (position >= 0 && position <= this.length) { const node = new Node(element); let current = this.head; let previous = null; let index = 0; if (position === 0) { this.head = node; } else { while (index++ < position) { previous = current; current = current.next; } node.next = current; previous.next = node; } this.length++; return true; } return false; } // 移除指定位置元素 removeAt(position) { // 检查越界值 if (position > -1 && position < length) { let current = this.head; let previous = null; let index = 0; if (position === 0) { this.head = current.next; } else { while (index++ < position) { previous = current; current = current.next; } previous.next = current.next; } this.length–; return current.element; } return null; } // 寻找元素下标 findIndex(element) { let current = this.head; let index = -1; while (current) { if (element === current.element) { return index + 1; } index++; current = current.next; } return -1; } // 删除指定文档 remove(element) { const index = this.indexOf(element); return this.removeAt(index); } isEmpty() { return !this.length; } size() { return this.length; } // 转为字符串 toString() { let current = this.head; let string = “”; while (current) { string += ${current.element}; current = current.next; } return string; } } const linkedList = new LinkedList(); console.log(linkedList); linkedList.append(2); linkedList.append(6); linkedList.append(24); linkedList.append(152); linkedList.insert(3, 18); console.log(linkedList); console.log(linkedList.findIndex(24)); 1.4 字典字典:类似对象,以key,value存贮值class Dictionary { constructor() { this.items = {}; } set(key, value) { this.items[key] = value; } get(key) { return this.items[key]; } remove(key) { delete this.items[key]; } get keys() { return Object.keys(this.items); } get values() { /* 也可以使用ES7中的values方法 return Object.values(this.items) */ // 在这里我们通过循环生成一个数组并输出 return Object.keys(this.items).reduce((r, c, i) => { r.push(this.items[c]); return r; }, []); } } const dictionary = new Dictionary(); dictionary.set(“Gandalf”, “gandalf@email.com”); dictionary.set(“John”, “johnsnow@email.com”); dictionary.set(“Tyrion”, “tyrion@email.com”); console.log(dictionary); console.log(dictionary.keys); console.log(dictionary.values); console.log(dictionary.items); 1.5 二叉树特点:每个节点最多有两个子树的树结构class NodeTree { constructor(key) { this.key = key; this.left = null; this.right = null; } } class BinarySearchTree { constructor() { this.root = null; } insert(key) { const newNode = new NodeTree(key); const insertNode = (node, newNode) => { if (newNode.key < node.key) { if (node.left === null) { node.left = newNode; } else { insertNode(node.left, newNode); } } else { if (node.right === null) { node.right = newNode; } else { insertNode(node.right, newNode); } } }; if (!this.root) { this.root = newNode; } else { insertNode(this.root, newNode); } } //访问树节点的三种方式:中序,先序,后序 inOrderTraverse(callback) { const inOrderTraverseNode = (node, callback) => { if (node !== null) { inOrderTraverseNode(node.left, callback); callback(node.key); inOrderTraverseNode(node.right, callback); } }; inOrderTraverseNode(this.root, callback); } min(node) { const minNode = node => { return node ? (node.left ? minNode(node.left) : node) : null; }; return minNode(node || this.root); } max(node) { const maxNode = node => { return node ? (node.right ? maxNode(node.right) : node) : null; }; return maxNode(node || this.root); } } const tree = new BinarySearchTree(); tree.insert(11); tree.insert(7); tree.insert(5); tree.insert(3); tree.insert(9); tree.insert(8); tree.insert(10); tree.insert(13); tree.insert(12); tree.insert(14); tree.inOrderTraverse(value => { console.log(value); }); console.log(tree.min()); console.log(tree.max()); 2.算法篇2.1 冒泡算法冒泡排序,选择排序,插入排序,此处不做赘述,请戳 排序2.2 斐波那契特点:第三项等于前面两项之和function fibonacci(num) { if (num === 1 || num === 2) { return 1 } return fibonacci(num - 1) + fibonacci(num - 2) }2.3 动态规划特点:通过全局规划,将大问题分割成小问题来取最优解案例:最少硬币找零 美国有以下面额(硬币):d1=1, d2=5, d3=10, d4=25 如果要找36美分的零钱,我们可以用1个25美分、1个10美分和1个便士( 1美分)class MinCoinChange {constructor(coins) { this.coins = coins this.cache = {}}makeChange(amount) { if (!amount) return [] if (this.cache[amount]) return this.cache[amount] let min = [], newMin, newAmount this.coins.forEach(coin => { newAmount = amount - coin if (newAmount >= 0) { newMin = this.makeChange(newAmount) } if (newAmount >= 0 && (newMin.length < min.length - 1 || !min.length) && (newMin.length || !newAmount)) { min = [coin].concat(newMin) } }) return (this.cache[amount] = min)}}const rninCoinChange = new MinCoinChange([1, 5, 10, 25])console.log(rninCoinChange.makeChange(36))// [1, 10, 25]const minCoinChange2 = new MinCoinChange([1, 3, 4])console.log(minCoinChange2.makeChange(6))// [3, 3]2.4 贪心算法特点:通过最优解来解决问题用贪心算法来解决2.3中的案例class MinCoinChange2 {constructor(coins) { this.coins = coins}makeChange(amount) { const change = [] let total = 0 this.coins.sort((a, b) => a < b).forEach(coin => { if ((total + coin) <= amount) { change.push(coin) total += coin } }) return change}}const rninCoinChange2 = new MinCoinChange2 ( [ 1, 5, 10, 25])console.log (rninCoinChange2. makeChange (36)) ...

April 8, 2019 · 5 min · jiezi

数据结构与算法——图

什么是图?前面说完了树这种数据结构,接下来在看看一种更加复杂的非线性数据结构——图。先看看下面图这种数据结构的图片演示吧:像上图这样的数据结构就叫做图了,图中的每个节点叫做 顶点 ,各个顶点之间的连接关系叫做 边 ,每个顶点有多少条边,叫做这个顶点的 度 。其实图这种数据结构比较适合用来存储我们常用的微信、微博好友关系。例如存储微信好友,例如两个人互加了微信,就相当于在两个顶点之间加上一条边,而顶点的度则表示一个人有多少微信好友。而微博这样的存储关系,要稍微复杂一些,因为微博允许当方面关注,例如 A 关注了 B ,而 B 可以不关注 A,这样的关系,我们可以在图中引入方向的概念,先看下图:例如 A 关注了 B,那么直接将 A 的边指向 B 即可。这样有方向关系的图,叫做 有向图 ,显然,没有方向关系的图,就叫做 无向图 。无向图中有度的概念,表示一个顶点有多少条边,而有向图中的度,则还有 入度 和 出度 的区分,例如 A 指向 B,叫做 A 顶点的出度,E 指向了 A,叫做 A 的入度。不难理解,对应到微博的关系中,一个顶点的出度,就表示他关注了多少人,入度,则表示他有多少粉丝。2. 图是如何存储的?图有两种存储的方式,第一种叫做邻接矩阵,其底层是利用二维数组来存储的。对于无向图,如果顶点 i 和 j 之间有边,则在二维数组中 A[i] [j] 和 A[j] [i] 位置处标记为 1 ,对于有向图,如果 i 指向了 j,则将二维数组中 A[i] [j] 位置标记为 1,类似,如果 j 指向了 i,则将二维数组中 A[j] [i] 位置标记为 1。看下图的说明就很容易明白了:这种存储方式虽然支持较为高效的查找操作,因为可以直接根据数组下标取出数据,但是存在的问题便是比较浪费存储空间,特别是对于数据量较大的情况。另一种更加常用的图存储方式是邻接表,每个顶点对应一个链表,就像下图这样:上面是使用的有向图,每个顶点对应的链表存储的是该顶点所指向的顶点,如果是无向图的话,那就更简单了,每个顶点链表存储的是与该顶点有边关系的顶点。3. 简单实现一个图接下来我是用代码简单使用了一个图,你可以看看,顺便理解一下:public class Graph { private int vertex;//图中的顶点个数 private LinkedList<Integer>[] list; public Graph(int vertex) { this.vertex = vertex; list = new LinkedList[vertex]; for (int i = 0; i < vertex; i++) { list[i] = new LinkedList(); } } //两个顶点之间建立边关系 public void addSide(int v1, int v2){ if (v1 >= vertex || v2 >= vertex || v1 == v2) return; if (!list[v1].contains(v2)) list[v1].add(v2); if (!list[v2].contains(v1)) list[v2].add(v1); } //删除顶点之间的边 public void removeSide(int v1, int v2){ if (v1 >= vertex || v2 >= vertex || v1 == v2) return; if (list[v1].contains(v2)) list[v1].remove(v2); if (list[v2].contains(v1)) list[v2].remove(v1); } //列出与某顶点有边关系的所有顶点 public void listVertexes(int v){ if (v >= vertex) return; System.out.println(list[v].toString()); }}

April 5, 2019 · 1 min · jiezi

数据结构与算法——堆的应用

概述前面说完了堆这种数据结构,并且讲到了它很经典的一个应用:堆排序,其实堆这种数据结构还有其他很多的应用,今天就一起来看看,主要有下列内容:优先级队列求 Top K 问题求中位数2. 优先级队列优先级队列是一种特殊的队列,前面学习队列的时候,说到队列满足 先进先出,后进后出 的特点,优先级队列则不是这样。优先级队列中的数据,出队的顺序是有优先级的,优先级高的,先出队列。而堆其实就可以看作是一个优先级队列,因为堆顶元素总是数据中最大或最小的元素,每次出队列都可以看作取出堆顶元素。如果你熟悉 Java 语言,则或多或少听说或是使用过 PriorityQueue 这个容器,在《Java 核心技术·卷 I》中,说到 PriorityQueue 就是优先级队列,并且它基于一种很优雅的数据结构——堆。接下来就小试牛刀,举一个具体的例子来看看优先级队列的应用。例如我们需要合并 10 个有序的小文件,小文件中存储的是有序的字符串数据。借助优先级队列,我们可以很高效的解决这个问题。我们从每个文件中读取第一个字符串存入优先级队列中,那么每次出队列,都是最小的那个元素。将出队列的数据存储到一个大文件中,然后继续从文件中读取一个字符串存入队列,然后继续出队列,一直循环这个操作。当然,这主要是针对数据文件较大的情况,如果数据不多,那么直接将全部的数据存入队列,然后依次出队列就可以了,具体问题具体分析。3. Top K 问题这样的问题其实非常的常见了,在一组数据当中 ,我们需要求得其前 K 大的数据。这分为了两种情况,一是针对 静态数据 ,即数据不会发生变化。我们可以维护一个大小为 K 的小顶堆,然后依次遍历数组,如果数组数据比堆顶元素大,则插入到堆中,如果小,则不做处理。遍历完之后,则堆中存在的数据就是 Top K 了。我用代码模拟了这个过程:public class GetTopK { public static void main(String[] args) { int[] num = {2, 34, 45, 56, 76, 65, 678, 33, 888, 678, 98, 0, 7}; //求 Top 3 Queue<Integer> queue = new PriorityQueue<>(3); queue.add(num[0]); queue.add(num[1]); queue.add(num[2]); for (int i = 3; i < num.length; i++) { int small = queue.peek(); if (num[i] > small){ queue.poll(); queue.add(num[i]); } } System.out.println(queue.toString()); }}第二种情况,是动态的数据集合,数据会有增加、删除的情况,如果新增一个元素,将其和堆顶元素进行比较,如果数据比堆顶元素大,则插入到堆中,如果小,则不做处理。这样的话,无论数据怎样变化,我们都能够随时拿到 Top K,而不用因为数据的变化重新组织堆。4. 求中位数顾名思义,中位数就是一组数据中最中间的那个数据,只不过注意,数据需要有序排列。针对一个大小为 n 的数据集,如果 n 为偶数,那么中位数有两个,分别是 n/2 和 n/2 + 1 这两个数据,我们可以随机取其中一个;如果 n 为奇数,则 n/2 + 1 这个数为中位数。如果是一个静态的数据,那么可直接排序然后求中位数,但是如果数据有变化,这样每次排序的成本太高了。所以,可以借助堆来实现求中位数的功能。我们可以维护一个大顶堆,一个小顶堆,小顶堆中存储后 n/2 个数据,大顶堆中存储前面剩余的数据。如果 n 是偶数,则两个堆中存储的都是相同个数的数据,如果 n 为奇数,则大顶堆中要多一个数据。结合下图你就很容易明白了:如果有数据插入的情况,如果数据小于等于大顶堆顶元素,则插入到大顶堆中,如果数据大于等于小顶堆顶元素,则插入到小顶堆中。只不过可能会出现一个问题,就是堆中的数据不满足均分情况,那么我们需要移动两个堆中的元素,反正需要保证 大顶堆的元素个数和小顶堆的元素个数要么相等,或者大顶堆中多一个。我用代码简单模拟了整个实现: public class GetMiddleNum { public static void main(String[] args) { //原始数据 Integer[] num = {12, 34, 6, 43, 78, 65, 42, 33, 5, 8}; //排序后存入ArrayList中 Arrays.sort(num); ArrayList<Integer> data = new ArrayList<>(Arrays.asList(num)); //大顶堆 Queue<Integer> bigQueue = new PriorityQueue<>((o1, o2) -> { if (o1 <= o2) return 1; else return -1; }); //小顶堆 Queue<Integer> smallQueue = new PriorityQueue<>(); int n = data.size(); int i; if (n % 2 == 0) i = n / 2; else i = n / 2 + 1; //后 n/2 的数据存入到小顶堆中 for (int j = i; j < n; j++) { smallQueue.add(data.get(j)); } //前面的数据存入到大顶堆中 for (int j = 0; j < i; j++) { bigQueue.add(data.get(j)); } //插入数据,需要做单独的处理 insert(data, 99, bigQueue, smallQueue); insert(data, 3, bigQueue, smallQueue); insert(data, 1, bigQueue, smallQueue); //大顶堆的堆顶元素就是中位数 System.out.println(“The middle num = " + bigQueue.peek()); } private static void insert(List<Integer> list, int value, Queue<Integer> bigQueue, Queue<Integer> smallQueue){ list.add(value); if (value <= bigQueue.peek()) bigQueue.add(value); if (value >= smallQueue.peek()) smallQueue.add(value); while (smallQueue.size() > bigQueue.size()) bigQueue.add(smallQueue.poll()); while (bigQueue.size() - smallQueue.size() > 1) smallQueue.add(bigQueue.poll()); } }

April 4, 2019 · 2 min · jiezi

你见过的最全面的python重点

前端span设置margin上下无效果,因为span是行内元素,是没有宽高的。Py2 VS Py3print成为了函数,python2是关键字不再有unicode对象,默认str就是unicodepython3除号返回浮点数没有了long类型rangex不存在,range替代了rangex可以使用中文定义函数名变量名高级解包 和*解包限定关键字参数 后的变量必须加入名字=值raise fromiteritems移除变成items()yield from 链接子生成器asyncio,async/await原生协程支持异步编程新增enum,mock,ipaddress,concurrent.futures,asyncio urllib,selector不同枚举类间不能进行比较同一枚举类间只能进行相等的比较枚举类的使用(编号默认从1开始)为了避免枚举类中相同枚举值的出现,可以使用@unique装饰枚举类#枚举的注意事项from enum import Enumclass COLOR(Enum): YELLOW=1#YELLOW=2#会报错 GREEN=1#不会报错,GREEN可以看作是YELLOW的别名 BLACK=3 RED=4print(COLOR.GREEN)#COLOR.YELLOW,还是会打印出YELLOWfor i in COLOR:#遍历一下COLOR并不会有GREEN print(i)#COLOR.YELLOW\nCOLOR.BLACK\nCOLOR.RED\n怎么把别名遍历出来for i in COLOR.members.items(): print(i)# output:(‘YELLOW’, <COLOR.YELLOW: 1>)\n(‘GREEN’, <COLOR.YELLOW: 1>)\n(‘BLACK’, <COLOR.BLACK: 3>)\n(‘RED’, <COLOR.RED: 4>)for i in COLOR.members: print(i)# output:YELLOW\nGREEN\nBLACK\nRED#枚举转换#最好在数据库存取使用枚举的数值而不是使用标签名字字符串#在代码里面使用枚举类a=1print(COLOR(a))# output:COLOR.YELLOWpy2/3转换工具six模块:兼容pyton2和pyton3的模块2to3工具:改变代码语法版本__future__:使用下一版本的功能常用的库必须知道的collectionspython排序操作及heapq模块itertools模块超实用方法不常用但很重要的库dis(代码字节码分析)inspect(生成器状态)cProfile(性能分析)bisect(维护有序列表)fnmatchfnmatch根据系统决定fnmatch(string,".txt") #win下不区分大小写fnmatchcase完全区分大小写timeit(代码执行时间) def isLen(strString): #还是应该使用三元表达式,更快 return True if len(strString)>6 else False def isLen1(strString): #这里注意false和true的位置 return [False,True][len(strString)>6] import timeit print(timeit.timeit(‘isLen1(“5fsdfsdfsaf”)’,setup=“from main import isLen1”)) print(timeit.timeit(‘isLen(“5fsdfsdfsaf”)’,setup=“from main import isLen”))contextlib@contextlib.contextmanager使生成器函数变成一个上下文管理器types(包含了标准解释器定义的所有类型的类型对象,可以将生成器函数修饰为异步模式) import types types.coroutine #相当于实现了__await__html(实现对html的转义) import html html.escape("<h1>I’m Jim</h1>") # output:’&lt;h1&gt;I&#x27;m Jim&lt;/h1&gt;’ html.unescape(’&lt;h1&gt;I&#x27;m Jim&lt;/h1&gt;’) # <h1>I’m Jim</h1>mock(解决测试依赖)concurrent(创建进程池河线程池)from concurrent.futures import ThreadPoolExecutorpool = ThreadPoolExecutor()task = pool.submit(函数名,(参数)) #此方法不会阻塞,会立即返回task.done()#查看任务执行是否完成task.result()#阻塞的方法,查看任务返回值task.cancel()#取消未执行的任务,返回True或False,取消成功返回Truetask.add_done_callback()#回调函数task.running()#是否正在执行 task就是一个Future对象for data in pool.map(函数,参数列表):#返回已经完成的任务结果列表,根据参数顺序执行 print(返回任务完成得执行结果data) from concurrent.futures import as_completedas_completed(任务列表)#返回已经完成的任务列表,完成一个执行一个wait(任务列表,return_when=条件)#根据条件进行阻塞主线程,有四个条件selectot(封装select,用户多路复用io编程)asynciofuture=asyncio.ensure_future(协程) 等于后面的方式 future=loop.create_task(协程)future.add_done_callback()添加一个完成后的回调函数loop.run_until_complete(future)future.result()查看写成返回结果asyncio.wait()接受一个可迭代的协程对象asynicio.gather(*可迭代对象,可迭代对象) 两者结果相同,但gather可以批量取消,gather对象.cancel()一个线程中只有一个loop在loop.stop时一定要loop.run_forever()否则会报错loop.run_forever()可以执行非协程最后执行finally模块中 loop.close()asyncio.Task.all_tasks()拿到所有任务 然后依次迭代并使用任务.cancel()取消偏函数partial(函数,参数)把函数包装成另一个函数名 其参数必须放在定义函数的前面loop.call_soon(函数,参数)call_soon_threadsafe()线程安全 loop.call_later(时间,函数,参数)在同一代码块中call_soon优先执行,然后多个later根据时间的升序进行执行如果非要运行有阻塞的代码使用loop.run_in_executor(executor,函数,参数)包装成一个多线程,然后放入到一个task列表中,通过wait(task列表)来运行通过asyncio实现httpreader,writer=await asyncio.open_connection(host,port)writer.writer()发送请求async for data in reader: data=data.decode(“utf-8”) list.append(data)然后list中存储的就是htmlas_completed(tasks)完成一个返回一个,返回的是一个可迭代对象 协程锁async with Lock():Python进阶进程间通信:Manager(内置了好多数据结构,可以实现多进程间内存共享)from multiprocessing import Manager,Processdef add_data(p_dict, key, value): p_dict[key] = valueif name == “main”: progress_dict = Manager().dict() from queue import PriorityQueue first_progress = Process(target=add_data, args=(progress_dict, “bobby1”, 22)) second_progress = Process(target=add_data, args=(progress_dict, “bobby2”, 23)) first_progress.start() second_progress.start() first_progress.join() second_progress.join() print(progress_dict)Pipe(适用于两个进程)from multiprocessing import Pipe,Process#pipe的性能高于queuedef producer(pipe): pipe.send(“bobby”)def consumer(pipe): print(pipe.recv())if name == “main”: recevie_pipe, send_pipe = Pipe() #pipe只能适用于两个进程 my_producer= Process(target=producer, args=(send_pipe, )) my_consumer = Process(target=consumer, args=(recevie_pipe,)) my_producer.start() my_consumer.start() my_producer.join() my_consumer.join()Queue(不能用于进程池,进程池间通信需要使用Manager().Queue())from multiprocessing import Queue,Processdef producer(queue): queue.put(“a”) time.sleep(2)def consumer(queue): time.sleep(2) data = queue.get() print(data)if name == “main”: queue = Queue(10) my_producer = Process(target=producer, args=(queue,)) my_consumer = Process(target=consumer, args=(queue,)) my_producer.start() my_consumer.start() my_producer.join() my_consumer.join()进程池def producer(queue): queue.put(“a”) time.sleep(2)def consumer(queue): time.sleep(2) data = queue.get() print(data)if name == “main”: queue = Manager().Queue(10) pool = Pool(2) pool.apply_async(producer, args=(queue,)) pool.apply_async(consumer, args=(queue,)) pool.close() pool.join()sys模块几个常用方法argv 命令行参数list,第一个是程序本身的路径path 返回模块的搜索路径modules.keys() 返回已经导入的所有模块的列表exit(0) 退出程序a in s or b in s or c in s简写采用any方式:all() 对于任何可迭代对象为空都会返回True # 方法一 True in [i in s for i in [a,b,c]] # 方法二 any(i in s for i in [a,b,c]) # 方法三 list(filter(lambda x:x in s,[a,b,c]))set集合运用{1,2}.issubset({1,2,3})#判断是否是其子集{1,2,3}.issuperset({1,2}){}.isdisjoint({})#判断两个set交集是否为空,是空集则为True代码中中文匹配[u4E00-u9FA5]匹配中文文字区间[一到龥]查看系统默认编码格式 import sys sys.getdefaultencoding() # setdefaultencodeing()设置系统编码方式getattr VS getattributeclass A(dict): def getattr(self,value):#当访问属性不存在的时候返回 return 2 def getattribute(self,item):#屏蔽所有的元素访问 return item类变量是不会存入实例__dict__中的,只会存在于类的__dict__中globals/locals(可以变相操作代码)globals中保存了当前模块中所有的变量属性与值locals中保存了当前环境中的所有变量属性与值python变量名的解析机制(LEGB)本地作用域(Local)当前作用域被嵌入的本地作用域(Enclosing locals)全局/模块作用域(Global)内置作用域(Built-in)实现从1-100每三个为一组分组 print([[x for x in range(1,101)][i:i+3] for i in range(0,100,3)])什么是元类?即创建类的类,创建类的时候只需要将metaclass=元类,元类需要继承type而不是object,因为type就是元类type.bases #(<class ‘object’>,)object.bases #()type(object) #<class ’type’> class Yuan(type): def new(cls,name,base,attr,args,**kwargs): return type(name,base,attr,args,**kwargs) class MyClass(metaclass=Yuan): pass什么是鸭子类型(即:多态)?Python在使用传入参数的过程中不会默认判断参数类型,只要参数具备执行条件就可以执行深拷贝和浅拷贝深拷贝拷贝内容,浅拷贝拷贝地址(增加引用计数)copy模块实现神拷贝单元测试一般测试类继承模块unittest下的TestCasepytest模块快捷测试(方法以test_开头/测试文件以test_开头/测试类以Test开头,并且不能带有 init 方法)coverage统计测试覆盖率 class MyTest(unittest.TestCase): def tearDown(self):# 每个测试用例执行前执行 print(‘本方法开始测试了’) def setUp(self):# 每个测试用例执行之前做操作 print(‘本方法测试结束’) @classmethod def tearDownClass(self):# 必须使用 @ classmethod装饰器, 所有test运行完后运行一次 print(‘开始测试’) @classmethod def setUpClass(self):# 必须使用@classmethod 装饰器,所有test运行前运行一次 print(‘结束测试’) def test_a_run(self): self.assertEqual(1, 1) # 测试用例gil会根据执行的字节码行数以及时间片释放gil,gil在遇到io的操作时候主动释放什么是monkey patch?猴子补丁,在运行的时候替换掉会阻塞的语法修改为非阻塞的方法什么是自省(Introspection)?运行时判断一个对象的类型的能力,id,type,isinstancepython是值传递还是引用传递?都不是,python是共享传参,默认参数在执行时只会执行一次try-except-else-finally中else和finally的区别else在不发生异常的时候执行,finally无论是否发生异常都会执行except一次可以捕获多个异常,但一般为了对不同异常进行不同处理,我们分次捕获处理GIL全局解释器锁同一时间只能有一个线程执行,CPython(IPython)的特点,其他解释器不存在cpu密集型:多进程+进程池io密集型:多线程/协程什么是Cython将python解释成C代码工具生成器和迭代器实现__next__和__iter__方法的对象就是迭代器可迭代对象只需要实现__iter__方法使用生成器表达式或者yield的生成器函数(生成器是一种特殊的迭代器)什么是协程比线程更轻量的多任务方式实现方式yieldasync-awiatdict底层结构为了支持快速查找使用了哈希表作为底层结构哈希表平均查找时间复杂度为o(1)CPython解释器使用二次探查解决哈希冲突问题Hash扩容和Hash冲突解决方案循环复制到新空间实现扩容冲突解决:链接法二次探查(开放寻址法):python使用 for gevent import monkey monkey.patch_all() #将代码中所有的阻塞方法都进行修改,可以指定具体要修改的方法判断是否为生成器或者协程 co_flags = func.code.co_flags # 检查是否是协程 if co_flags & 0x180: return func # 检查是否是生成器 if co_flags & 0x20: return func 斐波那契解决的问题及变形#一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。#请问用n个21的小矩形无重叠地覆盖一个2n的大矩形,总共有多少种方法?#方式一:fib = lambda n: n if n <= 2 else fib(n - 1) + fib(n - 2)+fib(n-3)#方式二:def fib(n): a, b = 0, 1 for _ in range(n): a, b = b, a + b return b#一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。fib = lambda n: n if n < 2 else 2 * fib(n - 1)获取电脑设置的环境变量 import os os.getenv(env_name,None)#获取环境变量如果不存在为None垃圾回收机制引用计数标记清除分代回收 #查看分代回收触发 import gc gc.get_threshold() #output:(700, 10, 10)True和False在代码中完全等价于1和0,可以直接和数字进行计算,inf表示无穷大C10M/C10KC10M:8核心cpu,64G内存,在10gbps的网络上保持1000万并发连接C10K:1GHz CPU,2G内存,1gbps网络环境下保持1万个客户端提供FTP服务yield from与yield的区别:yield from跟的是一个可迭代对象,而yield后面没有限制GeneratorExit生成器停止时触发单下划线的几种使用在定义变量时,表示为私有变量在解包时,表示舍弃无用的数据在交互模式中表示上一次代码执行结果可以做数字的拼接(111_222_333)使用break就不会执行else10进制转2进制 def conver_bin(num): if num == 0: return num re = [] while num: num, rem = divmod(num,2) re.append(str(rem)) return “".join(reversed(re)) conver_bin(10)list1 = [‘A’, ‘B’, ‘C’, ‘D’] 如何才能得到以list中元素命名的新列表 A=[],B=[],C=[],D=[]呢 list1 = [‘A’, ‘B’, ‘C’, ‘D’] # 方法一 for i in list1: globals()[i] = [] # 可以用于实现python版反射 # 方法二 for i in list1: exec(f’{i} = []’) # exec执行字符串语句memoryview与bytearray$\color{#000}(不常用,只是看到了记载一下)$ # bytearray是可变的,bytes是不可变的,memoryview不会产生新切片和对象 a = ‘aaaaaa’ ma = memoryview(a) ma.readonly # 只读的memoryview mb = ma[:2] # 不会产生新的字符串 a = bytearray(‘aaaaaa’) ma = memoryview(a) ma.readonly # 可写的memoryview mb = ma[:2] # 不会会产生新的bytearray mb[:2] = ‘bb’ # 对mb的改动就是对ma的改动Ellipsis类型# 代码中出现…省略号的现象就是一个Ellipsis对象L = [1,2,3]L.append(L)print(L) # output:[1,2,3,[…]]lazy惰性计算 class lazy(object): def init(self, func): self.func = func def get(self, instance, cls): val = self.func(instance) #其相当于执行的area(c),c为下面的Circle对象 setattr(instance, self.func.name, val) return val` class Circle(object): def init(self, radius): self.radius = radius @lazy def area(self): print(’evalute’) return 3.14 * self.radius ** 2遍历文件,传入一个文件夹,将里面所有文件的路径打印出来(递归)all_files = [] def getAllFiles(directory_path): import os for sChild in os.listdir(directory_path): sChildPath = os.path.join(directory_path,sChild) if os.path.isdir(sChildPath): getAllFiles(sChildPath) else: all_files.append(sChildPath) return all_files文件存储时,文件名的处理#secure_filename将字符串转化为安全的文件名from werkzeug import secure_filenamesecure_filename(“My cool movie.mov”) # output:My_cool_movie.movsecure_filename(”../../../etc/passwd") # output:etc_passwdsecure_filename(u’i contain cool \xfcml\xe4uts.txt’) # output:i_contain_cool_umlauts.txt日期格式化from datetime import datetimedatetime.now().strftime("%Y-%m-%d")import time#这里只有localtime可以被格式化,time是不能格式化的time.strftime("%Y-%m-%d",time.localtime())tuple使用+=奇怪的问题# 会报错,但是tuple的值会改变,因为t[1]id没有发生变化t=(1,[2,3])t[1]+=[4,5]# t[1]使用append\extend方法并不会报错,并可以成功执行__missing__你应该知道class Mydict(dict): def missing(self,key): # 当Mydict使用切片访问属性不存在的时候返回的值 return key+与+=# +不能用来连接列表和元祖,而+=可以(通过iadd实现,内部实现方式为extends(),所以可以增加元组),+会创建新对象不可变对象没有__iadd__方法,所以直接使用的是__add__方法,因此元祖可以使用+=进行元祖之间的相加如何将一个可迭代对象的每个元素变成一个字典的所有键?dict.fromkeys([‘jim’,‘han’],21) # output:{‘jim’: 21, ‘han’: 21}wireshark抓包软件网络知识什么是HTTPS?安全的HTTP协议,https需要cs证书,数据加密,端口为443,安全,同一网站https seo排名会更高常见响应状态码 204 No Content //请求成功处理,没有实体的主体返回,一般用来表示删除成功 206 Partial Content //Get范围请求已成功处理 303 See Other //临时重定向,期望使用get定向获取 304 Not Modified //求情缓存资源 307 Temporary Redirect //临时重定向,Post不会变成Get 401 Unauthorized //认证失败 403 Forbidden //资源请求被拒绝 400 //请求参数错误 201 //添加或更改成功 503 //服务器维护或者超负载http请求方法的幂等性及安全性WSGI # environ:一个包含所有HTTP请求信息的dict对象 # start_response:一个发送HTTP响应的函数 def application(environ, start_response): start_response(‘200 OK’, [(‘Content-Type’, ’text/html’)]) return ‘<h1>Hello, web!</h1>‘RPCCDNSSL(Secure Sockets Layer 安全套接层),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议。SSH(安全外壳协议) 为 Secure Shell 的缩写,由 IETF 的网络小组(Network Working Group)所制定;SSH 为建立在应用层基础上的安全协议。SSH 是目前较可靠,专为远程登录会话和其他网络服务提供安全性的协议。利用 SSH 协议可以有效防止远程管理过程中的信息泄露问题。SSH最初是UNIX系统上的一个程序,后来又迅速扩展到其他操作平台。SSH在正确使用时可弥补网络中的漏洞。SSH客户端适用于多种平台。几乎所有UNIX平台—包括HP-UX、Linux、AIX、Solaris、Digital UNIX、Irix,以及其他平台,都可运行SSH。TCP/IPTCP:面向连接/可靠/基于字节流UDP:无连接/不可靠/面向报文三次握手四次挥手三次握手(SYN/SYN+ACK/ACK)四次挥手(FIN/ACK/FIN/ACK)为什么连接的时候是三次握手,关闭的时候却是四次握手?因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,“你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。XSS/CSRFHttpOnly禁止js脚本访问和操作Cookie,可以有效防止XSSMysql索引改进过程线性结构->二分查找->hash->二叉查找树->平衡二叉树->多路查找树->多路平衡查找树(B-Tree)Mysql面试总结基础篇Mysql面试总结进阶篇深入浅出Mysql清空整个表时,InnoDB是一行一行的删除,而MyISAM则会从新删除建表text/blob数据类型不能有默认值,查询时不存在大小写转换什么时候索引失效以%开头的like模糊查询出现隐士类型转换没有满足最左前缀原则对于多列索引,不是使用的第一部分,则不会使用索引失效场景:应尽量避免在 where 子句中使用 != 或 <> 操作符,否则引擎将放弃使用索引而进行全表扫描尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,即使其中有条件带索引也不会使用,这也是为什么尽量少用 or 的原因如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不会使用索引应尽量避免在 where 子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描例如:select id from t where substring(name,1,3) = ‘abc’ – name;以abc开头的,应改成:select id from t where name like ‘abc%’ 例如:select id from t where datediff(day, createdate, ‘2005-11-30’) = 0 – ‘2005-11-30’;应改为:不要在 where 子句中的 “=” 左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描如:select id from t where num/2 = 100 应改为:select id from t where num = 1002;不适合键值较少的列(重复数据较多的列)比如:set enum列就不适合(枚举类型(enum)可以添加null,并且默认的值会自动过滤空格集合(set)和枚举类似,但只可以添加64个值)如果MySQL估计使用全表扫描要比使用索引快,则不使用索引什么是聚集索引B+Tree叶子节点保存的是数据还是指针MyISAM索引和数据分离,使用非聚集InnoDB数据文件就是索引文件,主键索引就是聚集索引Redis命令总结为什么这么快?基于内存,由C语言编写使用多路I/O复用模型,非阻塞IO使用单线程减少线程间切换因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)。数据结构简单自己构建了VM机制,减少调用系统函数的时间优势性能高 – Redis能读的速度是110000次/s,写的速度是81000次/s丰富的数据类型原子 – Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行丰富的特性 – Redis还支持 publish/subscribe(发布/订阅), 通知, key 过期等等特性什么是redis事务?将多个请求打包,一次性、按序执行多个命令的机制通过multi,exec,watch等命令实现事务功能Python redis-py pipeline=conn.pipeline(transaction=True)持久化方式RDB(快照)save(同步,可以保证数据一致性)bgsave(异步,shutdown时,无AOF则默认使用)AOF(追加日志)怎么实现队列pushrpop常用的数据类型(Bitmaps,Hyperloglogs,范围查询等不常用)String(字符串):计数器整数或sds(Simple Dynamic String)List(列表):用户的关注,粉丝列表ziplist(连续内存块,每个entry节点头部保存前后节点长度信息实现双向链表功能)或double linked listHash(哈希):Set(集合):用户的关注者intset或hashtableZset(有序集合):实时信息排行榜skiplist(跳跃表)与Memcached区别Memcached只能存储字符串键Memcached用户只能通过APPEND的方式将数据添加到已有的字符串的末尾,并将这个字符串当做列表来使用。但是在删除这些元素的时候,Memcached采用的是通过黑名单的方式来隐藏列表里的元素,从而避免了对元素的读取、更新、删除等操作Redis和Memcached都是将数据存放在内存中,都是内存数据库。不过Memcached还可用于缓存其他东西,例如图片、视频等等虚拟内存–Redis当物理内存用完时,可以将一些很久没用到的Value 交换到磁盘存储数据安全–Memcached挂掉后,数据没了;Redis可以定期保存到磁盘(持久化)应用场景不一样:Redis出来作为NoSQL数据库使用外,还能用做消息队列、数据堆栈和数据缓存等;Memcached适合于缓存SQL语句、数据集、用户临时性数据、延迟查询数据和Session等Redis实现分布式锁使用setnx实现加锁,可以同时通过expire添加超时时间锁的value值可以是一个随机的uuid或者特定的命名释放锁的时候,通过uuid判断是否是该锁,是则执行delete释放锁常见问题缓存雪崩短时间内缓存数据过期,大量请求访问数据库缓存穿透请求访问数据时,查询缓存中不存在,数据库中也不存在缓存预热初始化项目,将部分常用数据加入缓存缓存更新数据过期,进行更新缓存数据缓存降级当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级一致性Hash算法使用集群的时候保证数据的一致性基于redis实现一个分布式锁,要求一个超时的参数setnx虚拟内存内存抖动LinuxUnix五种i/o模型阻塞io非阻塞io多路复用io(Python下使用selectot实现io多路复用)select并发不高,连接数很活跃的情况下poll比select提高的并不多epoll适用于连接数量较多,但活动链接数少的情况信号驱动io异步io(Gevent/Asyncio实现异步)比man更好使用的命令手册tldr:一个有命令示例的手册kill -9和-15的区别-15:程序立刻停止/当程序释放相应资源后再停止/程序可能仍然继续运行-9:由于-15的不确定性,所以直接使用-9立即杀死进程分页机制(逻辑地址和物理地址分离的内存分配管理方案):操作系统为了高效管理内存,减少碎片程序的逻辑地址划分为固定大小的页物理地址划分为同样大小的帧通过页表对应逻辑地址和物理地址分段机制为了满足代码的一些逻辑需求数据共享/数据保护/动态链接每个段内部连续内存分配,段和段之间是离散分配的查看cpu内存使用情况?topfree 查看可用内存,排查内存泄漏问题设计模式单例模式 # 方式一 def Single(cls,*args,**kwargs): instances = {} def get_instance (*args, **kwargs): if cls not in instances: instances[cls] = cls(*args, **kwargs) return instances[cls] return get_instance @Single class B: pass # 方式二 class Single: def init(self): print(“单例模式实现方式二。。。”) single = Single() del Single # 每次调用single就可以了 # 方式三(最常用的方式) class Single: def new(cls,*args,**kwargs): if not hasattr(cls,’_instance’): cls._instance = super().new(cls,*args,**kwargs) return cls._instance 工厂模式 class Dog: def init(self): print(“Wang Wang Wang”) class Cat: def init(self): print(“Miao Miao Miao”) def fac(animal): if animal.lower() == “dog”: return Dog() if animal.lower() == “cat”: return Cat() print(“对不起,必须是:dog,cat”)构造模式 class Computer: def init(self,serial_number): self.serial_number = serial_number self.memory = None self.hadd = None self.gpu = None def str(self): info = (f’Memory:{self.memoryGB}’, ‘Hard Disk:{self.hadd}GB’, ‘Graphics Card:{self.gpu}’) return ‘’.join(info) class ComputerBuilder: def init(self): self.computer = Computer(‘Jim1996’) def configure_memory(self,amount): self.computer.memory = amount return self #为了方便链式调用 def configure_hdd(self,amount): pass def configure_gpu(self,gpu_model): pass class HardwareEngineer: def init(self): self.builder = None def construct_computer(self,memory,hdd,gpu) self.builder = ComputerBuilder() self.builder.configure_memory(memory).configure_hdd(hdd).configure_gpu(gpu) @property def computer(self): return self.builder.computer数据结构和算法内置数据结构和算法python实现各种数据结构快速排序 def quick_sort(_list): if len(_list) < 2: return _list pivot_index = 0 pivot = _list(pivot_index) left_list = [i for i in _list[:pivot_index] if i < pivot] right_list = [i for i in _list[pivot_index:] if i > pivot] return quick_sort(left) + [pivot] + quick_sort(right)选择排序 def select_sort(seq): n = len(seq) for i in range(n-1) min_idx = i for j in range(i+1,n): if seq[j] < seq[min_inx]: min_idx = j if min_idx != i: seq[i], seq[min_idx] = seq[min_idx],seq[i]插入排序 def insertion_sort(_list): n = len(_list) for i in range(1,n): value = _list[i] pos = i while pos > 0 and value < _list[pos - 1] _list[pos] = _list[pos - 1] pos -= 1 _list[pos] = value print(sql)归并排序 def merge_sorted_list(_list1,_list2): #合并有序列表 len_a, len_b = len(_list1),len(_list2) a = b = 0 sort = [] while len_a > a and len_b > b: if _list1[a] > _list2[b]: sort.append(_list2[b]) b += 1 else: sort.append(_list1[a]) a += 1 if len_a > a: sort.append(_list1[a:]) if len_b > b: sort.append(_list2[b:]) return sort def merge_sort(_list): if len(list1)<2: return list1 else: mid = int(len(list1)/2) left = mergesort(list1[:mid]) right = mergesort(list1[mid:]) return merge_sorted_list(left,right)堆排序heapq模块 from heapq import nsmallest def heap_sort(_list): return nsmallest(len(_list),_list)栈 from collections import deque class Stack: def init(self): self.s = deque() def peek(self): p = self.pop() self.push(p) return p def push(self, el): self.s.append(el) def pop(self): return self.pop()队列 from collections import deque class Queue: def init(self): self.s = deque() def push(self, el): self.s.append(el) def pop(self): return self.popleft()二分查找 def binary_search(_list,num): mid = len(_list)//2 if len(_list) < 1: return Flase if num > _list[mid]: BinarySearch(_list[mid:],num) elif num < _list[mid]: BinarySearch(_list[:mid],num) else: return _list.index(num)面试加强题:关于数据库优化及设计如何使用两个栈实现一个队列反转链表合并两个有序链表删除链表节点反转二叉树设计短网址服务?62进制实现设计一个秒杀系统(feed流)?为什么mysql数据库的主键使用自增的整数比较好?使用uuid可以吗?为什么?如果InnoDB表的数据写入顺序能和B+树索引的叶子节点顺序一致的话,这时候存取效率是最高的。为了存储和查询性能应该使用自增长id做主键。对于InnoDB的主索引,数据会按照主键进行排序,由于UUID的无序性,InnoDB会产生巨大的IO压力,此时不适合使用UUID做物理主键,可以把它作为逻辑主键,物理主键依然使用自增ID。为了全局的唯一性,应该用uuid做索引关联其他表或做外键如果是分布式系统下我们怎么生成数据库的自增id呢?使用redis基于redis实现一个分布式锁,要求一个超时的参数setnx()如果redis单个节点宕机了,如何处理?还有其他业界的方案实现分布式锁码?使用hash一致算法缓存算法LRU(least-recently-used):替换最近最少使用的对象LFU(Least frequently used):最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小服务端性能优化方向使用数据结构和算法数据库索引优化慢查询消除slow_query_log_file开启并且查询慢查询日志通过explain排查索引问题调整数据修改索引批量操作,从而减少io操作使用NoSQL:比如Redis网络io批量操作pipeline缓存Redis异步Asyncio实现异步操作使用Celery减少io阻塞并发多线程Gevent ...

April 2, 2019 · 6 min · jiezi

你可能知道的 javaScript 数据结构与算法

本文已同步到github 你可能知道的 javaScript 数据结构与算法,欢迎Star。如果想阅读笔者更多文章,欢迎猛戳这里关于数据结构与算法,终于抽时间把之前看过的这两本书《学习JavaScript数据结构与算法》、《数据结构与算法JavaScript描述》,整理出来了一部分内容,由于最近较忙,先把已整理出来的内容发一下。对于未整理出来的内容会在后续整理出来,并更新到此文,也会随着对数据结构与算法不断的学习,不断优化更新此文,感兴趣的小伙伴可以先收藏哦。这两本书对前端来讲是很好的入门数据结构与算法的书,个人感觉《学习JavaScript数据结构与算法》这本书从排版以及思路上更清晰一些。另外,为了截图和验证方便,本文的例子大多是书中的例子。本文较长,如果阅读起来不方便,可链接到我的github中,单独查看。 github 数据结构 栈 队列 链表 集合 树 排序算法 冒泡排序 选择排序 插入排序 闲言少叙,直接开始了JavaScript 数据结构栈栈是一种遵从后进先出LIFO(Last In First Out,后进先出)原则的有序集合。新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另一端就叫栈底。定义一个栈的类,并为该栈声明一些方法,存储数据的底层数据结构使用数组class Stack { constructor() { this.dataStore = [] } // 向栈中添加一个或多个元素到栈顶 push() { for (let i = 0; i < arguments.length; i++) { this.dataStore.push(arguments[i]) } } // 移出栈顶元素,并返回被移出的元素 pop() { return this.dataStore.pop() } // 返回栈顶元素,不对栈做修改 peek() { return this.dataStore[this.dataStore.length - 1] } // 判断栈是否为空,如果为空返回true,否则返回false isEmpty() { return this.dataStore.length === 0 } // 清空栈 clear() { this.dataStore = [] } // 返回栈中元素的个数 size() { return this.dataStore.length }}// 栈的操作let stack = new Stack()stack.push(1, 2, 3)console.log(stack.dataStore) // [1, 2, 3]console.log(stack.pop()) // 3console.log(stack.dataStore) // [1, 2]console.log(stack.peek()) // 2console.log(stack.dataStore) // [1, 2]console.log(stack.size()) // 2console.log(stack.isEmpty()) // falsestack.clear()console.log(stack.dataStore) // []console.log(stack.isEmpty()) // trueconsole.log(stack.size()) // 0栈的应用本文举书中一个进制转换的例子并稍作修改,栈的类还是使用上面定义的Stackfunction transformBase(target, base) { let quotient; // 商 let remainder; // 余数 let binaryStr = ‘’; // 转换后的值 let digits = ‘0123456789ABCDEF’ // 对转换为16进制数做处理 let stack = new Stack() while(target > 0) { remainder = target % base stack.dataStore.push(remainder) target = Math.floor(target / base) } while(!stack.isEmpty()) { binaryStr += digits[stack.dataStore.pop()].toString() } return binaryStr}console.log(transformBase(10, 2)) // 1010console.log(transformBase(10, 8)) // 12console.log(transformBase(10, 16)) // A队列队列是遵循FIFO(First In First Out,先进先出,也称为先来先服务)原则的一组有序的项。队列在尾部添加新元素,并从顶部移除元素。最新添加的元素必须排在队列的末尾。其实队列和栈类似,只是原则不同,队列是先进先出,用代码来实现一个队列及操作队列的一些方法,以下用代码实现一个队列的类, 测试和栈类似,就不做具体测试了。class Queue { constructor() { this.dataStore = [] } // 入队 enqueue() { for (let i = 0; i < arguments.length; i++) { this.dataStore.push(arguments[i]) } } // 出队 dequeue() { return this.dataStore.shift() } // 返回队列第一个元素,不改变队列 front() { return this.dataStore[0] } // 队列是否为空 isEmpty() { return this.dataStore.length === 0 } // 返回队列的的元素个数 size() { return this.dataStore.length }}优先队列队列中在生活中有着大量应用,如登机时,商务舱要优于经济舱,这时候可以给队列中的元素设置优先级,下面用代码来实现一个优先队列的类class PriorityQueue { constructor() { this.dataStore = [] } isEmpty() { return this.dataStore.length === 0 } enqueue(element, priority) { function QueueElement(element, priority) { this.element = element this.priority = priority } // 定义每次往队列里添加的元素 let queueElement = new QueueElement(element, priority) if (this.isEmpty()) { // 如果每次队列为空直接添加到队列中 this.dataStore.push(queueElement) } else { // 定一个是否被添加到队列的标志 let isAdded = false for (let i = 0; i < this.dataStore.length; i++) { if (queueElement.priority < this.dataStore[i].priority) { // 优先级数值越小,代表优先级越高 this.dataStore.splice(i, 0, queueElement) isAdded = true break; } } if (!isAdded) { // 如果被添加的新元素优先级最低,添加到队尾 this.dataStore.push(queueElement) } } } //}let priorityQueue = new PriorityQueue()priorityQueue.enqueue(‘a’, 5)priorityQueue.enqueue(‘b’, 2)priorityQueue.enqueue(‘c’, 3)console.log(priorityQueue.dataStore)最后的队列如下图: 链表直接上代码:class LinkedList { constructor() { this.head = null // 链表的第一个元素 this.length = 0 } // 向链表尾部添加一个新元素 append(element) { let Node = function(element) { this.element = element this.next = null } let node = new Node(element) let currentNode; if (this.head == null) { // 如果链表head为null,表示链表无元素,直接把node赋值给head即可 this.head = node } else { currentNode = this.head while (currentNode.next) { // 每次循环会进行到链表的倒数第一个元素,把currentNode设置为倒数第一个元素 currentNode = currentNode.next } // 把新增的node赋值给currentNode的next属性,最后一个元素的next永远为null currentNode.next = node } // 链表的元素个数每次append后 +1 this.length++ } // 从链表中按位置删除元素 removeAt(position) { // position表示要移除元素的位置 let index = 0 let previous = null let currentNode = this.head if (position >= 0 && position < this.length) { while (index < position) { // 主要是找出position位置的元素,设置为currentNode previous = currentNode currentNode = currentNode.next index++ } // 把currentNode的上一个元素的next指向currentNode的下一个元素,就对应删除了currentNode previous.next = currentNode.next } else { // 表示链表中不存在这个元素,直接return null return null } // 删除后链表的元素个数每次删除减1 this.length–; // 返回删除的元素 return currentNode } // 按元素值删除元素 remove(element) { let index = this.indexOf(element) return this.removeAt(index) } // 向链表中插入新元素 insert(element, position) { // element表示被插入元素的具体值 // position表示被插入元素的位置 if (position >= 0 && position < this.length) { let index = 0 let previous = null let currentNode = this.head let Node = function(element) { this.element = element this.next = null } let node = new Node(element) while (index < position) { previous = currentNode currentNode = currentNode.next index++ } // 把当前元素的上一个元素的next设置为被插入的元素 previous.next = node // 把被插入元素的next设置为当前元素 node.next = currentNode // 链表元素个数加1 this.length++; // 如果插入元素成功,返回true return true } else { // 如果找不到插入元素位置,返回false return false } } // 查找元素在链表中的位置 indexOf(element) { let currentNode = this.head let index = 0 // 如果currentNode也就是head为空,则链表为空不会进入while循环,直接返回 -1 while (currentNode) { if (element === currentNode.element) { // 如果被找到,返回当前index return index } // 每一轮循环如果被查找元素还没有被找到,index后移一位,currentNode指向后一位元素,继续循环 index++ currentNode = currentNode.next } // 如果一直while循环结束都没找到返回 -1 return -1 } // 链表是否为空 isEmpty() { return this.length === 0 } // 链表元素个数 size() { return this.length }}let linkedList = new LinkedList()linkedList.append(‘a’)linkedList.append(‘b’)linkedList.append(‘c’)linkedList.append(’d’)linkedList.removeAt(2)linkedList.insert(’e’, 2)console.log(‘bIndex’, linkedList.indexOf(‘b’))console.log(‘fIndex’, linkedList.indexOf(‘f’))linkedList.remove(’d’)console.log(linkedList)上述代码测试结果如下图所示: 集合class Set { constructor() { this.items = {} } has(val) { return val in this.items } // 向集合中添加一个新的项 add(val) { this.items[val] = val } // 从集合中移除指定项 remove(val) { if (val in this.items) { delete this.items[val] return true } return false } // 清空集合 clear() { this.items = {} } // 返回集合中有多少项 size() { return Object.keys(this.items).length } // 提取items对象的所有属性,以数组的形式返回 values() { return Object.keys(this.items) } // 取当前集合与其他元素的并集 union(otherSet) { let unionSet = new Set() let values = this.values() for (let i = 0; i < values.length; i++) { unionSet.add(values[i]) } let valuesOther = otherSet.values() for (let i = 0; i < valuesOther.length; i++) { unionSet.add(valuesOther[i]) } return unionSet } // 取当前集合与其他元素的交集 intersection(otherSet) { let intersectionSet = new Set() let values = this.values() for (let i = 0; i < values.length; i++) { if (otherSet.has(values[i])) { intersectionSet.add(values[i]) } } return intersectionSet } // 取当前集合与其他元素的差集 diff(otherSet) { let intersectionSet = new Set() let values = this.values() for (let i = 0; i < values.length; i++) { if (!otherSet.has(values[i])) { intersectionSet.add(values[i]) } } return intersectionSet } // 判断当前集合是否是其他集合的子集 isSubSet(otherSet) { // 如果当前集合项的个数大于被比较的otherSet的项的个数,则可判断当前集合不是被比较的otherSet的子集 if (this.size() > otherSet.size()) { return false } else { let values = this.values() for (let i = 0; i < values.length; i++) { // 只要当前集合有一项不在otherSet中,则返回false if (!otherSet.has(values[i])) { return false } } // 循环判断之后,当前集合每一项都在otherSet中,则返回true return true } }}// 测试let setA = new Set()setA.add(‘a’)setA.add(‘b’)setA.add(‘c’)setA.remove(‘b’)console.log(setA.values()) // [‘a’, ‘c’]console.log(setA.size()) // 2let setB = new Set()setB.add(‘c’)setB.add(’d’)setB.add(’e’)let unionAB = setA.union(setB)console.log(unionAB.values()) // [‘a’, ‘c’, ’d’, ’e’]let intersectionAB = setA.intersection(setB)console.log(intersectionAB.values()) // [‘c’]let diffAB = setA.diff(setB)console.log(diffAB.values()) // [‘a’]let setC = new Set()setC.add(’d’)setC.add(’e’)let isSubSetCB = setC.isSubSet(setB)console.log(isSubSetCB) // truelet isSubSetAB = setA.isSubSet(setB)console.log(isSubSetAB) // false树一个树结构包含一系列存在父子关系的节点。每个节点都有一个父节点(除了顶部的第一个节点)以及零个或多个子节点二叉树// 创建一个键function createNode(key) { this.key = key this.left = null this.right = null}// 向树中插入键function insertNode(node, newNode) { if (newNode.key < node.key) { if (node.left === null) { node.left = newNode } else { insertNode(node.left, newNode) } } else { if (node.right === null) { node.right = newNode } else { insertNode(node.right, newNode) } }}// 遍历回调function printNode(value) { console.log(value)}// 中序遍历function inOrderTraverseNode(node, callback) { if (node !== null) { inOrderTraverseNode(node.left, callback) callback(node.key) // debugger 可以加入debugger,用浏览器控制观察Call Stack(执行环境栈)来分析程序执行过程 inOrderTraverseNode(node.right, callback) }}// 先序遍历function prevOrderTraverseNode(node, callback) { if (node !== null) { // 先访问节点本身 callback(node.key) // 再访问左侧节点 prevOrderTraverseNode(node.left, callback) // 然后再访问右侧节点 prevOrderTraverseNode(node.right, callback) }}// 后序遍历function postOrderTraverseNode(node, callback) { if (node !== null) { // 先访问左侧节点 postOrderTraverseNode(node.left, callback) // 再访问右侧节点 postOrderTraverseNode(node.right, callback) // 然后再访问节点本身 callback(node.key) }}class BinarySearchTree { constructor() { this.key = null } insert(key) { let newNode = new createNode(key) if (this.key === null) { this.key = newNode } else { insertNode(this.key, newNode) } } // 中序遍历访问节点(结果为按值由小到大访问) inOrderTraverse(callback) { inOrderTraverseNode(this.key, callback) } // 先序遍历访问节点(结果为先访问节点本身,再左侧节点,然后再访问右侧节点) prevOrderTraverse(callback) { prevOrderTraverseNode(this.key, callback) } // 后序遍历访问节点(结果为先访问左侧节点,再访问右侧节点,然后再访问节点本身) postOrderTraverse(callback) { postOrderTraverseNode(this.key, callback) } // 查找树中的最小值 findMin(node) { if (node) { while(node && node.left !== null) { node = node.left } return node.key } return null } // 查找树中的最小值对应的节点 findMinNode(node) { if (node) { while(node && node.left !== null) { node = node.left } return node } return null } // 查找树中的最大值 findMax(node) { if (node) { while(node && node.right !== null) { node = node.right } return node.key } return null } // 查找树中的特定值,如果存在返回true,否则返回false search(node, key) { if (node === null) { return false } if (key < node.key) { // 如果被查找的key小于节点值,从节点的左侧节点继续递归查找 return this.search(node.left, key) } else if (key > node.key) { // 如果被查找的key大于节点值,从节点的左侧节点继续递归查找 return this.search(node.right, key) } else { // 被查找的key等于node.key return true } } // 移除树中的特定节点 removeNode(node, key) { if (node === null) { return null } if (key < node.key) { node.left = this.removeNode(node.left, key) } else if (key > node.key) { node.right = this.removeNode(node.right, key) } else { // console.log(node) // 移除叶子节点(无左右节点的节点) if (node.left === null && node.right === null) { node = null return node } // 移除只有一个节点的节点(只有左节点或只有右节点) if (node.left === null) { node = node.right return node } else if (node.right === null) { node = node.left return node } // 移除有两个节点(既有左节点又有右节点) if (node.left && node.right) { // 1. 找到被移除节点的右节点下的最小节点,替换被移除的节点 let minRightNode = this.findMinNode(node.right) // 2. 把被移除节点的key设置为 被移除节点的右节点下的最小节点的key node.key = minRightNode.key // 3. 移除找到的那个最小节点 this.removeNode(node.right, node.key) // 4. 向被移除节点的父节点返回更新后节点的引用 return node } } }}测试如下:let tree = new BinarySearchTree()tree.insert(11)tree.insert(7)tree.insert(15)tree.insert(5)tree.insert(6)tree.insert(3)tree.insert(9)tree.insert(8)tree.insert(10)tree.insert(13)tree.insert(20)tree.insert(12)tree.insert(14)tree.insert(18)tree.insert(25)tree.inOrderTraverse(printNode) // 3 5 6 7 8 9 10 11 12 13 14 15 18 20 25tree.prevOrderTraverse(printNode) // 11 7 5 3 6 9 8 10 15 13 12 14 20 18 25tree.postOrderTraverse(printNode) // 3 6 5 8 10 9 7 12 14 13 18 25 20 15 11// tree.key为根节点,为了保持树不同层的结构一致,没有使用root为属性,使用了keylet minNodeVal = tree.findMin(tree.key)console.log(‘minNodeVal’, minNodeVal)let maxNodeVal = tree.findMax(tree.key)console.log(‘maxNodeVal’, maxNodeVal)let isHasNodeVal = tree.search(tree.key, 7)console.log(isHasNodeVal) // truetree.removeNode(tree.key, 15)console.log(tree) // 可以查看树的结构,15的这个节点的key已经被替换为18,并且key为18的节点已经被删除树的遍历1. 中序遍历2. 先序遍历3. 后序遍历移除节点的过程1. 移除以一个叶节点2. 移除只有一个左侧子节点或右侧子节点的节点3. 移除有两个子节点的节点JavaScript 算法排序算法1.冒泡排序冒泡排序的执行过程代码如下:let arr = [5, 4, 3, 2, 1]// 交换元素的位置function swap(arr, index1, index2) { var temp = arr[index1] arr[index1] = arr[index2] arr[index2] = temp}function bubbleSort(arr) { for (let i = 0; i < arr.length - 1; i++) { for (let j = 0; j < arr.length - i - 1; j++) { if (arr[j] > arr[j + 1]) { swap(arr, j, j + 1) } } }}bubbleSort(arr)console.log(arr) // [1, 2, 3, 4, 5]2.选择排序选择排序的执行过程代码如下:let arr = [5, 4, 3, 2, 1]function swap(arr, index1, index2) { var temp = arr[index1] arr[index1] = arr[index2] arr[index2] = temp}function changeSort(arr) { for (let i = 0; i < arr.length - 1; i++) { let minIndex = i for (let j = i; j < arr.length; j++) { if (arr[minIndex] > arr[j]) { minIndex = j } } if (i !== minIndex) { swap(arr, i, minIndex) } }}changeSort(arr)console.log(arr) // [1, 2, 3, 4, 5]3.插入排序插入排序的执行过程代码如下:let arr = [5, 4, 3, 2, 1]function swap(arr, index1, index2) { var temp = arr[index1] arr[index1] = arr[index2] arr[index2] = temp}function insertSort(arr) { for (let i = 1; i < arr.length; i++) { let j = i let temp = arr[i] while (j > 0 && arr[j - 1] > temp) { arr[j] = arr[j - 1] j– } arr[j] = temp }}insertSort(arr)console.log(arr) // [1, 2, 3, 4, 5]由于功力有限,本文有错误和不合理的地方,欢迎各位大神多多指正,非常感谢。参考文章:学习JavaScript数据结构与算法数据结构与算法JavaScript描述 ...

April 1, 2019 · 8 min · jiezi

数据结构与算法——二叉树(下)

概述前面的文章说到了二叉树,其实今天讲的二叉搜索(查找)树就是二叉树最常用的一种形式,它支持高效的查找、插入、删除操作,它的定义是这样的:对于树中的任意一个节点,其左子节点值必须小于该节点,其右子节点必须大于该节点。例如下图中的几种树都是二叉查找树:2. 二叉搜索树的查找我们直接拿查找的数据和根节点数据作比较,如果大于根节点,则在右子树中递归查找,如果小于根节点,则在左子树中查找,如果等于,则直接返回。就像下图的查找过程:结合代码能够更直观的理解:public class BinaryTree { private Node head = null;//树的根节点 //1.查找节点 public Node find(int value){ Node p = head; while (p != null){ if (p.getData() > value) p = p.left; else if (p.getData() < value) p = p.right; else return p; } return null; } //定义树的节点 public static class Node{ private int data; private Node left; private Node right; public Node(int data) { this.data = data; this.left = null; this.right = null; } public int getData() { return data; } }}3. 二叉搜索树的插入插入操作和查找其实比较的类似,都是需要拿插入的数据和树中的数据进行比较,如果插入的数据大于树节点数据,并且节点的右子树为空,则直接插入到右子树,否则继续在右子树中递归查找位置;如果插入的数据小于树节点数据,并且节点的左子树为空,则直接插入到左子树,否则继续在左子树中递归查找位置。结合代码理解一下: public void insert(int value){ Node node = new Node(value); if (head == null){ head = node; return; } Node p = head; while (p != null){ if (p.getData() > value){ if (p.left == null) { p.left = node; return; } p = p.left; } else { if (p.right == null) { p.right = node; return; } p = p.right; } } }4. 二叉搜索树的删除前面的查找和插入操作都比较的简单易懂,但是二叉搜索树的删除操作就比较的复杂了,分为了几种情况。第一种情况:要删除的节点没有子节点,这样的话,可以直接将指向该节点的父节点指针设为 null。第二种情况:要删除的节点只有一个子节点,直接将该节点的父节点的指针,指向该节点的子节点即可。第三种情况:要删除的节点有两个子节点,我们需要在删除节点的右子树中,寻找到最小的那个节点,然后将其放在删除的节点的位置上。三种情况对应下图:结合代码来理解一下: //3.删除数据 public void delete(int value){ Node p = head; Node pParent = null;//p节点的父节点 //先找到这个节点 while (p != null && p.getData() != value){ pParent = p; if (p.getData() > value)p = p.left; else p = p.right; } if (p == null) return;//表示没有找到值为value的节点 //1.假如要删除的节点有两个子节点 if (p.left != null && p.right != null){ //查找p节点的右子节点中的最小值 Node minP = p.right; Node minPP = p;//minPP表示minP的父节点 while (minP.left != null){ minPP = minP; minP = minP.left; } p.data = minP.getData(); if (minPP == p) p.right = null; else minPP.left = null; return; } //2.假如删除的节点p是叶子节点或只有一个子节点 Node child = null; if (p.left != null) child = p.left; else if (p.right != null) child = p.right; if (pParent == null) head = child; else if (pParent.left == p) pParent.left = child; else pParent.right = child; }5. 二叉搜索树分析最后,还有两个需要说明一下,前面说到了二叉树的三种遍历方式,其中,中序遍历的方式是先遍历节点的左子节点,再遍历这个节点本身,然后遍历节点的右子节点。所以,如果中序遍历二叉搜索树,会得到一个有序的数据,时间复杂度是 O(n),所以二叉搜索树又叫做二叉排序树。在理想的情况下,我们的二叉树是一棵满二叉树或者完全二叉树,那么查找、插入、删除操作十分的高效,时间复杂度是 O(logn),但是,如果二叉树的左右子树非常的不平衡,极端的情况下,可能会退化为链表,那么性能就下降了。所以,我们需要一种方式来维持二叉树的平衡,最好是将其维持为满二叉树或者完全二叉树,这就是后面会说到的平衡二叉查找树,常见的有 AVL 树,红黑树。

March 31, 2019 · 2 min · jiezi

数据结构(一)时间复杂度分析

简单的时间复杂度分析算法时间复杂度:O(1),O(n),O(lgn),O(nlogn),O(n^2)大O描述的是算法的运行时间和输入数据之间的关系看一个对输入数据进行求和的算法:1 public static int sum(int[] nums) {2 int sum = 0;3 for(int num: nums) sum += num;4 return sum;5 }第3行,对于nums中的每个数,都要进行这种操作,执行时间我们计为常量c1;第2行和第4行的执行时间计做常量c2;得出该算法的运行时间与输入数据(数组个数规模)之间是一种线性关系:T = c1n + c2分析时间复杂度时,忽略常数。线性关系的时间复杂度为O(n),因此该算法的时间复杂度为O(n)。再看下面的关系:T1 = 2n + 2 O(n)T2 = 2000n + 10000 O(n)T3 = 1nn + 0 O(n^2)当n等于10时,T2=12000,T3=100,T2时间明显超过T3,而O(n^2)的时间复杂度是高于O(n)的,不是违背了?其实,大O的表示,指的是渐进时间复杂度,描述的是n趋近于无穷时的情况。所以,在n趋于无穷时,高阶算法时间复杂度O(n^2) > 低阶算法时间复杂度O(n)是正确的。在实际中,对于n比较小的时候,有可能一个高阶算法的常数比较小,反而运行时间会快于低阶算法的。最典型的例子,比如高级的快速排序算法或归并算法,对于较小的数组转而使用插入排序这种方式来进行优化。利用的就是插入排序算法的常数比快速排序算法或归并排序算法的常数要小的原理;当n趋于无穷的情况下,同时存在高阶和低阶时,低阶是可以被忽略的,请看下面的时间复杂度:T1 = 300n + 10 O(n)T2 = 1n*n + 300n + 10 O(n^2)

March 31, 2019 · 1 min · jiezi

ArrayList 类(一)

ArrayList 类(一)ArrayList 类提供了 List ADT 的可增长数组的实现。一、自定义实现的 ArrayList 类 MyArrayList源码链接:戳此进GitHub查看MyArrayList 泛型类实现了 Iterable 接口从而可以拥有增强 for 循环(for each 循环)。public class MyArrayList<AnyType> implements Iterable<AnyType> { @Override public Iterator<AnyType> iterator() { return new ArrayListIterator(); } }1. 类属性MyArrayList 是基于数组实现的,其属性有:private static final int DEFAULT_CAPACITY = 10;private int theSize;private AnyType[] theArrays;其中常量 DEFAULT_CAPACITY 表示数组的基础容量。theSize 表示数组表当前长度(数组元素个数),作索引时,A[theSize - 1] 表示数组的最后一个元素,而 A[theSize] 表示新添加的项可以被放置的位置。泛型数组 theArrays 为 MyArrayList 类的数组实现,即对 MyArrayList 对象的操作实际为对数组 theArrays 的操作。2. 构造方法当实例化 MyArrayList 对象时,调用构造方法:public MyArrayList(){ theArrays = (AnyType[])new Object[DEFAULT_CAPACITY]; doClear(); }在构造方法中先实例化泛型数组 theArrays。由于泛型数组的创建是非法的,所以我们需要创建一个泛型类型限界的数组;即创建一个 Object[] 类型的数组,然后向泛型类型数组 AnyType[] 强制转型。然后调用 doClear() 方法对数组表进行清空、初始化的操作,此方法仅类内部可调用:private void doClear(){ theSize = 0; expandCapacity(DEFAULT_CAPACITY);}此处先设置 theSize = 0,然后调用 expandCapacity() 方法改变数组容量为基础容量。(在 expandCapacity() 方法的实现中,若扩充的容量(参数)小于 theSize 时表示非法的操作。)3. 成员方法数组扩容方法 expandCapacity() :public void expandCapacity(int newCapacity){ if (newCapacity < theSize){ return; } // 数组容量的扩充: AnyType[] newArrays = (AnyType[])new Object[newCapacity]; // 利用 System.arraycopy() 方法拷贝数组 System.arraycopy(theArrays,0,newArrays,0,theSize); theArrays = newArrays;}System.arraycopy() 方法: public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);get 方法的实现/** * 根据下标得到数组元素的值 /public AnyType get(int idx){ if (idx <0 || idx >= theSize){ throw new ArrayIndexOutOfBoundsException(); } return theArrays[idx];}set 方法的实现/* * 根据下标设置数组元素的值 * 返回该下标元素的原值 /public AnyType set(int idx,AnyType newVal){ if (idx <0 || idx >= theSize){ throw new ArrayIndexOutOfBoundsException(); } AnyType oldVal = theArrays[idx]; theArrays[idx] = newVal; return oldVal;}add 方法的实现 /* * 根据下标向数组插入新元素 /public boolean add(int idx,AnyType newVal){ // 当 idx=theSize 时,在 A[theSize] 位置处插入元素 if (idx < 0 || idx > theSize){ throw new ArrayIndexOutOfBoundsException(); } // 数组满时扩充容量 if (theArrays.length == theSize){ expandCapacity(theSize2 + 1); } // 数组元素后移 for (int i=theSize;i>idx;i–){ theArrays[i] = theArrays[i-1]; } theArrays[idx] = newVal; theSize++; return true;}/** * 在数组末尾插入新元素 /public boolean add(AnyType newVal){ add(theSize,newVal); return true;}remove 方法的实现/* * 根据下标删除元素 * 返回被删除的元素 /public AnyType remove(int idx){ if (idx < 0 || idx >= theSize){ throw new ArrayIndexOutOfBoundsException(); } // 数组元素前移 AnyType removedElem = theArrays[idx]; for (int i = idx; i < theSize-1; i++) { theArrays[i] = theArrays[i+1]; } theSize–; return removedElem;}4. Iterator 迭代器关于 Iterator 接口实现 Iterable 接口的集合必须提供 iterator 方法,该方法返回一个 Iterator (java.util.Iterator)类型的对象:public interface Iterator<AnyType> { boolean hasNext(); AnyType next(); void remove();}即每个集合均可通过 iterator 方法创建并返回给客户一个实现 Iterator 接口的对象,并把 当前位置 的概念在对象内部存储下来。根据当前位置项每次调用 hasNext() 来判断是否存在下一项,调用 next() 来给出下一项,而 remove() 方法则删除由 next() 方法最新返回的项(即当调用一次 remove() 后,直到对 next() 再调用一次后才能调用 remove() 方法)。例:public static <AnyType> void print(Collection<AnyType> coll){ Iterator<AnyType> itr = coll.iterator(); while(itr.hasNext()){ AnyType item = itr.next(); System.out.println(item); // itr.remove(); }}Java 中的增强 for 循环(for each)底层即是通过这种迭代器模式来实现的,当使用增强 for 循环时也就是间接的使用 Iterator。MyArrayList 中 Iterator 的实现import java.util.Iterator;import java.util.NoSuchElementException;public class MyArrayList<AnyType> implements Iterable<AnyType> { …… @Override public Iterator<AnyType> iterator() { return new ArrayListIterator(); } private class ArrayListIterator implements Iterator<AnyType>{ // 初始时当前位置为 0 private int current = 0; @Override public boolean hasNext() { return current < theSize; } @Override public AnyType next() { if (!hasNext()){ throw new NoSuchElementException(); } return theArrays[current++]; } @Override public void remove() { / 因为 remove() 方法有相同的,MyArrayList.this 表示外部类当前对象的一个引用 */ MyArrayList.this.remove(–current); } }}iterator() 方法直接返回 ArrayListIterator 类的一个实例,该类是一个实现 Iterator 接口的类。ArrayListIterator 存储当前位置的概念,并提供 hasNext()、next()、remove() 的实现。当前位置 表示要被查看的下一元素(的数组下标),因此初始化当前位置为 0。其中,泛型类 ArrayListIterator 是一个 内部类,使用内部类的目的及优点:next() 方法中使用当前位置作为下标访问数组元素然后将当前位置向后推进,而迭代器中是没有数组的,使用内部类可以访问外部类的域 theArrays;theArrays 是 MyArrayList 的私有域,使用内部类访问可以很好的满足面向对象编程的基本原则,即让数据尽可能地隐藏;满足迭代器 Iterator 的特性,即当集合(外部类)不存在的时候,迭代器(内部类)也是不存在的。二、MyArrayList 类各方法的算法分析因为 ArrayList 是基于数组实现的,所以和数组相似:ArrayList 的 get() 和 set() 方法花费常数时间 O(1);而 add() 和 remove() 方法需要挨个移动其余数组元素,所以其花费的时间代价为 O(n)。 ...

March 31, 2019 · 3 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

数据结构与算法——广度和深度优先搜索

概论前面说到了图这种非线性的数据结构,并且我使用了代码,简单演示了图是如何实现的。今天就来看看基于图的两种搜索算法,分别是广度优先搜索和深度优先搜索算法,这两个算法都十分的常见,在平常的面试当中也可能遇到。在图上面的搜索算法,其实主要的表现形式就是从图中的一个顶点,找到和另一个顶点之间的路径,而两种搜索算法,都是解决这个问题的。2. 广度优先搜索广度优先搜索的基本思路就是从一个顶点出发,层层遍历,直到找到目标顶点,其实这样搜索出来的路径也就是两个顶点之间的最短距离。如下图所示,例如要搜索出顶点 s -> t 的路径,搜索的方式就是这样的:其中黄色的线条表示搜索的节点,数字 1、2、3、4 表示搜索的次序,广度优先搜索的原理看起来十分的简单,但是它的代码实现还是稍微有点难的,先来看看整体的代码实现,然后再具体讲解一下:public class BFS { /** * 广度优先搜索算法 * @param graph 图 * @param s 搜索的起点(对应图中的一个顶点) * @param t 搜索的终点 */ public static void bfs(Graph graph, int s, int t){ if (s == t) return; //获得图的顶点个数 int vertex = graph.getVertex(); //获取存储图顶点的列表 LinkedList<Integer>[] list = graph.getList(); //如果某个顶点已经被访问,则设置为true boolean[] visited = new boolean[vertex]; visited[s] = true; //队列,存储的是已经被访问,但是其相连的顶点还没有被访问的顶点 Queue<Integer> queue = new LinkedList<>(); queue.add(s); //记录搜索的路径 int[] path = new int[vertex]; for (int i = 0; i < vertex; i++) { path[i] = -1; } while (queue.size() != 0){ int w = queue.poll(); for (int i = 0; i < list[w].size(); i++) { int q = list[w].get(i); if (!visited[q]){ path[q] = w; if (q == t){ print(path, s, t); return; } visited[q] = true; queue.add(q); } } } } //递归打印 s-t 的路径 private static void print(int[] prev, int s, int t){ if (prev[t] != -1 && t != s){ print(prev, s, prev[t]); } System.out.print(t + " “); }}程序中有三个辅助的变量:一是 boolean[] visited ,这个数组表示如果已经被访问,则设置为 true,例如顶点 s,是最开始被访问的,直接设置为 true。二是有一个队列 queue,它表示的是,一个顶点已经被访问,但是其相邻的顶点还没有被访问的顶点。例如顶点 s,它自己被访问了,但是和它相邻的两个顶点还没有被访问,因此直接被添加到了队列当中。三是数组 path,它表示一个顶点是被哪个顶点所访问的,数组下标表示的是顶点,对应存储的是由谁所访问。这个逻辑在代码中的体现便是 path[q] = w 这一行。最后,这个数组中存储的便是搜索的路径,需要递归打印出来。3. 深度优先搜索再来看看深度优先搜索,这种搜索的基本思路就是:从起始顶点出发,任意遍历顶点,如果走不通,则回退一个顶点,然后换一个顶点继续遍历,知道找到目标顶点。像下图这样:图中从顶点 s 出发,蓝色的表示前进的顶点,红色的表示后退一个顶点,直到找到目标顶点 t,相信你可以看出来,这样搜索出来的路径其实并不是 s 到 t 的最短路径,而是任意的一条路径。深度优先的代码实现:public class DFS { //判断是否找到了目标顶点 private static boolean found = false; public static void dfs(Graph graph, int s, int t){ int vertex = graph.getVertex(); boolean[] visited = new boolean[vertex]; int[] path = new int[vertex]; for (int i = 0; i < vertex; i++) { path[i] = -1; } LinkedList<Integer>[] list = graph.getList(); recursionDfs(list, s, t, visited, path); print(path, s, t); } //递归遍历 private static void recursionDfs(LinkedList<Integer>[] list, int w, int t, boolean[] visited, int[] path){ if (found) return; visited[w] = true; if (w == t){ found = true; return; } for (int i = 0; i < list[w].size(); i++) { int q = list[w].get(i); if (!visited[q]){ path[q] = w; recursionDfs(list, q, t, visited, path); } } } private static void print(int[] path, int s, int t){ if (path[t] != -1 && t != s){ print(path, s, path[t]); } System.out.print(t + " “); }}变量 boolean[] visited 和广度优先搜索一样,都是表示访问过的节点设置为 true,path 数组表示访问的路径。最后,总结一下,广度和深度优先搜索,都是比较暴力的搜索方式,没有什么优化,层层遍历或者一路递归,所以不难看出,这两个算法的时间复杂度接近 O(n),空间复杂度也是 O(n),还是稍微有点高的,所以这两种搜索算法适用于图上的顶点数据不太多的情况。

March 28, 2019 · 2 min · jiezi

JS数据结构与算法_排序和搜索算法

上一篇:JS数据结构与算法_树写在前面这是《学习JavaScript数据结构与算法》的最后一篇博客,也是在面试中常常会被问到的一部分内容:排序和搜索。在这篇博客之前,我每每看到排序头就是大的,心里想着类似“冒泡排序,两层遍历啪啪啪“就完事了,然后再也无心去深入研究排序相关的问题了。如果你也有类似的经历,希望下面的内容对你有一定帮助一、准备在进入正题之前,先准备几个基础的函数(1)交换数组两个元素function swap(arr, sourceIndex, targetIndex) { let temp = arr[sourceIndex]; arr[sourceIndex] = arr[targetIndex]; arr[targetIndex] = temp;}(2)快速生成0~N的数组 可点击查看更多生成方法function createArr(length) { return Array.from({length}, (_, i) => i);}(3)洗牌函数洗牌函数可快速打乱数组,常见的用法如切换音乐播放顺序function shuffle(arr) { for (let i = 0; i < arr.length; i += 1) { const rand = Math.floor(Math.random() * (i + 1)); if (rand !== i) { swap(arr, i, rand); } } return arr;}二、排序常见排序算法可以分为两大类:比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序在本篇博客中,仅对比较类排序的几种排序方式进行学习介绍2.1 冒泡排序冒泡排序是所有排序算法中最简单的,通常也是我们学习排序的入门方法。但是,从运行时间的角度来看,冒泡排序是最差的一种排序方式。核心:比较任何两个相邻的项,如果第一个比第二个大,则交换它们。元素项向上移动至正确的顺序,就好像气泡升至表面一样,冒泡排序因而得名动图:注意:第一层遍历找出剩余元素的最大值,至指定位置【依次冒泡出最大值】代码:function bubbleSort(arr) { const len = arr.length; for (let i = 0; i < len; i += 1) { for (let j = 0; j < len - 1 - i; j += 1) { if (arr[j] > arr[j + 1]) { // 比较相邻元素 swap(arr, j, j + 1); } } } return arr;}2.2 选择排序选择排序是一种原址比较排序算法。核心:首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕动图:注意:第一层遍历找出剩余元素最小值的索引,然后交换当前位置和最小值索引值【依次找到最小值】代码:function selectionSort(arr) { const len = arr.length; let minIndex; for (let i = 0; i < len - 1; i += 1) { minIndex = i; for (let j = i + 1; j < len; j += 1) { if (arr[i] > arr[j]) { minIndex = j; // 寻找最小值对应的索引 } } if (minIndex === i) continue; swap(arr, minIndex, i); } return arr;}2.3 插入排序插入排序的比较顺序不同于冒泡排序和选择排序,插入排序的比较顺序是当前项向前比较。核心:通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入动图:注意:从第二项开始,依次向前比较,保证当前项以前的序列是顺序排列代码:function insertionSort(arr) { const len = arr.length; let current, pointer; for (let i = 1; i < len; i += 1) { current = arr[i]; pointer = i; while(pointer >= 0 && current < arr[pointer - 1]) { // 每次向前比较 arr[pointer] = arr[pointer - 1]; // 前一项大于指针项,则向前移动一项 pointer -= 1; } arr[pointer] = current; // 指针项还原成当前项 } return arr;}2.4 归并排序归并排序和快速排序相较于上面三种排序算法在实际中更具有可行性(在第四小节我们会通过实践复杂度来比较这几种排序算法)JavaScript的Array类定义了一个sort函数(Array.prototype.sort)用以排序JavaScript数组。ECMAScript没有定义用哪个排序算法,所以浏览器厂商可以自行去实现算法。例如,Mozilla Firefox使用归并排序作为Array.prototype.sort的实现,而Chrome使用了一个快速排序的变体归并排序是一种分治算法。其思想是将原始数组切分成较小的数组,直到每个小数组只有一 个位置,接着将小数组归并成较大的数组,直到最后只有一个排序完毕的大数组。因此需要用到递归核心:归并排序,拆分成左右两块数组,分别排序后合并动图:注意:递归中最小的左右数组比较为单个元素的数组,因此在较上层多个元素对比时,左右两个数组一定是顺序的代码:function mergeSort(arr) { const len = arr.length; if (len < 2) return arr; // 递归的终止条件 const middle = Math.floor(len / 2); // 拆分左右数组 const left = arr.slice(0, middle); const right = arr.slice(middle); return merge(mergeSort(left), mergeSort(right));}function merge(left, right) { // 将左右两侧比较后进行合并 const ret = []; while (left.length && right.length) { if (left[0] > right[0]) { ret.push(right.shift()); } else { ret.push(left.shift()); } } while (left.length) { ret.push(left.shift()); } while (right.length) { ret.push(right.shift()); } return ret;}2.5 快速排序快速排序也许是最常用的排序算法了。它的复杂度为O(nlogn),且它的性能通常比其他的复 杂度为O(nlogn)的排序算法要好。和归并排序一样,快速排序也使用分治的方法,将原始数组分为较小的数组核心:分治算法,以参考值为界限,将比它小的和大的值拆开动图:注意:每一次遍历筛选出比基准点小的值代码:function quickSort(arr, left = 0, right = arr.length - 1) { // left和right默认为数组首尾 if (left < right) { let partitionIndex = partition(arr, left, right); quickSort(arr, left, partitionIndex - 1); quickSort(arr, partitionIndex + 1, right); } return arr;}function partition(arr, left, right) { let pivot = left; let index = left + 1; // 满足比较条件的依次放在分割点后 for (let i = index; i <= right; i += 1) { if (arr[i] < arr[pivot]) { swap(arr, i, index); index += 1; } } swap(arr, index - 1, pivot); // 交换顺序时,以最后一位替换分隔项 return index - 1;}三、搜索算法3.1 顺序搜索顺序或线性搜索是最基本的搜索算法。它的机制是,将每一个数据结构中的元素和我们要找的元素做比较。顺序搜索是最低效的一种搜索算法。function findItem(item, arr) { for (let i = 0; i < arr.length; i += 1) { if (item === arr[i]) { return i; } } return -1;}3.2 二分搜索二分搜索要求被搜索的数据结构已排序。以下是该算法遵循的步骤:选择数组的中间值如果选中值是待搜索值,那么算法执行完毕如果待搜索值比选中值要小,则返回步骤1在选中值左边的子数组中寻找如果待搜索值比选中值要大,则返回步骤1在选中值右边的子数组中寻找function binarySearch(item, arr) { arr = quickSort(arr); // 排序 let low = 0; let high = arr.length - 1; let mid; while (low <= high) { min = Math.floor((low + high) / 2); if (arr[mid] < item) { low = mid + 1; } else if (arr[mid] > item) { high = mid - 1; } else { return mid; } } return -1;}四、算法复杂度4.1 理解大O表示法大O表示法用于描述算法的性能和复杂程度。分析算法时,时常遇到一下几类函数(1)O(1)function increment(num){ return ++num;}执行时间和参数无关。因此说,上述函数的复杂度是O(1)(常数)(2)O(n)以顺序搜索函数为例,查找元素需要遍历整个数组,直到找到该元素停止。函数执行的总开销取决于数组元素的个数(数组大小),而且也和搜索的值有关。但是函数复杂度取决于最坏的情况:如果数组大小是10,开销就是10;如果数组大小是1000,开销就是1000。这种函数的时间复杂度是O(n),n是(输入)数组的大小(3)O(n2)以冒泡排序为例,在未优化的情况下,每次排序均需进行n*n次执行。时间复杂度为O(n2)时间复杂度O(n)的代码只有一层循环,而O(n2)的代码有双层嵌套循环。如 果算法有三层遍历数组的嵌套循环,它的时间复杂度很可能就是O(n3)4.2 时间复杂度比较(1)常用数据结构时间复杂度(2)排序算法时间复杂度上一篇:JS数据结构与算法_树参考:十大经典排序算法(动图演示) ...

March 27, 2019 · 3 min · jiezi

Java版-数据结构-队列(循环队列)

前情回顾在上一篇,笔者给大家介绍了数组队列,并且在文末提出了数组队列实现上的劣势,以及带来的性能问题(因为数组队列,在出队的时候,我们往往要将数组中的元素往前挪动一个位置,这个动作的时间复杂度O(n)级别),如果不清楚的小伙伴欢迎查看阅读。为了方便大家查阅,笔者在这里贴出相关的地址:Java版-数据结构-数组Java版-数据结构-栈Java版-数据结构-队列(数组队列)为了解决数组队列带来的问题,本篇给大家介绍一下循环队列。思路分析图解啰嗦一下,由于笔者不太会弄贴出来的图片带有动画效果,比如元素的移动或者删除(毕竟这样看大家比较直观),笔者在这里只能通过静态图片的方式,帮助大家理解实现原理,希望大家不要见怪,如果有朋友知道如何搞的话,欢迎在评论区慧言。在这里,我们声明了一个容量大小为8的数组,并标出了索引0-7,然后使用front和tail分别来表示队列的,队首和队尾;在下图中,front和tail的位置一开始都指向是了索引0的位置,这意味着当front == tai的时候 <font color = ‘red’>队列为空</font> 大家务必牢记这一点,以便区分后面介绍队列快满时的临界条件为了大家更好地理解下面的内容,在这里,我简单做几点说明front:表示队列队首,始终指向队列中的第一个元素(当队列空时,front指向索引为0的位置)tail:表示队列队尾,始终指向队列中的最后一个元素的下一个位置元素入队,维护tail的位置,进行tail++操作元素出队,维护front的位置,进行front++操作上面所说的,元素进行入队和出队操作,都简单的进行++操作,来维护tail和front的位置,其实是不严谨的,正确的维护tail的位置应该是(tail + 1) % capacity,同理front的位置应该是(front + 1) % capacity,这也是为什么叫做循环队列的原因,大家先在这里知道下,暂时不理解也没关系,后面相信大家会知晓。下面我们看一下,现在如果有一个元素a入队,现在的示意图:我们现在看到了元素a入队,我们的tail指向的位置发生了变化,进行了++操作,而front的位置,没有发生改变,仍旧指向索引为0的位置,还记得笔者上面所说的,front的位置,始终指向队列中的第一个元素,tail的位置,始终指向队列中的最后一个元素的下一个位置现在,我们再来几个元素b、c、d、e进行入队操作,看一下此时的示意图:想必大家都能知晓示意图是这样,好像没什么太多的变化(还请大家别着急,笔者这也是方便大家理解到底是什么循环队列,还请大家原谅我O(∩_∩)O哈!)看完了元素的入队的操作情况,那现在我们看一下,元素的出队操作是什么样的?元素a出队,示意图如下:现在元素a已经出队,front的位置指向了索引为1的位置,现在数组中所有的元素不再需要往前挪动一个位置这一点和我们的数组队列(我们的数组队列需要元素出队,后面的元素都要往前挪动一个位置)完全不同,我们只需要改变一下front的指向就可以了,由之前的O(n)操作,变成了O(1)的操作我们再次进行元素b出队,示意图如下:到这里,可能有的小伙伴会问,为什么叫做,循环队列?那么现在我们尝试一下,我们让元素f、g分别进行入队操作,此时的示意图如下:大家目测看下来还是没什么变化,如果此时,我们再让一个元素h元素进行入队操作,那么问题来了我们的tail的位置该如何指向呢?示意图如下:根据我们之前说的,元素入队:维护tail的位置,进行tail++操作,而此时我们的tail已经指向了索引为7的位置,如果我们此时对tail进行++操作,显然不可能(数组越界)细心的小伙伴,会发现此时我们的队列并没有满,还剩两个位置(这是因为我们元素出队后,当前的空间,没有被后面的元素挤掉),大家可以把我们的数组想象成一个环状,那么索引7之后的位置就是索引0如何才能从索引7的位置计算到索引0的位置,之前我们一直说进行tail++操作,笔者也在开头指出了,这是不严谨的,应该的是(tail + 1) % capacity这样就变成了(7 + 1) % 8等于 0 所以此时如果让元素h入队,那么我们的tail就指向了索引为0的位置,示意图如下:假设现在又有新的元素k入队了,那么tail的位置等于(tail + 1) % capacity 也就是(0 + 1)% 8 等于1就指向了索引为1的位置那么问题来了,我们的循环队列还能不能在进行元素入队呢?我们来分析一下,从图中显示,我们还有一个索引为0的空的空间位置,也就是此时tail指向的位置按照之前的逻辑,假设现在能放入一个新元素,我们的tail进行(tail +1) % capacity计算结果为2(如果元素成功入队,此时队列已经满了),此时我们会发现表示队首的front也指向了索引为2的位置如果新元素成功入队的话,我们的tail也等于2,那么此时就成了 tail == front ,一开始我们提到过,当队列为空的tail == front,现在呢,如果队列为满时tail也等于front,那么我们就无法区分,队列为满时和队列为空时收的情况了所以,在循环队列中,我们总是浪费一个空间,来区分队列为满时和队列为空时的情况,也就是当 ( tail + 1 ) % capacity == front的时候,表示队列已经满了,当front == tail的时候,表示队列为空。了解了循环队列的实现原理之后,下面我们用代码实现一下。代码实现接口定义 :Queue<E>public interface Queue<E> { /** * 入队 * * @param e / void enqueue(E e); /* * 出队 * * @return / E dequeue(); /* * 获取队首元素 * * @return / E getFront(); /* * 获取队列中元素的个数 * * @return / int getSize(); /* * 判断队列是否为空 * * @return / boolean isEmpty();}接口实现:LoopQueue<E>public class LoopQueue<E> implements Queue<E> { /* * 承载队列元素的数组 / private E[] data; /* * 队首的位置 / private int front; /* * 队尾的位置 / private int tail; /* * 队列中元素的个数 / private int size; /* * 指定容量,初始化队列大小 * (由于循环队列需要浪费一个空间,所以我们初始化队列的时候,要将用户传入的容量加1) * * @param capacity / public LoopQueue(int capacity) { data = (E[]) new Object[capacity + 1]; } /* * 模式容量,初始化队列大小 */ public LoopQueue() { this(10); } @Override public void enqueue(E e) { // 检查队列为满 if ((tail + 1) % data.length == front) { // 队列扩容 resize(getCapacity() * 2); } data[tail] = e; tail = (tail + 1) % data.length; size++; } @Override public E dequeue() { if (isEmpty()) { throw new IllegalArgumentException(“队列为空”); } // 出队元素 E element = data[front]; // 元素出队后,将空间置为null data[front] = null; // 维护front的索引位置(循环队列) front = (front + 1) % data.length; // 维护size大小 size–; // 元素出队后,可以指定条件,进行缩容 if (size == getCapacity() / 2 && getCapacity() / 2 != 0) { resize(getCapacity() / 2); } return element; } @Override public E getFront() { if (isEmpty()) { throw new IllegalArgumentException(“队列为空”); } return data[front]; } @Override public int getSize() { return size; } @Override public boolean isEmpty() { return front == tail; } // 队列快满时,队列扩容;元素出队操作,指定条件可以进行缩容 private void resize(int newCapacity) { // 这里的加1还是因为循环队列我们在实际使用的过程中要浪费一个空间 E[] newData = (E[]) new Object[newCapacity + 1]; for (int i = 0; i < size; i++) { // 注意这里的写法:因为在数组中,front 可能不是在索引为0的位置,相对于i有一个偏移量 newData[i] = data[(i + front) % data.length]; } // 将新的数组引用赋予原数组的指向 data = newData; // 充值front的位置(front总是指向队列中第一个元素) front = 0; // size 的大小不变,因为在这过程中,没有元素入队和出队 tail = size; } private int getCapacity() { // 注意:在初始化队列的时候,我们有意识的为队列加了一个空间,那么它的实际容量自然要减1 return data.length - 1; } @Override public String toString() { return “LoopQueue{” + “【队首】data=” + Arrays.toString(data) + “【队尾】” + “, front=” + front + “, tail=” + tail + “, size=” + size + “, capacity=” + getCapacity() + ‘}’; }}测试类:LoopQueueTestpublic class LoopQueueTest { @Test public void testLoopQueue() { LoopQueue<Integer> loopQueue = new LoopQueue<>(); for (int i = 0; i < 10; i++) { loopQueue.enqueue(i); } // 初始化队列数据 System.out.println(“原始队列: " + loopQueue); // 元素0出队 loopQueue.dequeue(); System.out.println(“元素0出队: " + loopQueue); loopQueue.dequeue(); System.out.println(“元素1出队: " + loopQueue); loopQueue.dequeue(); System.out.println(“元素2出队: " + loopQueue); loopQueue.dequeue(); System.out.println(“元素3出队: " + loopQueue); loopQueue.dequeue(); System.out.println(“元素4出队,发生缩容: " + loopQueue); // 队首元素 System.out.println(“队首元素:” + loopQueue.getFront()); }}测试结果:原始队列: LoopQueue{【队首】data=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, null]【队尾】, front=0, tail=10, size=10, capacity=10}元素0出队: LoopQueue{【队首】data=[null, 1, 2, 3, 4, 5, 6, 7, 8, 9, null]【队尾】, front=1, tail=10, size=9, capacity=10}元素1出队: LoopQueue{【队首】data=[null, null, 2, 3, 4, 5, 6, 7, 8, 9, null]【队尾】, front=2, tail=10, size=8, capacity=10}元素2出队: LoopQueue{【队首】data=[null, null, null, 3, 4, 5, 6, 7, 8, 9, null]【队尾】, front=3, tail=10, size=7, capacity=10}元素3出队: LoopQueue{【队首】data=[null, null, null, null, 4, 5, 6, 7, 8, 9, null]【队尾】, front=4, tail=10, size=6, capacity=10}元素4出队,发生缩容: LoopQueue{【队首】data=[5, 6, 7, 8, 9, null]【队尾】, front=0, tail=5, size=5, capacity=5}队首元素:5完整版代码GitHub仓库地址:Java版数据结构-队列(循环队列) 欢迎大家【关注】和【Star】至此笔者已经为大家带来了数据结构:静态数组、动态数组、栈、数组队列、循环队列;接下来,笔者还会一一的实现其它常见的数组结构,大家一起加油。静态数组动态数组栈数组队列循环队列链表循环链表二分搜索树优先队列堆线段树字典树AVL红黑树哈希表….持续更新中,欢迎大家关注公众号:小白程序之路(whiteontheroad),第一时间获取最新信息!!!笔者博客地址:http:www.gulj.cn ...

March 26, 2019 · 3 min · jiezi

Java版-数据结构-数组

数组知识点回顾声明Java数组时,会在内存中开辟一块连续指定大小的空间,用来存储固定大小的同类型元素在java中定义个名为scores,长度为8,类型为int类型的数组如下:public static void main(String[] args) { int[] scores = new int[8];}为了便于理解,我们看下它在内存的中的分布示意图:图中的一个个小格子是用来存放数组的元素,小格子上方的0-7数字,是数组中每个元素的下标(也可以叫索引),如果我们要查询数组中指定位置的元素,我们可以通过数组名[索引]来获取,比如图中的scores[2]在图中我们还可以看到,数组的起始下标是从0开始的(也就是第一个元素),最后一个元素的下标是7(也就是数组的长度8减1)由此类推,数组长度若是n,那么数组最后一个元素的下标是n-1(数组的起始下标总是从0开始的)各位不要闲唠叨哈,为了照顾所有人(其实我的内心是很纠结的。。。????)自定义数组类思路分析使用data属性表示存放数组的元素,使用capacity属性表示数组的容量(等价于数组的长度),但是真正自定义数组类的时候我们不需要显示声明,因为隐示等价于(Array.length)使用size属性表示数组中真正存放元素的个数(注意和capacity概念的区分)。我们画出示意图:下面我们来完成初始代码public class ArrayExample { /** * 存放数组的元素 / private int data[]; /* * 数组中元素的个数 / private int size; /* * 根据指定capacity容量初始化数组 * * @param capacity 容量 / public ArrayExample(int capacity) { data = new int[capacity]; size = 0; } /* * 无参构造函数,指定默认数组容量capacity=10 / public ArrayExample() { this(10); } /* * 获取数组中元素的个数 * * @return / public int getSize() { return size; } /* * 获取数组容量 * * @return / public int getCapacity() { return data.length; }}向数组中添加元素向指定位置添加元素假设现在数组的形态是这样,我们需要将77元素插入到索引为1的位置思路分析:把当前索引为1的位置元素以及后面的元素都向后挪一个位置,然后将77这个元素放到索引为1的位置(注意:挪位置的时候,我们应该从最后一个元素100向后挪一个位置,换句话说从后往前挪),完成之后,维护一下size的索引,进行size++操作(size是始终指向数组中下一个没有元素的位置)下面我们基于前面写的代码,来完成数组元素的添加操作/* * 在index位置插入元素 * * @param index 指定索引 * @param element 插入的元素 /public void add(int index, int element) { // 简单的边界判断 if (size == data.length) { throw new IllegalArgumentException(“数组添加失败,数组已满”); } if (index < 0 || index > size) { throw new IllegalArgumentException(“index索引不合法”); } // 从最后一个元素一直到size位置的元素,往后挪动一位 for (int i = size - 1; i >= index; i–) { data[i + 1] = data[i]; } // 位置赋值 data[index] = element; // 维护size大小 size++;}由于方面大家查看,只贴出添加数组元素的代码,本文文末,会贴出完成的代码示例地址现在如果我们想把一个元素添加到数组头部的位置或者尾部的位置,我们可以这么做/* * 向数组头的位置添加元素 * * @param element 元素 /public void addFirst(int element) { add(0, element);}/* * 向数组尾的位置添加元素 * * @param element 元素 /public void addLast(int element) { add(size, element);}大家一定要注重代码的复用性哈查询数组元素和修改数组元素查询数组元素/* * 获取index索引位置的元素 * * @param index 索引 * @return /public int get(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException(“获取失败,索引不合法”); } return data[index];}修改数组元素/* * 修改index索引位置的元素为element * * @param index 索引 * @param element 元素 /public void set(int index, int element) { if (index < 0 || index >= size) { throw new IllegalArgumentException(“获取失败,索引不合法”); } data[index] = element;}包含数组元素和搜索数组元素包含数组元素/* * 查找数组中是否有元素element * * @param element * @return /public boolean contains(int element) { return Arrays.stream(data).filter(x -> x == element).findAny().isPresent();}这里使用了Java8的lambda表达式,不知晓的盆友,可以自行去了解一下搜索数组元素/* * 查找数组中元素element所在的索引,如果不存在元素element,则返回-1 * * @param element * @return /public int find(int element) { for (int i = 0; i < data.length; i++) { if (data[i] == element) { return i; } } return -1;}删除数组中的元素现在我们要删除索引为1的元素77思路分析:我们知晓了数组的插入思路,那么数组的删除思路,刚好和数组的插入思路相反,如果要删除索引为1位置的元素77,我们只需要,从索引2开始,将索引2位置的元素向左移动到索引为1的位置,也就是将索引2位置的元素的值赋值给索引为1位置的元素(等价于data[1]=data[2]),依次类推,将索引为3位置的元素,移动到索引为2位置的元素,一直到最后一个元素,比如图中的元素100,完成之后,这时候,我们需要再次维护一下size的大小,我们要进行size–操作重要 size 既表示数组中元素的大小,又表示始终指向数组中第一个没有元素的位置代码完成数组的删除操作/* * 删除索引index位置的元素,并将删除的元素返回 * * @param index * @return /public int remove(int index) { // 简单判断数组索引的合法性 if (index < 0 || index >= size) { throw new IllegalArgumentException(“删除数组元素失败,索引不合法”); } // 存放删除指定索引的位置元素 int result = data[index]; // 从删除指定索引的后一个位置,一直往前挪一位,直到最后一个元素 for (int i = index + 1; i < size; i++) { data[i - 1] = data[i]; } // 维护size的位置 size–; return result;}完成了数组元素的删除操作,我们还可以便捷地为数组添加删除数组中第一个元素的方法和删除数组中最后一个元的方法。删除数组中第一个元素的方法/* * 删除数组中第一个元素,并将删除的元素进行返回 * * @return /public int removeFirst() { return remove(0);}删除数组中最后一个元素的方法/* * 删除数组中最后一个元素,并将删除的元素进行返回 * * @return */public int removeLast() { return remove(size - 1);}至此,我们已经完成了数组的增删改查操作,下面我们写个测试类,来使用一下我们自己写的简单版数组public class ArrayExampleTest { @Test public void testAdd() { // 初始化数组容量大小为5,目前数组中没有任何元素 ArrayExample arrayExample = new ArrayExample(5); System.out.println(arrayExample); // 向数组中欧添加第一个元素 arrayExample.addFirst(1); System.out.println(arrayExample); // 向数组中添加最后一个元素 arrayExample.addLast(2); System.out.println(arrayExample); // 向数组中索引为0的位置添加元素 arrayExample.add(0, 10); System.out.println(arrayExample); } // TODO 其它测试方法,读者可以自行测试}运行结果ArrayExample{data=[0, 0, 0, 0, 0], size=0,capacity=5}ArrayExample{data=[1, 0, 0, 0, 0], size=1,capacity=5}ArrayExample{data=[1, 2, 0, 0, 0], size=2,capacity=5}ArrayExample{data=[10, 1, 2, 0, 0], size=3,capacity=5}完整版代码GitHub仓库地址:Java版数据结构-数组 欢迎大家 关注和 Star本次我们完成的是静态数组的实现,往往静态数组不够灵活,后面笔者会在代码仓库中实现动态数组,就不作为一个篇幅来讲解了,接下来,笔者还会一一的实现其它常见的数组结构。静态数组动态数组栈队列链表循环链表二分搜索树优先队列堆线段树字典树AVL红黑树哈希表….持续更新中,欢迎大家关注公众号:小白程序之路(whiteontheroad),第一时间获取最新信息!!!笔者博客地址:http:www.gulj.cn ...

March 26, 2019 · 3 min · jiezi

Java版-数据结构-队列(数组队列)

前言看过笔者前两篇介绍的Java版数据结构数组和栈的盆友,都给予了笔者一致的好评,在这里笔者感谢大家的认可!!!由于本章介绍的数据结构是队列,在队列的实现上会基于前面写的动态数组来实现,而队列又和栈不论是从特点上和操作上都有类似之处,所以在这里对这两种数据结构不了解的朋友,可以去看一下笔者前两篇文章介绍的数据结构数组和栈,这里笔者把链接贴出来(看过的盆友可以跳过此步骤…)Java版-数据结构-数组Java版-数据结构-栈介绍队列是一种特殊的线性表,它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列的操作方式和栈类似,唯一的区别在于队列只允许新数据在后端(rear)进行添加。特点队列是一种线性结构只能从一端(队尾)添加元素,从另一端(队首)取出元素先进先出,First In First Out(FIFO)之前在介绍栈的时候,通过示意图来帮助大家了解什么是栈;这里,我仍采用示意图形式向大家演示队列常用的两个操作:入队操作和出队操作。队列<font color=‘green’>入队操作</font>这里我们可以形象地想成我们到银行办理业务排队的场景,现在A、B、C三个元素分别到银行柜台排成一条队办理业务(我们都是文明的孩纸,总不能插队O(∩∩)O哈!),依次排队的元素是:A、B、C。队列<font color=‘green’>出队操作</font>当元素A办理完业务时,当前是元素A先离开队列,然后是元素B,最后是元素C我们时刻要牢记队列,入队是从队尾一端进行入队,出队是从队首一端进行出队,是一种:先进先出的数据结构。本文会介绍队列的两张实现方式,一种是数组队列,另外一种是循环队列,考虑篇幅长度原因,本篇我们暂时只介绍数组队列,循环队列放在下一篇介绍。数组队列(<font color=‘green’>底层基于数组实现</font>)底层原理分析现在我们声明一个数组的长度(capacity=3),元素个数为(size=0)的int类型数组的空队列,在这里,假设对队列的队首为数组的左侧,队尾为数组的右侧,示意图如下:现在如果我们有四个元素:A、B、C、D要入队元素A入队元素A已经入队了,现在开始元素B入队元素A和元素B已经入队了,现在开始元素C入队元素A、B和C已经分别入队了,现在如果我们要开始元素D入队,根据我们之前定义的动态数组的特性,如果元素D进行入队操作,会发现此时我们的数组已经满了,这时候数组会自动地扩容(扩容的原理:新建一个容量是原数组容量两倍的数组,把原数组中的元素依次拷贝到新的数组中,最后引用指向新的数组)的原来的两倍(具体扩容多少,盆友可以自行设置)示意图如下:到这里我们已经完成了元素:A、B、C、D的入队操作了,现在我们来看一下,它们的出队操作,根据队列的特性,队列是一种先进先出的数据结构,之前入队操作顺序依次是:A->B->C->D,那么出队操作顺序仍然是:A->B->C->D现在我们来看一下元素A和元素B出队后的示意图:元素C和D的出队原理和元素A出队的原理一样,直至全部出队完成,变成空队列在元素出队的过程中,相应地也会进行缩容操作,之前笔者这边定义,当数组中元素的个数(size)等于数组容量(capacity)的一半时,数组会进行缩容操作,这也正是动态数组的特点。了解了数组队列的底层原理之后,接下来我们用代码来实现一下(建议盆友,在看之前,自己可以尝试写一下,然后在看,这样印象可能会比较深刻O(∩∩)O哈!)队列基本操作向队列中添加元素(入队)void enqueue(E e);从队列中取出元素(出队)E dequeue();获取队首元素E getFront();获取队列中元素个数int getSize();判断队列是否为空boolean isEmpty();代码实现接口定义 :Queue<E>public interface Queue<E> { /** * 入队 * * @param e / void enqueue(E e); /* * 出队 * * @return / E dequeue(); /* * 获取队首元素 * * @return / E getFront(); /* * 获取队列中元素的个数 * * @return / int getSize(); /* * 判断队列是否为空 * * @return / boolean isEmpty();}DynamicArrayQueue<E> 类实现接口 Queue<E>public class DynamicArrayQueue<E> implements Queue<E> { /* * 用数组存放队列中元素的个数 / private DynamicArray<E> dynamicArray; /* * 指定容量,初始化队列 * * @param capacity / public DynamicArrayQueue(int capacity) { dynamicArray = new DynamicArray<>(capacity); } /* * 默认容量,初始化队列 */ public DynamicArrayQueue() { dynamicArray = new DynamicArray<>(); } @Override public void enqueue(E e) { dynamicArray.addLast(e); } @Override public E dequeue() { return dynamicArray.removeFirst(); } @Override public E getFront() { return dynamicArray.getFirst(); } @Override public int getSize() { return dynamicArray.getSize(); } @Override public boolean isEmpty() { return dynamicArray.isEmpty(); } @Override public String toString() { return “DynamicArrayQueue{” + “【队首】dynamicArray=” + dynamicArray + “}【队尾】”; }}测试类: DynamicArrayQueueTestpublic class DynamicArrayQueueTest { @Test public void testArrayQueue() { // 指定容量(capacity=6)初始化队列 DynamicArrayQueue<String> dynamicArrayQueue = new DynamicArrayQueue(3); System.out.println(“初始队列:” + dynamicArrayQueue); // 准备入队元素 List<String> enQueueElements = Arrays.asList(“A”, “B”, “C”); // 元素入队 enQueueElements.forEach(x -> dynamicArrayQueue.enqueue(x)); System.out.println(“元素A、B、C入队:” + dynamicArrayQueue); // 此时如果又有一个元素D入队,会发生扩容操作 (size == capacity)进行扩容 dynamicArrayQueue.enqueue(“D”); System.out.println(“元素D入队,发生扩容:” + dynamicArrayQueue); // 元素A出队,会发生缩容操作(size == capacity / 2)进行缩容 dynamicArrayQueue.dequeue(); System.out.println(“元素A出队,发生缩容:” + dynamicArrayQueue); // 元素B出队 dynamicArrayQueue.dequeue(); System.out.println(“元素B出队:” + dynamicArrayQueue); }}运行结果初始队列:DynamicArrayQueue{【队首】dynamicArray=DynamicArray{data=[null, null, null], size=0,capacity=3}}【队尾】元素A、B、C入队:DynamicArrayQueue{【队首】dynamicArray=DynamicArray{data=[A, B, C], size=3,capacity=3}}【队尾】元素D入队,发生扩容:DynamicArrayQueue{【队首】dynamicArray=DynamicArray{data=[A, B, C, D, null, null], size=4,capacity=6}}【队尾】元素A出队,发生缩容:DynamicArrayQueue{【队首】dynamicArray=DynamicArray{data=[B, C, D], size=3,capacity=3}}【队尾】元素B出队:DynamicArrayQueue{【队首】dynamicArray=DynamicArray{data=[C, D, null], size=2,capacity=3}}【队尾】细心的盆友,会发现,因为队列的底层是数组来实现的,队列的出队操作实际上就是:删除数组中的第一个元素,后面的所有元素都要往前面挪一位;其实这样性能是比较低下的,时间复杂度是O(n)级别的。我们想如果元素进行出队操作后,能否不挪动后面的元素,还能维持队列的特性,这样问题不就解决了吗?盆友可以自行思考一下。完整版代码GitHub仓库地址:Java版数据结构-队列(数组队列) 欢迎大家【<font color=‘red’>关注</font>】和【<font color=‘red’>Star</font>】本篇完成的数组队列是基于之前【Java版-数据结构-数组】动态数组来实现的,下一篇笔者会给大家介绍用循环队列来解决数组队列带来的性能问题。接下来,笔者还会一一的实现其它常见的数组结构。静态数组动态数组栈数组队列循环队列链表循环链表二分搜索树优先队列堆线段树字典树AVL红黑树哈希表….持续更新中,欢迎大家关注公众号:<font color=‘green’>小白程序之路(whiteontheroad)</font>,第一时间获取最新信息!!!笔者博客地址:http:www.gulj.cn ...

March 26, 2019 · 2 min · jiezi

Java版-数据结构-栈

介绍栈是一种后进先出的线性表数据结构,分为栈顶和栈底两端,仅允许在表的一端插入元素,这一端被称为栈顶,另外一端称之为栈底。栈,只有两种操作,分为入栈(压栈)和出栈(退栈);向栈中添加元素的操作叫做入栈,相反从栈中删除元素叫做出栈。特点只能从栈顶添加元素或者删除元素后进先出的数据结构,Last In First Out(LIFO)为了大家更好的形象了解我们通过示意图来看一下栈的入栈和出栈操作<font color=‘green’>入栈操作示意图</font><font color=‘green’>出栈操作示意图</font>(后进的元素先出)栈的基本操作向栈中添加一个元素(入栈)void push(E e)从栈中删除一个元素(出栈)E pop()查看栈顶元素E peek()查看栈中元素个数int getSize()判断栈是否为空boolean isEmpty()实现栈的方式,实际上底层有多种实现方式,比如:动态数组等,这里我们使用Java语言本身为我们提供的集合LinkedList接口定义:Stack<E>public interface Stack<E> { /** * 向栈中添加元素 * * @param e / void push(E e); /* * 从栈中删除元素 / void pop(); /* * 获取栈顶元素 * * @return / E peek(); /* * 获取栈中元素个数 * * @return / int getSize(); /* * 判断栈中是否为空 * * @return / boolean isEmpty();}LinkedListStack<E> 类实现接口Stack<E>public class LinkedListStack<E> implements Stack<E> { /* * 存放栈元素 / LinkedList<E> list; /* * 构造栈结构 */ public LinkedListStack() { list = new LinkedList<>(); } @Override public void push(E e) { list.addLast(e); } @Override public void pop() { list.removeLast(); } @Override public E peek() { return list.getLast(); } @Override public int getSize() { return list.size(); } @Override public boolean isEmpty() { return list.isEmpty(); } @Override public String toString() { return “LinkedListStack{” + “list=” + list + ‘}’; }}测试类:LinkedListStackTest@Testpublic void testLinkedListStack() { // 栈 Stack<String> stack = new LinkedListStack<>(); // 准备入栈元素 List<String> prepareElements = Arrays.asList(“A”, “B”, “C”, “D”, “E”); // 入栈 prepareElements.forEach(x -> { stack.push(x); System.out.println(“入栈操作:” + stack); }); // 出栈 stack.pop(); System.out.println(“出栈操作:” + stack); // 获取栈顶元素 String peekElement = stack.peek(); System.out.println(“栈顶元素:” + peekElement); // 获取栈中元素的个数 int stackSize = stack.getSize(); System.out.println(“栈中元素个数:” + stackSize);}运行结果入栈操作:LinkedListStack{list=[A]}入栈操作:LinkedListStack{list=[A, B]}入栈操作:LinkedListStack{list=[A, B, C]}入栈操作:LinkedListStack{list=[A, B, C, D]}入栈操作:LinkedListStack{list=[A, B, C, D, E]}出栈操作:LinkedListStack{list=[A, B, C, D]}栈顶元素:D栈中元素个数:4栈的应用虚拟机栈的入栈和出栈操作在Java虚拟机运行时数据区有一块被称之为:虚拟机栈,它是线程私有的,声明周期与线程相同。我们编写的每个Java方法,每个方法都会在执行的时候同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应这一个栈帧在虚拟机栈中入栈到出栈的过程。现在我们假设有A、B、C三个方法,在A方法中调用B方法(A->B),在B方法中调用C方法(B->C),C方法执行本方法业务逻辑。当程序执行到A()方法的中的第二行时,此时程序会中断A()方法并开始调用B()方法,然后会在虚拟机栈中记录调用B()方法的栈帧,我这里暂且称之为A2(实际存储的并不是O(∩_∩)O哈!)示意图如下:同理,当程序执行到B()方法中第二行时,此时程序也会中断B()方法开始调用C()方法,然后同样地会在虚拟机栈中生成调用C()方法的栈帧并记录,我这里暂且称之为B2,示意图如下:当程序开始执行到C()方法时,直到执行完C()方法时,这时候,程序该如何执行呢?此时就要查看一下虚拟机栈了,发现虚拟机栈,栈中栈顶的元素是B2,我们的程序就知道了,它是执行到B()方法的B2位置就中断了,去执行C()方法了;现在C()方法执行完成之后,它就可以跳回到B2的位置继续执行了,当B()方法执行完之后,虚拟机栈中的B2栈帧也就可以出栈了,依次类推….如果一个方法,使用递归调用,若递归临界点判断有误,则方法就会一直的被进行入栈操作,如果超过虚拟机栈的默认容量大小,则会出现我们常见的 StackOverflowError 异常完整版代码GitHub仓库地址:Java版数据结构-栈 欢迎大家【关注】和【Star】本次我们完成的是基于Java自身自带的集合LinkedList来实现栈,有兴趣的童鞋,可以使用动态数组方式来实现;接下来,笔者还会一一的实现其它常见的数组结构。静态数组动态数组栈队列链表循环链表二分搜索树优先队列堆线段树字典树AVL红黑树哈希表….持续更新中,欢迎大家关注公众号:小白程序之路(whiteontheroad),第一时间获取最新信息!!!笔者博客地址:http:www.gulj.cn ...

March 26, 2019 · 1 min · jiezi

数据结构与算法——散列表

什么是散列表?散列表(Hash Table)又叫做哈希表,是一种很常用的数据结构。散列表其实是基于数组实现的,可以说,没有数组就没有散列表。先来举一个简单的例子,来认识一下什么是散列表。假如在学校的运动会上,每个运动员的胸前都会标识自己的号码,编号是1,2,3……,这样的话,我们可以很容易的将运动员信息存储在数组当中,运动员的编号就是数组的下标。但是会存在这样一种情况,假如运动员的编号不是顺序排列的,而是需要加上更多的信息,比如年级,班级,例如一个运动员的编号是15030711,15是年级,03是专业,07是班级,11是顺序号,这样的话我们该怎么存储运动员信息呢?其实也不难,我们只需要截取运动员编号的最后两位,作为数组的下标存储在数组中,当需要根据编号查询信息的时候,我们也同样截取编号最后两位来进行查询。这就是很典型的散列思想。选手的编号叫做 键 , 把运动员编号转换为数组下标的方法叫做 散列函数(或哈希函数), 通过散列函数计算得到的值叫做 散列值(或哈希值) 。根据下图你更能理解散列表:2. 哈希函数结合上面的理解,你应该可以想到,其实散列表的关键就在于哈希函数的实现。哈希函数,顾名思义,其实就是一个函数,key 就是键值,经过 hash(key) 得到的值就是哈希值。哈希函数的设计有三个原则:通过哈希函数计算得到的哈希值是一个非负的整数。如果 key1 = key2,那么 hash(key1) = hash(key2)。如果 key1 != key2,那么 hash(key1) != hash(key2)。前面两点其实很好理解,第一点,要求是一个非负的整数,这是因为数组的下标是从 0 开始的,第二点,如果 key 相同,那么通过哈希函数得到的哈希值也相同。第三点稍微有点不好理解,key1 不等于 key2,那么哈希值也是不相等的,这只是一种理想的状况,但是在实际情况中,无法避免这种哈希冲突 。3. 哈希冲突哈希冲突,又叫哈希碰撞,是哈希函数可能会遇到的问题,即不同的 key 值经过哈希函数计算之后,可能得到相同的哈希值,那么这种状况该怎么解决呢?一般是通过两种方式:开放寻址法链表法开放寻址法可以通过线性探测这种方式来实现,比如我们的一个 key 经过哈希函数得到哈希值之后,相应的存储位置已经被占用,那么我们遍历散列表,找到一个空闲的位置,将数据插入。例如下图,标记为黄色的是已经有数据,标记为红色的是空闲空间,一个 key 经过 hash 哈希函数之后的存储位置为 2,但是下标为 2 的的地方已经有数据了,所以就重新探测一个空的位置。第二种方式是链表法,这种方式会更加简单,也更加适用。例如下图,在每一存储位置,都会有相应的链表,如果哈希值相同,我们直接将数据存放在存储位置对应的链表中。但是这种方式也可能会存在问题,比如说哈希函数设计的不合理,导致大量的数据都集中在一条链表中,这样的话,数据的插入和查找速度就会急剧退化为O(n)。针对这种情况,我们可以使用更加优秀的动态数据结构代替链表,例如红黑树、跳表等。这样,就算数据全都集中在一个节点上,数据的查询效率也不会下降得太厉害。4. 散列表的具体应用其实,散列表和链表在很多时候都是结合在一起使用的,接下来就看看散列表的两个具体应用:LRU(最近最少使用策略,Least Recently Used)缓存淘汰算法和 Java 的 LinkedHashMap。1.LRU 缓存淘汰算法首先,该怎么理解 LRU,即最近最少使用策略呢?举个简单的例子,比如你买了很多书,书架上渐渐放满了,当你有新的书的时候,需要将原来的书拿掉一些,腾出新的位置来。这样的话,你肯定会拿掉那些最近很少使用到的那些书,这就是一种最近最少使用策略。其实可以用单链表实现一个LRU缓存淘汰算法,具体可以这样做:我们维护一个有限的缓存空间,如果空间不够,需要淘汰缓存的话,我们直接将链表头部的数据删除即可。当要缓存某个数据的时候,我们需要查找这个数据,如果找到了,将其放置在链表尾部,如果没找到,则将数据插入到链表尾部。因为涉及到的查找操作需要遍历链表,时间复杂度是O(n),所以我们可以用散列表加上双向链表来实现,将时间复杂度降为O(1)。具体该怎么实现呢?先来看看下面实现的图:首先,如果空间不够,我们直接将双向链表头部的元素删除;查找一个元素,我们可以在接近 O(1) 的时间复杂度找到该元素,并且将其插入到链表的尾部;删除一个元素,由于双向链表保存了上一个链表的指针,所以能够在O(1) 的时间内删除;添加一个元素,如果此元素已经在链表中,则直接将该元素插入到链表尾部,如果不在链表中,直接将元素插入到链表尾部,如果缓存满了,则删除链表头部元素之后才添加。2.LinkedHashMap如果熟悉 Java 的话,肯定会经常用到 LinkedHashMap 这个容器,它与 HashMap 唯一的区别就是,LinkedHashMap 能够按照插入次序依次遍历得到数据,这个功能是怎么实现的呢?其实和上面的结构图很类似,插入到 HashMap 中的数据用双向链表连接起来,然后按照遍历链表的方法依次得到数据,这样就能够实现有序输出数据了。好了,散列表就基本上讲完了。

March 26, 2019 · 1 min · jiezi

数据结构与算法——跳表

概述前面说到了二分查找,并且它是基于顺序表结构的,即数组,如果直接用于链表,时间复杂度会比较的高,是 O(logn),一般我们不会这样做。那么有没有基于链表的二分查找呢?答案就是今天说到的跳跃链表。2. 跳表长什么样子?对于一般的链表,我们进行查找的话,需要遍历整个链表,就像下面这样:如果我们要找节点 9 ,需要遍历 9 个节点。如果我们在原始链表之上建立一级链表,会是什么样子呢?如下图:新建立的一级链表,我们叫做索引层或是索引,那么建立了一级索引之后,再来查找节点 9,会遍历多少节点呢?我们从第一级索引中开始查找,到了节点 9 的时候,通过向下的指针,可以找到节点 9,总共遍历了 6 个节点,相比没有索引,查找的效率提高了。这样效果其实还不是非常的明显,如果我们再加上几级索引,查找会不会更快呢?如上图,总共建立了三级索引,现在查找节点 9 ,只需要遍历 5 个节点了,查找的效率再一步提升了。其实,上面我举的例子,总共只有 10 个节点,所以看不出来性能有多大的提升,但是在实际的开发中,存储的数据成千上万,那么跳表的效率就更能体现了。3. 跳表分析通过上面的理解,你应该知道了跳表,其实就是通过建立多级索引来提升查找效率的一种数据结构。跳表的查询时间复杂度是 O(logn),跟二分查找一样,空间复杂度是 O(n),因为每一级建立的索引的节点个数都是上一级的一半,总共加起来就还需要原始链表的空间大小。综合分析,其实你已经不能看到,跳表其实利用的是空间换时间的思想。其实跳表不只是能够在 O(logn) 内查询数据,并且也可以高效的插入和删除数据,时间复杂度还是 O(logn)。删除操作其实很简单,找到之后直接删除节点即可。插入操作也类似,因为整个链表是有序的,所以查找之后,直接插入。只不过这里涉及到一个动态更新的问题。一般的动态数据结构都会维持平衡,保证插入查询操作的性能不会下降。例如跳表,假如没有动态更新,则可能会出现插入之后,链表节点特别集中的问题:在极端的情况下,跳表还是会退化为链表。所以跳表借助了随机函数这种方式来维持整个索引的平衡性,每插入一个节点,都会生成一个随机函数 k,然后不仅是在原始链表中插入这个节点,还会在 1-k 层索引中插入这个节点。4. 跳表实现跳表的具体实现其实还是比较复杂的,代码理解起来比较的困难,这里我就不贴出来了,大家有兴趣的可以到我的 Github 上面参考借鉴。点击进入我的 Github只不过我们也不用死扣跳表的代码实现,在实际的开发环境中,我们能够利用已有的实现就很不错了,这就是避免重复造轮子。例如在 Java 中已经有跳表的两个实现类,分别是 ConcurrentSkipListSet 和 ConcurrentSkipListMap,并且是线程安全的。

March 25, 2019 · 1 min · jiezi

数据结构与算法——选择排序和插入排序

回顾前面说到了冒泡排序,这是一种时间复杂度为 O(n2) 、是原地排序和稳定的的排序算法,具体思路是:根据相邻两个元素之间比较大小,然后交换位置,得出最后排序的结果。具体可参考我写的这一篇文章:数据结构与算法——冒泡排序,今天来看看另外两种基础的排序算法:选择排序和插入排序。2. 选择排序先来看看选择排序,选择排序的思路其实很简单,将排序的数据分为已排序区间和未排序区间,一般是以第一个元素为已排序区间,然后依次遍历未排序区间,找到其最小值,和未排序区间的最后一个值进行比较,交换位置。未排序区间遍历完毕,则排序结束。光说可能有点抽象,我画了一张图来帮助你理解: 是不是很简单呢?下面是它的代码实现:public class SelectionSort { public static void selectionSort(int[] data){ int length = data.length; //如果只有一个元素,或者数组为空,则直接退出 if (length <= 1) return; for (int i = 0; i < length - 1; i++) { int min = i + 1; //找到最小值 for (int j = i + 1; j < length; j++) { if (data[j] < data[min]) min = j; } //交换位置 if (data[min] < data[i]){ int temp = data[min]; data[min] = data[i]; data[i] = temp; } } }}结合代码分析,选择排序在最好、最坏和平均情况下的时间复杂度都是 O(n2),并且没有借助额外的存储空间,是一种原地排序算法。那么选择排序是稳定的吗?答案是否定的,举个例子:排序的数组为 data[2, 2, 1, 3, 5, 4, 8],第一次遍历未排序的数组,找到最小值为 1,和第一个 2 交换位置,那么这两个 2 的前后顺序就被打乱了,所以稳定性被破坏。正因如此,选择排序比起冒泡排序和插入排序就显得逊色很多了。 3. 插入排序我们再来看看插入排序,其实思路和上面的选择排序非常的类似,也是将排序数据分为已排序区间和未排序区间,依次遍历未排序区间,和已排序区间的值进行比较,将其插入到合适的位置上,直至将未排序的区间数据遍历完。你可以结合下面的图来理解:可以看到,和选择排序一样,将第一个数据作为已排序区间,第一次遍历到 2 ,将其插入到 4 后面,然后再依次遍历。下面是代码实现:public class InsertionSort { public static void insertionSort(int[] data){ int length = data.length; if (length <= 1) return; for (int i = 1; i < length; i++) { int value = data[i]; int j = i - 1; for (; j >= 0; j –){ if (data[j] > value) data[j + 1] = data[j]; else break; } data[j + 1] = value; } }}很显然,插入排序也是稳定的,因为我们是在 data[j] > da[j + 1] 的时候,才进行数据交换,不会影响到相同元素的前后位置。并且,插入排序是原地排序,最好情况下,数组本来就是有序的,所以我们只需要遍历一次数组就可以了,时间复杂度是 O(n),最坏情况和平均情况下,时间复杂度都是 O(n2)。最后还有个问题,为什么在实际中,插入排序的比冒泡排序使用的更加广泛呢?虽然这两个排序算法的平均时间复杂度都是 O(n2),但是结合代码,不难发现,它们涉及到的数据交换操作时略有差别的。冒泡排序在交换数据的时候,需要进行三次赋值操作,而插入排序只需要一次。//插入排序的赋值操作 for (; j >= 0; j –){ if (data[j] > value) data[j + 1] = data[j]; else break;}//冒泡排序的赋值操作 for (int j = 0; j < n - i - 1; j++) { //如果data[j] > data[j + 1],交换两个数据的位置 if (data[j] > data[j + 1]){ int temp = data[j]; data[j] = data[j + 1]; data[j + 1] = temp; }在数据规模小的时候,这样的差别没什么影响,但是如果我们要排序的是一组较大的数据,那么两种排序算法的执行时间的差别就会很明显了。

March 19, 2019 · 2 min · jiezi

数据结构与算法——冒泡排序

导言因为这是排序算法系列的第一篇文章,所以多啰嗦几句。排序是很常见的算法之一,现在很多编程语言都集成了一些排序算法,比如Java 的Arrays.sort()方法,这种方式让我们可以不在乎内部实现细节而直接调用,在实际的软件开发当中也会经常使用到。但是站在开发者的角度而言,知其然必须知其所以然。多练练排序算法,不仅能够让我们知道一些排序方法的底层实现细节,更能够锻炼我们的思维,提升编程能力。现在很多技术面试也会涉及到基本的排序算法,所以多练习是有好处的。文中涉及到的代码都是Java实现的,但是不会涉及到太多的Java语言特性,并且我会加上详细的注释,帮助你理解代码并且转换成你熟悉的编程语言。常见的排序算法有以下10种:冒泡排序、选择排序、插入排序,平均时间复杂度都是O(n2)希尔排序、归并排序、快速排序、堆排序,平均时间复杂度都是O(nlogn)计数排序、基数排序、桶排序,平均时间复杂度都是O(n + k)在开始具体的排序算法讲解之前,先得明白两个概念:原地排序:指的是在排序的过程当中不会占用额外的存储空间,空间复杂度为O(1)。排序算法的稳定性:一个稳定的排序,指的是在排序之后,相同元素的前后顺序不会被改变,反之就称为不稳定。举个例子:一个数组 [3,5,1,4,9,6,6,12] 有两个6(为了区分,我把其中一个 6 加粗),如果排序之后是这样的:[1,3,4,5,6,6,9,12](加粗的 6 仍然在前面),就说明这是一个稳定的排序算法。2. 言归正传冒泡排序的思路其实很简单,一个数据跟它相邻的数据进行大小的比较,如果满足大小关系,就将这两个数据交换位置。一直重复这个操作,就能将数据排序。 举个例子,假如有数组 a[3,5,1,4,9,6],第一次冒泡的操作如下图所示:重复进行这个操作,6次冒泡之后,数据排序完成。根据这个思路,应该能很容易能够写出下面的代码实现冒泡排序:public class BubbleSort { //data表示整型数组,n表示数组大小 public static void bubbleSort(int[] data, int n){ //数组大小小于等于1,无须排序 if (n <= 1) return; for (int i = 0; i < n; i++) { for (int j = 0; j < n - i - 1; j++) { //如果data[j] > data[j + 1],交换两个数据的位置 if (data[j] > data[j + 1]){ int temp = data[j]; data[j] = data[j + 1]; data[j + 1] = temp; } } } }}但是这个排序算法还可以进行优化,当冒泡操作已经没有数据交换的时候,说明排序已经完成,就不用在进行冒泡操作了。例如上面的例子,第一次冒泡之后,数据为 [3,1,4,5,6,9],再进行一次冒泡,数据变为 [1,3,4,5,6,9],此时已经完成了排序,就可以结束循环了。所以针对这个数组的排序,上面的代码需要6次冒泡才能完成,其中有4次都是不需要的。所以可以对代码进行优化:public class BubbleSort { //优化后的冒泡排序 //data表示整型数组,n表示数组大小 public static void bubbleSort(int[] data, int n){ //数组大小小于等于1,无须排序,返回空 if (n <= 1) return; for (int i = 0; i < n; i++) { boolean flag = false;//判断是否有数据交换 for (int j = 0; j < n - i - 1; j++) { //如果data[j] > data[j + 1],交换两个数据的位置 if (data[j] > data[j + 1]){ int temp = data[j]; data[j] = data[j + 1]; data[j + 1] = temp; flag = true;//表示有数据交换 } } //如果没有数据交换,则直接退出循环 if (!flag) break; } }}好了,冒泡排序的基本思路和代码都已经实现,最后总结一下:冒泡排序是基于数据比较的最好情况时间复杂度是O(n),最坏情况时间复杂度是O(n2),平均时间复杂度是O(n2)冒泡排序是原地排序算法,并且是稳定的。

March 18, 2019 · 1 min · jiezi

数据结构与算法——队列

概述前面说完了栈,接下来再看看另一种很基础的数据结构,队列。顾名思义,队列跟我们现实生活中的排队很相似:例如我们在食堂排队打饭,先来的先打到,后来的只能一次排在后面,不允许插队。很明显,队列的操作也是受限的,插入元素(入队)只能在队尾,删除元素(出队)只能在队头。结合下面的图就很容易理解了:2. 队列实现和栈一样,队列也有两种实现方式,使用数组实现的队列叫做顺序队列,使用链表实现的队列叫做链式队列。这里我实现了链式队列,你也可以根据其思想使用数组来实现。public class LinkedListQueue { private Node head;//队头节点指针 private Node tail;//队尾节点指针 private int size;//队列中元素个数 public LinkedListQueue() { this.head = null; this.tail = null; this.size = 0; } //入队 public boolean enqueue(String value) { Node node = new Node(value); if(tail == null) { head = tail = node; this.size ++; return true; } tail.next = node; tail = node; this.size ++; return true; } //出队列 public String dequeue() { if(head == null) return null;//队列为空 String result = head.getData(); head = head.next; this.size –; return result; } //定义链表节点 public static class Node{ private String data; private Node next; public Node(String data) { this.data = data; this.next = null; } public String getData() { return data; } }}3. 循环队列在使用数组实现队列的时候,会出现一个问题,就是随着队头和队尾指针的后移,就算数组中还有空间,也不能进行入队操作了。这时候我们需要进行数据搬移,性能会受到影响,而循环队列解决了这个问题。循环队列就是将数组首尾相连,形成了一个环形:如上图,当指针 tail 到达数组末尾的时候,并不进行数据搬移,而是直接将指针向前移,到达了 0 这个位置。在进行一次入队,就变成了下面的样子:可以看到,循环队列判断空的条件是 head == tail,而怎么判断队列满呢?答案是 (tail + 1)== head 的时候,队列就满了,不能看出循环队列实际上浪费了一个存储空间。下面我给出了循环队列的代码实现:public class CircularQueue { private String[] items; private int size; private int head; private int tail; public CircularQueue(int capacity) { this.items = new String[capacity]; this.size = 0; this.head = 0; this.tail = 0; } public CircularQueue() { this(16); } //入队列 public boolean enqueue(String value) { if((tail + 1) % items.length == head) return false;//队列已满 items[tail] = value; //head 和 tail 指针的移动不是简单的加减1了 tail = (tail + 1) % items.length; this.size ++; return true; } //出队列 public String dequeue() { if(head == tail) return null;//队列为空 String result = items[head]; head = (head + 1) % items.length; this.size –; return result; } //队列中元素个数 public int size() { return size; } //打印队列中数据 public void print() { int h = head; int t = tail; while(h != t) { System.out.print(items[h] + " “); h = (h + 1) % items.length; } System.out.println(); }}

March 17, 2019 · 2 min · jiezi

数据结构与算法——栈

概述今天来看看栈这种线性数据结构,非常的基础,我举个例子你就能明白了。比如你桌子上堆放的一摞文件,最先放的在最下面,拿的时候也是最后拿,最后放的在最上面,拿的时候也先拿到。这种满足了 先进后出,后进先出 特点的数据结构,就叫做栈。相信结合上图你能够看到,栈这种数据结构,插入和删除的操作都被局限在了栈的一端,插入数据叫做入栈,删除数据叫做出栈。2. 栈的实现栈有两种实现方式,一是使用数组,这种实现叫做顺序栈,二是使用链表,叫做链式栈。下面是其实现的代码:顺序栈的代码实现public class ArrayStack { private String[] items;//储存数据的数组 private int size;//栈中数据个数 public ArrayStack(int capacity) { this.items = new String[capacity]; this.size = 0; } public ArrayStack() { this(10); } //入栈 public boolean push(String value) { //如果栈已满,则扩容数组 if(size == items.length) resize(items.length * 2); items[size ++] = value; return true; } //出栈 public String pop() { if(size == 0) return null; return items[– size]; } //获取栈顶元素 public String peek() { if(size == 0) return null; return items[size - 1]; } //重新设置数组大小,用于扩容 private void resize(int newSize) { String[] temp = new String[newSize]; for (int i = 0; i < items.length; i++) { temp[i] = items[i]; } items = temp; }}链式栈的代码实现public class LinkedListStack { private Node head = null;//栈顶元素 private int size = 0;//栈中元素个数 //入栈 public boolean push(String value) { Node node = new Node(value); if(head == null) { head = node; this.size ++; return true; } node.next = head; head = node; this.size ++; return true; } //出栈 public String pop() { if(head == null) return null; String result = head.data; head = head.next; this.size –; return result; } //获取栈顶元素 public String peek() { if(head == null) return null; return head.getData(); } //判断栈是否为空 public boolean isEmpty() { return this.head == null; } //栈中元素的个数 public int size() { return this.size; } //清空栈 public void clear() { this.head = null; this.size = 0; } //打印栈中所有的元素 public void print() { Node pNode = head; while(pNode != null) { System.out.print(pNode.getData() + " “); pNode = pNode.next; } System.out.println(); } //定义栈的节点 public static class Node{ private String data; private Node next; public Node(String data) { this.data = data; this.next = null; } public String getData() { return data; } }}3. 栈练习下面思考一个练习题:如何使用栈来实现浏览器的前进和后退功能?我们在使用浏览器的时候,会新打开一个网页,如果连续打开了多个网页,我们可以后退,也可以前进,如果这时候又新打开了一个网页,那就不能在新页面中前进了。使用栈,我们可以轻松实现这个功能。浏览器的前进后退功能代码实现public class BrowserForwardAndBack { private LinkedListStack forward; private LinkedListStack back; public BrowserForwardAndBack() { this.forward = new LinkedListStack(); this.back = new LinkedListStack(); } //新打开一个页面 public void open(String url) { //将其保存到前进的栈中 this.forward.push(url); //如果后退的栈不为空,则清空后退的栈 if(!back.isEmpty()) back.clear(); System.out.println(“新打开一个页面,url = " + url); } //前进,从back中取出内容到forward中 public void goForward() { if(this.back.isEmpty()) { System.out.println(“前面没有页面!”); return; } this.forward.push(this.back.pop()); System.out.println(“前进一个页面,当前页面为:” + this.forward.peek()); } //后退,从forward中取出内容到back中 public void goBack() { if(this.forward.size() <= 1) { System.out.println(“后面没有页面!”); return; } this.back.push(this.forward.pop()); System.out.println(“后退一个页面,当前页面为:” + this.forward.peek()); }}

March 16, 2019 · 2 min · jiezi

数据结构与算法——单链表练习

概述前面的文章说到了一种很基础的数据结构——链表:数据结构与算法——链表,今天就来看看关于单链表的几种常见的操作,技术笔试的时候很大概率能够遇到其中的一些。多练习一下,对我们理解链表有很大的帮助,也能够提升我们的编码能力。废话不多说,这几个练习题是:单链表反转合并两个有序链表检测链表中的环删除链表倒数第 k 个节点找到链表的中间节点2. 单链表反转单链表反转,顾名思义,就是将链表的指针指向全部倒过来,尾结点变成头节点,头节点变成尾结点。具体的代码如下:public class SingleLinkedList { private Node head = null;//链表的头节点 public Node reverse(Node node) { Node head = null; Node previous = null; Node current = node; while(current != null) { Node next = current.next; if (next == null) { head = current; } current.next = previous; previous = current; current = next; } this.head = head; return this.head; }}3. 合并两个有序链表假如链表中存储的数据是数值类型,并且是有序的,这时候可以合并两个有序的链表,主要涉及到两个链表元素的比较。代码如下: //合并两个有序的链表 public Node mergeSortedList(Node la, Node lb) { if(la == null && lb == null) return null; if(la == null) return lb; if(lb == null) return la; Node p = la; Node q = lb; Node head = null; //比较第一个元素 if(p.getData() < q.getData()) { head = p; p = p.next; }else { head = q; q = q.next; } //比较后面的元素 Node r = head; while(p != null && q != null) { if(p.getData() < q.getData()) { r.next = p; p = p.next; }else { r.next = q; q = q.next; } r = r.next; } //比较链表可能剩余的元素 while(p != null) { r.next = p; p = p.next; r = r.next; } while(q != null) { r.next = q; q = q.next; r = r.next; } return head; }4. 检测链表中的环单链表中有可能存在像循环链表这样的环形结构,检测链表中是否有这样的结构,可以利用这样的思路:定义两个指针,一个指针每次移动两个节点,另一个指针移动一个节点,如果两个指针相遇,则说明存在环,代码如下: //检测链表中的环 public boolean checkCircle(Node node) { if(node == null) return false; Node fast = node.next; Node slow = node; while(fast != null && fast.next != null) { fast = fast.next.next; slow = slow.next; if(fast == slow) return true; } return false; }5. 删除链表倒数第 k 个节点这个题在笔试当中非常常见,解决的思路比较的巧妙,不容易想到,但是只要一想到,代码写起来就很简单了。主要的思路是这样的:定义一个指针 fast,从链表头开始,移动 k-1 个节点,再定义一个指针 slow,同时移动 fast 和 slow ,当 fast 到达链表尾节点的时候,slow 所指向的节点就是要删除的节点。具体的代码实现如下:public static Node deleteLastKth(Node head, int k) { Node fast = head; int i = 1; //先让前面的指针移动k - 1步 while(fast != null && i < k) { fast = fast.next; ++ i; } Node slow = head; Node prev = null; //前后指针同时移动 while(fast.next != null) { fast = fast.next; prev = slow; slow = slow.next; } if(prev == null) {//说明删除的是第一个节点 head = head.next; }else { prev.next = prev.next.next; } return head; }6. 找到链表的中间节点寻找链表中间的那个节点,思路很简单,也是定义两个指针,一个指针每次移动两个节点,另一个指针移动一个节点,前面的指针到达链表尾部的时候,后面的指针指向的节点就是中间的那个节点:public static Node findMiddleNode(Node head) { if(head == null) return null; Node fast = head; Node slow = head; while(fast.next != null && fast.next.next != null) { fast = fast.next.next; slow = slow.next; } return slow; }

March 15, 2019 · 2 min · jiezi

数据结构与算法——链表

概述前面说到了数组,利用连续的内存空间来存储相同类型的数据,其最大的特点是支持下标随机访问,但是删除和插入的效率很低。今天来看另一种很基础的数据结构——链表。链表不需要使用连续的内存空间,它使用指针将不连续的内存块连接起来,形成一种链式结构。2. 单链表首先来看看单链表,存储数据的内存块叫做节点,每个节点保存了一个 next 指针,指向下一个节点的内存地址,结合下图你就很容易看明白了:其中有两个节点指针比较的特殊,首先是链表头节点的指针,它指向了链表的第一个节点的地址,利用它我们可以遍历得到整个链表。其次是尾结点的指针,它指向了 null ,表示链表结束。不难看出,单链表的最大特点便是使用指针来连接不连续的节点,这样我们不用担心扩容的问题了,并且,链表的插入和删除操作也非常的高效,只需要改变指针的指向即可。结合上图不难理解,单链表能够在 O(1) 复杂度内删除和添加元素,这就比数组高效很多。但是,如果我们要查找链表数据怎么办呢?链表的内存不是连续的,不能像数组那样根据下标访问,所以只能通过遍历链表来查找,时间复杂度为 O(n)。下面是单链表的代码示例:public class SingleLinkedList { private Node head = null;//链表的头节点 //根据值查找节点 public Node findByValue(int value) { Node p = head; while (p != null && p.getData() != value) p = p.next; return p; } //根据下标查找节点 public Node findByIndex(int index) { Node p = head; int flag = 0; while (p != null){ if (flag == index) return p; flag ++; p = p.next; } return null; } //插入节点到链表头部 public void insertToHead(Node node){ if (head == null) head = node; else { node.next = head; head = node; } } public void insertToHead(int value){ this.insertToHead(new Node(value)); } //插入节点到链表末尾 public void insert(Node node){ if (head == null){ head = node; return; } Node p = head; while (p.next != null) p = p.next; p.next = node; } public void insert(int value){ this.insert(new Node(value)); } //在某个节点之后插入节点 public void insertAfter(Node p, Node newNode){ if (p == null) return; newNode.next = p.next; p.next = newNode; } public void insertAfter(Node p, int value){ this.insertAfter(p, new Node(value)); } //在某个节点之前插入节点 public void insertBefore(Node p, Node newNode){ if (p == null) return; if (p == head){ insertToHead(newNode); return; } //寻找节点p前面的节点 Node pBefore = head; while (pBefore != null && pBefore.next != p){ pBefore = pBefore.next; } if (pBefore == null) return; newNode.next = pBefore.next; pBefore.next = newNode; } public void insertBefore(Node p, int value){ insertBefore(p, new Node(value)); } //删除节点 public void deleteByNode(Node p){ if (p == null || head == null) return; if (p == head){ head = head.next; return; } Node pBefore = head; while (pBefore != null && pBefore.next != p){ pBefore = pBefore.next; } if (pBefore == null) return; pBefore.next = pBefore.next.next; } //根据值删除节点 public void deleteByValue(int value){ Node node = this.findByValue(value); if (node == null) return; this.deleteByNode(node); } //打印链表的所有节点值 public void print(){ Node p = head; while (p != null){ System.out.print(p.getData() + " “); p = p.next; } System.out.println(); } //定义链表节点 public static class Node{ private int data; private Node next; public Node(int data) { this.data = data; this.next = null; } public int getData() { return data; } }}3. 循环链表循环链表和单链表的唯一区别便是链表的尾结点指针并不是指向 null,而是指向了头节点,这样便形成了一个环形的链表结构:4. 双向链表双向链表,顾名思义,就是链表不只是存储了指向下一个节点的 next 指针,还存储了一个指向前一个节点的 prev 指针,如下图:为什么要使用这种具有两个指针的链表呢?主要是为了解决链表删除和插入操作的效率问题。在单链表中,要删除一个节点,必须找到其前面的节点,这样就要遍历链表,时间开销较高。但是在双向链表中,由于每个节点都保存了指向前一个节点的指针,这样我们能够在 O(1) 的时间复杂度内删除节点。插入操作也类似,比如要在节点 p 之前插入一个节点,那么也必须遍历单链表找到 p 节点前面的那个节点。但是双向链表可以直接利用前驱指针 prev 找到前一个节点,非常的高效。这也是双向链表在实际开发中用的更多的原因,虽然每个节点存储了两个指针,空间开销更大,这就是一种典型的用空间换时间的思想。下面是双向链表的代码示例:public class DoubleLinkedList { private Node head = null;//链表的头节点 //在某个节点之前插入节点,这里方能体现出双向链表的优势 public void insertBefore(Node p, Node newNode) { if (p == null) return; if(p == head) { this.insertToHead(newNode); return; } newNode.prev = p.prev; p.prev.next = newNode; newNode.next = p; p.prev = newNode; } public void insertBefore(Node p, int value) { this.insertBefore(p, new Node(value)); } //删除某个节点 public void deleteByNode(Node node) { if(node == null || head == null) return; if (node == head) { head = head.next; if(head != null) head.prev = null; return; } Node prev = node.prev; Node next = node.next; prev.next = next; if(next != null) next.prev = prev; } //根据值删除节点 public void deleteByValue(int value) { Node node = this.findByValue(value); if (node == null) return; this.deleteByNode(node); } //定义链表节点 public static class Node{ private int data; private Node prev;//链表的前驱指针 private Node next;//链表的后继指针 public Node(int data) { this.data = data; this.prev = null; this.next = null; } public int getData() { return data; } }}

March 14, 2019 · 3 min · jiezi

考研笔记之数据结构邓俊辉:第一章

知识点回顾1. 什么是数据结构?数据结构可以用来干嘛?为什么要学数据结构?balabala2. 如何评价和选择一个数据结构?balabala部分习题解析思路balabala

March 12, 2019 · 1 min · jiezi

JS数据结构与算法_树

上一篇:JS数据结构与算法_集合&字典一、递归学习树离不开递归。1.1 介绍递归是一种解决问题的方法,它解决问题的各个小部分,直到解决最初的大问题。递归通常涉及函数调用自身。通俗的解释:年级主任需要知道某个年级的数学成绩的平均值,他没法直接得到结果;年级主任需要问每个班的数学老师,数学老师需要问班上每个同学;然后再沿着学生–>老师–>主任这条线反馈,才能得到结果。递归也是如此,自己无法直接解决问题,将问题给下一级,下一级若无法解决,再给下一级,直到有结果再依次向上反馈。我们常见的使用递归解决的问题,如下:// 斐波拉契数列function fibo(n) { if (n === 0 || n === 1) return n; // 边界 return fibo(n - 1) + fibo(n - 2);}// 阶乘function factorial(n) { if (n === 0 || n === 1) return 1; // 边界 return facci(n - 1) * n;}他们有共同的特点,也是递归的特点:有边界条件,防止无限递归函数自身调用1.2 高效递归的两个方法以斐波拉契数列举例,下面是n=6时斐波拉契数列的计算过程。我们可以发现,这里面存在许多重复的计算,数列越大重复计算越多。如何避免呢?利用缓存,将fib(n)计算后的值存储,后面使用时,若存在直接取用,不存在则计算(1)缓存Memoizerconst fibo_memo = function() { const temp = {0: 0, 1: 1}; // 需要用闭包缓存 return function fib(n) { if (!(n in temp)) { // 缓存中无对应数据时,向下计算查找 temp[n] = fib(n - 1) + fib(n - 2); } return temp[n]; }}()(2)递推法(动态规划)动态规划并不属于高效递归,但是也是有效解决问题的一个方法。动态规划:从底部开始解决问题,将所有小问题解决掉,然后合并成一个整体解决方案,从而解决掉整个大问题;递归:从顶部开始将问题分解,通过解决掉所有分解的小问题来解决整个问题;使用动态规划解决斐波那契数列function fibo_dp(n) { let current = 0; let next = 1; for(let i = 0; i < n; i++) { [current, next] = [next, current + next]; } return current;}(3)效率对比const arr = Array.from({length: 40}, (_, i) => i);// 普通console.time(‘fibo’);arr.forEach((e) => { fibo(e); });console.timeEnd(‘fibo’);// 缓存console.time(‘fibo_memo’);arr.forEach((e) => { fibo_memo(e); });console.timeEnd(‘fibo_memo’);// 动态规划console.time(‘fibo_dp’);arr.forEach((e) => { fibo_dp(e); });console.timeEnd(‘fibo_dp’);// 打印结果【40】fibo: 1869.665msfibo_memo: 0.088msfibo_dp: 0.326ms// 当打印到【1000】时,普通的已溢出fibo_memo: 0.370msfibo_dp: 16.458ms总结:从上面的对比结果可知,使用缓存的性能最佳二、树一个树结构包含一系列存在父子关系的节点。每个节点都有一个父节点(除了顶部的第一个节点)以及零个或多个子节点:2.1 相关术语节点:树中的每个元素都叫作节点;根节点:位于树顶部的节点叫作根节点;内部节点/分支节点:至少有一个子节点的节点称为内部节点或;外部节点/叶节点:没有子元素的节点称为外部节点或叶节点;子女节点:7和15为11的子女节点父节点:11为7和15的父节点兄弟节点:同一个父节点的子女节点互称为兄弟;7和15互为兄弟节点祖先节点:从根节点到该节点所经过分支上的所有节点;如节点3的祖先节点为 11,7,8子孙节点:以某一节点构成的子树,其下所有节点均为其子孙节点;如12和14为13的子孙节点节点所在层次:根节点为1层,依次向下节点的度:树中距离根节点最远的节点所处的层次就是树的深度;上图中,树的深度是4节点的度:结点拥有子结点的数量;树的度:树中节点的度的最大值;有序树无序树关于数的深度和高度的问题,不同的教材有不同的说法,具体可以参考树的高度和深度以及结点的高度和深度这篇文章2.2 认识二叉搜索树BST2.2.1 定义二叉树是树的一种特殊情况,每个节点最多有有两个子女,分别称为该节点的左子女和右子女,就是说,在二叉树中,不存在度大于2的节点。二叉搜索树(BST)是二叉树的一种,但是它只允许你在左侧节点存储(比父节点)小的值, 在右侧节点存储(比父节点)大(或者等于)的值。上图展示的便是二叉搜索数2.2.2 特点同一层,数值从左到右依次增加以某一祖先节点为参考,该节点左侧值均小于节点值,右侧值均大于节点值在二叉树的第i(i>=1)层,最多有x^(i-1)个节点深度为k(k>=0)的二叉树,最少有k个节点,最多有2^(k-1)个节点对于一棵非空二叉树,叶节点的数量等于度为2的节点数量加1满二叉树:深度为k的满二叉树,是有2^(k-1)个节点的二叉树,每一层都达到了可以容纳的最大数量的节点2.2.3 基础方法insert(key): 向树中插入一个新的键;inOrderTraverse: 通过中序遍历方式遍历所有节点preOrderTraverse: 通过先序遍历方式遍历所有节点postOrderTraverse: 通过后序遍历方式遍历所有节点getMin: 返回树中最小的值/键getMax: 返回树中最大的值/键find(key): 在树中查找一个键,如果节点存在则返回该节点不存在则返回null;remove(key): 从树中移除某个键2.3 BST的实现2.3.1 基类// 基类class BinaryTreeNode { constructor(data) { this.key = data; this.left = null; this.right = null; }}下图展现了二叉搜索树数据结构的组织方式:2.3.2 BST类//二叉查找树(BST)的类class BinarySearchTree { constructor() { this.root = null; // 根节点 } insert(){} // 插入节点 preOrderTraverse(){} // 先序遍历 inOrderTraverse(){} // 中序遍历 postOrderTraverse(){} // 后序遍历 search(){} // 查找节点 getMin(){} // 查找最小值 getMax(){} // 查找最大值 remove(){} // 删除节点}2.3.3 insert方法insert某个值到树中,必须依照二叉搜索树的规则【每个节点Key值唯一,最多有两个节点,且左侧节点值<父节点值<右侧节点值】不同情况具体操作如下:根节点为null,直接赋值插入节点给根节点;根节点不为null,按照BST规则找到left/right为null的位置并赋值insert(key) { const newNode = new BinaryTreeNode(key); if (this.root !== null) { this.insertNode(this.root, newNode); } else { this.root = newNode; }}insertNode(node, newNode) { if (newNode.key < node.key) { if (node.left === null) {// 左侧 node.left = newNode; } else { this.insertNode(node.left, newNode); } } else { if (node.right === null) {// 右侧 node.right = newNode; } else { this.insertNode(node.right, newNode); } }}下图为在已有BST的基础上插入值为6的节点,步骤如下:有无根节点?有;对比根节点值(6<11),根节点左侧判断;第二层左侧节点是否为null?不为;对比第二层左侧节点的值(6<7),继续左侧判断;第三层左侧节点是否为null?不为;对比第三层左侧节点的值(6>5),以右侧判断;第四层右侧节点是否为null?为;插入该处2.3.4 树的遍历树的遍历,核心为递归:根节点需要找到其每一个子孙节点,但是并不知道这棵树有多少层。因此,它找到其子节点,子节点也不知道,依次向下找,直到叶节点。访问树的所有节点有三种方式:中序、先序和后序。下面依次介绍(1)中序遍历中序遍历是一种以上行顺序访问BST所有节点的遍历方式,也就是以从最小到最大的顺序访问所有节点。中序遍历的一种应用就是<u>对树进行排序操作</u>inOrderTraverse(callback) { this.inOrderTraverseNode(this.root, callback);}inOrderTraverseNode(node, callback) { if (node !== null) { this.inOrderTraverseNode(node.left, callback); callback(node.key); this.inOrderTraverseNode(node.right, callback); }}下面的图描绘了中序遍历方法的访问路径:(2)先序遍历先序遍历是以优先于后代节点的顺序访问每个节点的。先序遍历的一种应用是<u>打印一个结构化的文档</u>preOrderTraverse(callback) { this.preOrderTraverseNode(this.root, callback);}preOrderTraverseNode(node, callback) { if (node !== null) { callback(node.key); this.preOrderTraverseNode(node.left, callback); this.preOrderTraverseNode(node.right, callback); }}下面的图描绘了先序遍历方法的访问路径:(3)后序遍历后序遍历则是先访问节点的后代节点,再访问节点本身。后序遍历的一种应用是<u>计算一个目录和它的子目录中所有文件所占空间的大小</u>postOrderTraverse(callback) { this.postOrderTraverseNode(this.root, callback);}postOrderTraverseNode(node, callback) { if (node !== null) { this.postOrderTraverseNode(node.left, callback); this.postOrderTraverseNode(node.right, callback); callback(node.key); }}下面的图描绘了后序遍历方法的访问路径:2.3.5 查找方法(1)最值观察下图,我们可以非常直观的发现左下角为最小值,右下角为最大值具体代码实现如下getMin() { const ret = this.getMinNode(); return ret && ret.key;}getMinNode(node = this.root) { if (node) { while (node && node.left !== null) { node = node.left; } } return node;}getMax() { const ret = this.getMaxNode(); return ret && ret.key;}getMaxNode(node = this.root) { if (node) { while (node && node.right !== null) { node = node.right; } } return node;}(2)find()方法递归找到与目标key值相同的节点,并返回;具体实现如下:find(key) { return this.findNode(this.root, key);}findNode(node, key) { if (node === null) { return null; } if (key < node.key) { return this.findNode(node.left, key); } if (key > node.key) { return this.findNode(node.right, key); } return node;}2.3.6 remove()方法移除节点是这一类方法中最为复杂的操作,首先需要找到目标key值对应的节点,然后根据不同的目标节点类型需要有不同的操作remove(key) { return this.removeNode(this.root, key);}removeNode(node, key) { if (node === null) { return null; } if (key < node.key) { // 目标key小于当前节点key,继续向左找 node.left = this.removeNode(node.left, key); return node; } if (key > node.key) { // 目标key小于当前节点key,继续向右找 node.right = this.removeNode(node.right, key); return node; } // 找到目标位置 if (node.left === null && node.right === null) { // 目标节点为叶节点 node = null; return node; } if (node.right === null) { // 目标节点仅有左侧节点 node = node.left; return node; } if (node.left === null) { // 目标节点仅有右侧节点 node = node.right; return node; } // 目标节点有两个子节点 const tempNode = this.getMinNode(node.right); // 右侧最小值 node.key = tempNode.key; node.right = this.removeNode(node.right, node.key); return node;}目标节点为叶节点图例:子节点赋值为null,并将目标节点指向null目标节点为仅有左侧子节点或右侧子节点图例:将目标节点的父节点指向子节点目标节点有两个子节点:根据BST的构成规则,以目标节点右侧树最小值替换重新连接上一篇:JS数据结构与算法_集合&字典 ...

March 12, 2019 · 3 min · jiezi

加入B_树与hash | 自己动手写一个Redis

最近学习了Redis,对其内部结构较为感兴趣,为了进一步了解其运行原理,我打算自己动手用C++写一个redis。这是我第一次造轮子,所以纪念一下 ^ _ ^。源码github链接,项目现在实现了客户端与服务器的链接与交互,以及一些Redis的基本命令,下面是测试结果:(左边是服务端,右边是客户端)上节已经实现了小型Redis的基本功能,为了完善其功能并且锻炼一下自己的数据结构与算法,我打算参考《Redis设计与实现》一书优化其中的数据结构与算法从而完善自己的项目。本章讲解的是项目中B树与hash的引入。B树的引入在上一章中,我们的数据库使用的是原生的map结构,为了提高数据库的增删改查效率,这里我将其改为使用B_树这一数据结构。B树的具体实现方法如下:其中主要函数为(1)void insert(int k,string stt) 向B_树中插入一个关键字以及该关键字对应的value的值。(2)string getone(int k) 通过关键字获取其对应的value的值。// A BTree nodeclass BTreeNode{ int keys; // An array of keys string strs;//value的类型使用string数组 int t; // Minimum degree (defines the range for number of keys) BTreeNode **C; // An array of child pointers int n; // Current number of keys bool leaf; // Is true when node is leaf. Otherwise false public: BTreeNode(int _t, bool _leaf); // Constructor string getOne(int k); // A function to traverse all nodes in a subtree rooted with this node void traverse(); // A function to search a key in subtree rooted with this node. BTreeNode search(int k); // returns NULL if k is not present. // A function that returns the index of the first key that is greater // or equal to k int findKey(int k); // A utility function to insert a new key in the subtree rooted with // this node. The assumption is, the node must be non-full when this // function is called void insertNonFull(int k,string stt); // A utility function to split the child y of this node. i is index // of y in child array C[]. The Child y must be full when this // function is called void splitChild(int i, BTreeNode y); // A wrapper function to remove the key k in subtree rooted with // this node. void remove(int k); // A function to remove the key present in idx-th position in // this node which is a leaf void removeFromLeaf(int idx); // A function to remove the key present in idx-th position in // this node which is a non-leaf node void removeFromNonLeaf(int idx); // A function to get the predecessor of the key- where the key // is present in the idx-th position in the node int getPred(int idx); // A function to get the successor of the key- where the key // is present in the idx-th position in the node int getSucc(int idx); // A function to fill up the child node present in the idx-th // position in the C[] array if that child has less than t-1 keys void fill(int idx); // A function to borrow a key from the C[idx-1]-th node and place // it in C[idx]th node void borrowFromPrev(int idx); // A function to borrow a key from the C[idx+1]-th node and place it // in C[idx]th node void borrowFromNext(int idx); // A function to merge idx-th child of the node with (idx+1)th child of // the node void merge(int idx); // Make BTree friend of this so that we can access private members of // this class in BTree functions friend class BTree;}; class BTree{ BTreeNode root; // Pointer to root node int t; // Minimum degreepublic: // Constructor (Initializes tree as empty) BTree(int _t) { root = NULL; t = _t; } void traverse() { if (root != NULL) root->traverse(); } // function to search a key in this tree //查找这个关键字是否在树中 BTreeNode search(int k) { return (root == NULL)? NULL : root->search(k); } // The main function that inserts a new key in this B-Tree void insert(int k,string stt); // The main function that removes a new key in thie B-Tree void remove(int k); string getone(int k){ string ss=root->getOne(k); return ss; } }; BTreeNode::BTreeNode(int t1, bool leaf1){ // Copy the given minimum degree and leaf property t = t1; leaf = leaf1; // Allocate memory for maximum number of possible keys // and child pointers keys = new int[2t-1]; strs= new string[2t-1]; C = new BTreeNode [2t]; // Initialize the number of keys as 0 n = 0;} // A utility function that returns the index of the first key that is// greater than or equal to k//查找关键字的下标int BTreeNode::findKey(int k){ int idx=0; while (idx<n && keys[idx] < k) ++idx; return idx;}string BTreeNode::getOne(int k){ int idx = findKey(k); string s=strs[idx]; //cout<<“idx:"<<idx<<endl; return s;}// A function to remove the key k from the sub-tree rooted with this nodevoid BTreeNode::remove(int k){ int idx = findKey(k); cout<<idx<<endl; cout<<keys[idx]<<endl; // The key to be removed is present in this node if (idx < n && keys[idx] == k) { // If the node is a leaf node - removeFromLeaf is called // Otherwise, removeFromNonLeaf function is called if (leaf) removeFromLeaf(idx); else removeFromNonLeaf(idx); } else { // If this node is a leaf node, then the key is not present in tree if (leaf) { cout << “The key “<< k <<” is does not exist in the tree\n”; return; } // The key to be removed is present in the sub-tree rooted with this node // The flag indicates whether the key is present in the sub-tree rooted // with the last child of this node bool flag = ( (idx==n)? true : false ); // If the child where the key is supposed to exist has less that t keys, // we fill that child if (C[idx]->n < t) fill(idx); // If the last child has been merged, it must have merged with the previous // child and so we recurse on the (idx-1)th child. Else, we recurse on the // (idx)th child which now has atleast t keys if (flag && idx > n) C[idx-1]->remove(k); else C[idx]->remove(k); } return;} // A function to remove the idx-th key from this node - which is a leaf nodevoid BTreeNode::removeFromLeaf (int idx){ // Move all the keys after the idx-th pos one place backward for (int i=idx+1; i<n; ++i){ keys[i-1] = keys[i]; strs[i-1]=strs[i]; } // Reduce the count of keys n–; return;} // A function to remove the idx-th key from this node - which is a non-leaf nodevoid BTreeNode::removeFromNonLeaf(int idx){ int k = keys[idx]; // If the child that precedes k (C[idx]) has atleast t keys, // find the predecessor ‘pred’ of k in the subtree rooted at // C[idx]. Replace k by pred. Recursively delete pred // in C[idx] if (C[idx]->n >= t) { int pred = getPred(idx); keys[idx] = pred; C[idx]->remove(pred); } // If the child C[idx] has less that t keys, examine C[idx+1]. // If C[idx+1] has atleast t keys, find the successor ‘succ’ of k in // the subtree rooted at C[idx+1] // Replace k by succ // Recursively delete succ in C[idx+1] else if (C[idx+1]->n >= t) { int succ = getSucc(idx); keys[idx] = succ; C[idx+1]->remove(succ); } // If both C[idx] and C[idx+1] has less that t keys,merge k and all of C[idx+1] // into C[idx] // Now C[idx] contains 2t-1 keys // Free C[idx+1] and recursively delete k from C[idx] else { merge(idx); C[idx]->remove(k); } return;} // A function to get predecessor of keys[idx]int BTreeNode::getPred(int idx){ // Keep moving to the right most node until we reach a leaf BTreeNode *cur=C[idx]; while (!cur->leaf) cur = cur->C[cur->n]; // Return the last key of the leaf return cur->keys[cur->n-1];} int BTreeNode::getSucc(int idx){ // Keep moving the left most node starting from C[idx+1] until we reach a leaf BTreeNode *cur = C[idx+1]; while (!cur->leaf) cur = cur->C[0]; // Return the first key of the leaf return cur->keys[0];} // A function to fill child C[idx] which has less than t-1 keysvoid BTreeNode::fill(int idx){ // If the previous child(C[idx-1]) has more than t-1 keys, borrow a key // from that child if (idx!=0 && C[idx-1]->n>=t) borrowFromPrev(idx); // If the next child(C[idx+1]) has more than t-1 keys, borrow a key // from that child else if (idx!=n && C[idx+1]->n>=t) borrowFromNext(idx); // Merge C[idx] with its sibling // If C[idx] is the last child, merge it with with its previous sibling // Otherwise merge it with its next sibling else { if (idx != n) merge(idx); else merge(idx-1); } return;} // A function to borrow a key from C[idx-1] and insert it// into C[idx]void BTreeNode::borrowFromPrev(int idx){ BTreeNode *child=C[idx]; BTreeNode *sibling=C[idx-1]; // The last key from C[idx-1] goes up to the parent and key[idx-1] // from parent is inserted as the first key in C[idx]. Thus, the loses // sibling one key and child gains one key // Moving all key in C[idx] one step ahead for (int i=child->n-1; i>=0; –i){ child->keys[i+1] = child->keys[i]; child->strs[i+1]=child->strs[i]; } // If C[idx] is not a leaf, move all its child pointers one step ahead if (!child->leaf) { for(int i=child->n; i>=0; –i) child->C[i+1] = child->C[i]; } // Setting child’s first key equal to keys[idx-1] from the current node child->keys[0] = keys[idx-1]; child->strs[0]=strs[idx-1]; // Moving sibling’s last child as C[idx]’s first child if (!leaf) child->C[0] = sibling->C[sibling->n]; // Moving the key from the sibling to the parent // This reduces the number of keys in the sibling keys[idx-1] = sibling->keys[sibling->n-1]; strs[idx-1] = sibling->strs[sibling->n-1]; child->n += 1; sibling->n -= 1; return;} // A function to borrow a key from the C[idx+1] and place// it in C[idx]void BTreeNode::borrowFromNext(int idx){ BTreeNode *child=C[idx]; BTreeNode *sibling=C[idx+1]; // keys[idx] is inserted as the last key in C[idx] child->keys[(child->n)] = keys[idx]; child->strs[(child->n)] = strs[idx]; // Sibling’s first child is inserted as the last child // into C[idx] if (!(child->leaf)) child->C[(child->n)+1] = sibling->C[0]; //The first key from sibling is inserted into keys[idx] keys[idx] = sibling->keys[0]; strs[idx] = sibling->strs[0]; // Moving all keys in sibling one step behind for (int i=1; i<sibling->n; ++i) sibling->strs[i-1] = sibling->strs[i]; // Moving the child pointers one step behind if (!sibling->leaf) { for(int i=1; i<=sibling->n; ++i) sibling->C[i-1] = sibling->C[i]; } // Increasing and decreasing the key count of C[idx] and C[idx+1] // respectively child->n += 1; sibling->n -= 1; return;} // A function to merge C[idx] with C[idx+1]// C[idx+1] is freed after mergingvoid BTreeNode::merge(int idx){ BTreeNode *child = C[idx]; BTreeNode sibling = C[idx+1]; // Pulling a key from the current node and inserting it into (t-1)th // position of C[idx] child->keys[t-1] = keys[idx]; child->strs[t-1] = strs[idx]; int i; // Copying the keys from C[idx+1] to C[idx] at the end for (i=0; i<sibling->n; ++i){ child->strs[i+t] = sibling->strs[i]; } // Copying the child pointers from C[idx+1] to C[idx] if (!child->leaf) { for(i=0; i<=sibling->n; ++i) child->C[i+t] = sibling->C[i]; } // Moving all keys after idx in the current node one step before - // to fill the gap created by moving keys[idx] to C[idx] for (i=idx+1; i<n; ++i){ keys[i-1] = keys[i]; strs[i-1] = strs[i]; } // Moving the child pointers after (idx+1) in the current node one // step before for (i=idx+2; i<=n; ++i) C[i-1] = C[i]; // Updating the key count of child and the current node child->n += sibling->n+1; n–; // Freeing the memory occupied by sibling delete(sibling); return;} // The main function that inserts a new key in this B-Treevoid BTree::insert(int k,string stt){ // If tree is empty if (root == NULL) { // Allocate memory for root root = new BTreeNode(t, true); root->keys[0] = k; // Insert key root->strs[0]=stt; root->n = 1; // Update number of keys in root } else // If tree is not empty { // If root is full, then tree grows in height if (root->n == 2t-1) { // Allocate memory for new root BTreeNode s = new BTreeNode(t, false); // Make old root as child of new root s->C[0] = root; // Split the old root and move 1 key to the new root s->splitChild(0, root); // New root has two children now. Decide which of the // two children is going to have new key int i = 0; if (s->keys[0] < k) i++; s->C[i]->insertNonFull(k,stt); // Change root root = s; } else // If root is not full, call insertNonFull for root root->insertNonFull(k,stt); }} // A utility function to insert a new key in this node// The assumption is, the node must be non-full when this// function is calledvoid BTreeNode::insertNonFull(int k,string stt){ // Initialize index as index of rightmost element int i = n-1; // If this is a leaf node if (leaf == true) { // The following loop does two things // a) Finds the location of new key to be inserted // b) Moves all greater keys to one place ahead while (i >= 0 && keys[i] > k) { keys[i+1] = keys[i]; strs[i+1] = strs[i]; i–; } // Insert the new key at found location keys[i+1] = k; strs[i+1]=stt; n = n+1; } else // If this node is not leaf { // Find the child which is going to have the new key while (i >= 0 && keys[i] > k) i–; // See if the found child is full if (C[i+1]->n == 2t-1) { // If the child is full, then split it splitChild(i+1, C[i+1]); // After split, the middle key of C[i] goes up and // C[i] is splitted into two. See which of the two // is going to have the new key if (keys[i+1] < k) i++; } C[i+1]->insertNonFull(k,stt); }} // A utility function to split the child y of this node// Note that y must be full when this function is calledvoid BTreeNode::splitChild(int i, BTreeNode y){ // Create a new node which is going to store (t-1) keys // of y BTreeNode z = new BTreeNode(y->t, y->leaf); z->n = t - 1; int j; // Copy the last (t-1) keys of y to z for (j = 0; j < t-1; j++){ z->keys[j] = y->keys[j+t]; z->strs[j] = y->strs[j+t]; } // Copy the last t children of y to z if (y->leaf == false) { for (int j = 0; j < t; j++) z->C[j] = y->C[j+t]; } // Reduce the number of keys in y y->n = t - 1; // Since this node is going to have a new child, // create space of new child for (j = n; j >= i+1; j–) C[j+1] = C[j]; // Link the new child to this node C[i+1] = z; // A key of y will move to this node. Find location of // new key and move all greater keys one space ahead for (j = n-1; j >= i; j–){ strs[j+1] = strs[j]; } // Copy the middle key of y to this node keys[i] = y->keys[t-1]; strs[i] = y->strs[t-1]; // Increment count of keys in this node n = n + 1;} // Function to traverse all nodes in a subtree rooted with this nodevoid BTreeNode::traverse(){ // There are n keys and n+1 children, travers through n keys // and first n children int i; for (i = 0; i < n; i++) { // If this is not leaf, then before printing key[i], // traverse the subtree rooted with child C[i]. if (leaf == false) C[i]->traverse(); cout << " " << keys[i]; } // Print the subtree rooted with last child if (leaf == false) C[i]->traverse();} // Function to search key k in subtree rooted with this nodeBTreeNode BTreeNode::search(int k){ // Find the first key greater than or equal to k int i = 0; while (i < n && k > keys[i]) i++; // If the found key is equal to k, return this node if (keys[i] == k) return this; // If key is not found here and this is a leaf node if (leaf == true) return NULL; // Go to the appropriate child return C[i]->search(k);} void BTree::remove(int k){ if (!root) { cout << “The tree is empty\n”; return; } // Call the remove function for root root->remove(k); // If the root node has 0 keys, make its first child as the new root // if it has a child, otherwise set root as NULL if (root->n==0) { BTreeNode tmp = root; if (root->leaf) root = NULL; else root = root->C[0]; // Free the old root delete tmp; } return;}hash的引入由于客户端传入的是键值对,考虑到B_树的性质以及数据库的效率,我将作为键key的字符串的值hash后作为B_树中的关键字进行存储,并且仿照关键字数组开辟了一个字符串数组存储值value的值。因此get和set命令的实现做了如下的改动int DJBHash(string str){ unsigned int hash = 5381; for(int i=0;i<str.length();i++) { hash += (hash << 5) + str[i]; } return (hash & 0x7FFFFFFF)%1000;}//get命令void getCommand(Serverserver,Clientclient,string key,string&value){ //取值的时候现将key hash一下,然后再进行取值 int k=DJBHash(key); string ss=client->db->getone(k); if(ss==”"){ cout<<“get null”<<endl; }else{ value=ss; }}//set命令void setCommand(Serverserver,Clientclient,string key,string&value){ //client->db.insert(pair<string,string>(key,value)); //需要将key进行hash转成int int k=DJBHash(key); client->db->insert(k,value);} ...

February 26, 2019 · 13 min · jiezi

数据结构与算法(十四)深入理解红黑树和JDK TreeMap和TreeSet源码分析

本文主要包括以下内容:什么是2-3树2-3树的插入操作红黑树与2-3树的等价关系《算法4》和《算法导论》上关于红黑树的差异红黑树的5条基本性质的分析红黑树与2-3-4树的等价关系红黑树的插入、删除操作JDK TreeMap、TreeSet分析今天我们来介绍下非常重要的数据结构:红黑树。很多文章或书籍在介绍红黑树的时候直接上来就是红黑树的5个基本性质、插入、删除操作等。本文不是采用这样的介绍方式,在介绍红黑树之前,我们要了解红黑树是怎么发展出来的,进而就能知道为什么会有红黑树的5条基本性质。这样的介绍方式也是《算法4》的介绍方式。这也不奇怪,《算法4》的作者 Robert Sedgewick 就是红黑树的作者之一。在介绍红黑树之前,我们先来看下2-3树什么是2-3树在介绍红黑树之前为什么要先介绍 2-3树 呢?因为红黑树是 完美平衡的2-3树 的一种实现。所以,理解2-3树对掌握红黑树是至关重要的。2-3树 的一个Node可能有多个子节点(可能大于2个),而且一个Node可以包含2个键(元素)可以把 红黑树(红黑二叉查找树) 当作 2-3树 的一种二叉结构的实现。在前面介绍的二叉树中,一个Node保存一个值,在2-3树中把这样的节点称之为 2- 节点如果一个节点包含了两个值(可以当作两个节点的融合),在2-3树中把这样的节点称之为 3- 节点。 完美平衡的2-3树所有空链接到根节点的距离都应该是相同的下面看下《算法4》对 2-3-节点的定义:2- 节点,含有一个键(及其对应的值)和两条链接。该节点的左链接小于该节点的键;该节点的右链接大于该节点的键3- 节点,含有两个键(及其对应的值)和三条链接。左链接小于该节点的左键;中链接在左键和右键之间;右链接大于该节点右键如下面一棵 完美平衡的2-3树 :2-3树 是一棵多叉搜索树,所以数据的插入类似二分搜索树2-3树的插入操作红黑树是对 完美平衡的2-3树 的一种实现,所以我们主要介绍完美平衡的2-3树的插入过程完美平衡的2-3树插入分为以下几种情况(为了方便画图默认把空链接去掉):向 2- 结点中插入新键向一棵只含有一个3-结点的树中插入新键因为2-3树中节点只能是2-节点或者3-节点往3-点中再插入一个键就成了4-节点,需要对其进行分解,如下所示:向一个父结点为 2- 结点的 3- 结点插入新键往3-点中再插入一个键就成了4-节点,需要对其进行分解,对中间的键向上融合由于父结点是一个 2- 结点 ,融合后变成了 3- 结点,然后把 4- 结点的左键变成该 3- 节点的中间子结点向一个父结点为3- 结点的 3- 结点中插入新键在这种情况下,向3- 结点插入新键形成暂时的4- 结点,向上分解,父节点又形成一个4- 结点,然后继续上分解一个 4- 结点分解为一棵2-3树6种情况红黑树完美平衡的2-3树和红黑树的对应关系上面介绍完了2-3树,下面来看下红黑树是怎么来实现一棵完美平衡的2-3树的红黑树的背后的基本思想就是用标准的二分搜索树和一些额外的信息来表示2-3树的这额外的信息指的是什么呢?因为2-3树不是二叉树(最多有3叉),所以需要把 3- 结点 替换成 2- 结点额外的信息就是指替换3-结点的方式将2-3树的链接定义为两种类型:黑链接、红链接黑链接 是2-3树中普通的链接,可以把2-3树中的 2- 结点 与它的子结点之间的链当作黑链接红链接 2-3树中 3- 结点分解成两个 2- 结点,这两个 2- 结点之间的链接就是红链接那么如何将2-3树和红黑树等价起来,我们规定:红链接均为左链接根据上面对完美平衡的2-3树和红链接的介绍可以得出结论:没有一个结点同时和两个红链接相连根据上面对完美平衡的2-3树和黑链接的介绍可以得出结论:完美平衡的2-3树是保持完美黑色平衡的,任意空链接到根结点的路径上的黑链接数量相同据此,我们可以得出3条性质:红链接均为左链接没有一个结点同时和两个红链接相连完美平衡的2-3树是保持完美黑色平衡的,任意空链接到根结点的路径上的黑链接数量相同在红黑树中,没有一个对象来表示红链接和黑链接,通过在结点上加上一个属性(color)来标识红链接还是黑链接,color值为red表示结点是红结点,color值为black表示结点是黑结点。黑结点 2-3树中普通的 2-结点 的颜色红结点 2-3树中 3- 结点 分解出两个 2-结点 的最小 2-结点下面是2-3树和红黑树的一一对应关系图:红黑树的5个基本性质的分析介绍完了2-3树和红黑树的对应关系后,我们再来看下红黑树的5个基本性质:每个结点要么是红色,要么是黑色根结点是黑色每个叶子结点(最后的空节点)是黑色如果一个结点是红色的,那么他的孩子结点都是黑色的从任意一个结点到叶子结点,经过的黑色结点是一样的2-3树和红黑树的对应关系后我们也就知道了红黑树的5个基本性质是怎么来的了红黑树的第一条性质:每个节点要么是红色,要么是黑色因为我们用结点上的属性来表示红链还是黑链,所以红黑树的结点要么是红色,要么是黑色是很自然的事情红黑树的第二条性质:根结点是黑色红色节点的情况是 3- 结点分解出两个 2- 结点的最小节点是红色,根节点没有父节点所以只能是黑色红黑树的第三条性质:每个叶子结点(最后的空节点)是黑色叶子节点也就是2-3树中的空链,如果空链是红色说明下面还是有子结点的,但是空链是没有子结点的;另一方面如果空链是红色,空链指向的父结点结点如果也是红色就会出现两个连续的红色链接,就和上面介绍的 “没有一个结点同时和两个红链接相连” 相违背红黑树的第四条性质:如果一个结点是红色的,那么他的孩子结点都是黑色的上面介绍的‘没有一个结点同时和两个红链接相连’,所以一个结点是红色,那么他的孩子结点都是黑色红黑树的第五条性质:从任意一个结点到叶子结点,经过的黑色结点是一样的在介绍完美平衡的2-3树和黑链接我们得出的结论:‘完美平衡的2-3树是保持完美黑色平衡的,任意空链接到根结点的路径上的黑链接数量相同’, 所以从任意一个结点到叶子结点,经过的黑色结点数是一样的红黑树实现2-3树过程中的结点旋转和颜色翻转颜色翻转为什么要颜色翻转(flipColor)?在插入的过程中可能出现如下情况:两个左右子结点都是红色根据我们上面的描述,红链只允许是左链(也就是左子结点是红色)所以需要进行颜色转换:把该结点的左右子结点设置为黑色,自己设置为黑色private void flipColor(Node<K, V> node) { node.color = RED; node.left.color = BLACK; node.right.color = BLACK;}左旋转左旋情况大致有两种:结点是右子结点且是红色颜色翻转后,结点变成红色且它是父结点的右子节点private Node<K, V> rotateLeft(Node<K, V> node) { Node<K, V> x = node.right; node.right = x.left; x.left = node; x.color = node.color; node.color = RED; return x;}右旋转需要右旋的情况:连续出现两个左红色链接private Node<K, V> rotateRight(Node<K, V> node) { Node<K, V> x = node.left; node.left = x.right; x.right = node; x.color = node.color; node.color = RED; return x;}红黑树实现2-3树插入操作通过我们上面对红黑树和2-3树的介绍,红黑树实现2-3树插入操作就很简单了只要满足不出现 两个连续左红色链接、右红色链接、左右都是红色链接 的情况就可以了所以仅仅需要处理三种情况即可:如果出现右侧红色链接,需要左旋如果出现两个连续的左红色链接,需要右旋如果结点的左右子链接都是红色,需要颜色翻转private Node<K, V> _add(Node<K, V> node, K key, V value) { //向叶子结点插入新结点 if (node == null) { size++; return new Node<>(key, value); } //二分搜索的过程 if (key.compareTo(node.key) < 0) node.left = _add(node.left, key, value); else if (key.compareTo(node.key) > 0) node.right = _add(node.right, key, value); else node.value = value; //1,如果出现右侧红色链接,左旋 if (isRed(node.right) && !isRed(node.left)) { node = rotateLeft(node); } //2,如果出现两个连续的左红色链接,右旋 if (isRed(node.left) && isRed(node.left.left)) { node = rotateRight(node); } //3,如果结点的左右子链接都是红色,颜色翻转 if (isRed(node.left) && isRed(node.right)) { flipColor(node); }}public void add(K key, V value) { root = _add(root, key, value); root.color = BLACK;}这样下来红黑树依然保持着它的五个基本性质,下面我们来对比下JDK中的TreeMap的插入操作先按照上面的红黑树插入逻辑插入三个元素 [14, 5, 20],流程如下:使用Java TreeMap来插入上面三个元素,流程如下:通过对比我们发现两者的插入后的结果不一样,而且Java TreeMap是允许左右子结点都是红色结点!这就和我们一直在说的用完美平衡的2-3树作为红黑树实现的基础结构相违背了,我们一直在强调不允许右节点是红色,也不允许两个连续的红色左节点,不允许左右结点同时是红色这也是《算法4》在讲到红黑树时遵循的。但是JDK TreeMap(红黑树)是允许右结点是红色,也允许左右结点同时是红色,Java TreeMap的红黑树实现从它的代码注释(From CLR)说明它的实现来自《算法导论》说明《算法4》和《算法导论》中的所介绍的红黑树产生了一些“出入”,给我们理解红黑树增加了一些困惑和难度《算法4》在介绍红黑树之前先给我们详细介绍了2-3树,然后接着讲到完美平衡的2-3树和红黑树的对应关系(红黑树就等于完美平衡的2-3树),让我们知道红黑树是怎么来的,根据这些介绍你自己是可以解释红黑树的的5个基本性质为什么是这样的。而在《算法导论》中介绍红黑树的时候没有提及2-3树,直接就是红黑树的5个基本性质,以及红黑树的插入、删除操作,感觉对初学者是不太合适的,因为你不知道为什么是这样的,只是知道有这个五个性质,也许这就是为什么它叫导论的原因吧而且在《算法4》中作者最后好像也没有明确的给出红黑树的五个基本性质,在《算法导论》中在红黑树章节一开始就贴出了5条性质,感觉像是一种递进和升华这两本书除了对红黑树讲解的方式存在差异外,我们还发现《算法4》和《算法导论》在红黑树的实现上也是有差异的,就如我们上面插入三个元素 [14, 5, 20] 产生不同的结果在解释这些差异之前,我们再来看些2-3-4树,上面提到完美平衡的2-3树和红黑树等价,更准确的说是2-3-4树和红黑树等价2-3-4树2-3-4树 和 2-3树 非常相像。2-3树允许存在 2- 结点 和 3- 结点,类似的2-3-4树允许存在 2- 结点、3- 结点 和 4- 结点向2-结点、3-结点插入元素向2-结点插入元素,这个和上面介绍的2-3树是一样的,在这里就不叙述了向3-结点插入元素,形成一个4-结点,因为2-3-4树允许4-结点的存在,所以不需要向上分解向4-结点插入元素向4-结点插入元素,需要分解4-结点, 因为2-3-4树最多只允许存在4-结点,如:如果待插入的4-结点,它的父结点也是一个4-结点呢?如下图的2-3-4树插入结点K:主要有两个方案:Bayer于1972年提出的方案:使用相同的办法去分解父结点的4-结点,直到不需要分解为止,方向是自底向上Guibas 和 Sedgewick于1978年提出的方案:自上而下的方式,也就是在二分搜索的过程,一旦遇到4-结点就分解它,这样在最终插入的时候永远不会有父结点是4-结点的情况Bayer全名叫做Rudolf Bayer(鲁道夫·拜尔),他在1972年发明的 对称二叉B树(symmetric binary B-tree) 就是 红黑树(red black tree) 的前身。红黑树 这个名字是由 Leo J. Guibas 和 Robert Sedgewick 于1978年的一篇论文中提出来的,对该论文感兴趣的可以查看这个链接:http://professor.ufabc.edu.br…下面的图就是 自上而下 方案的流程图2-3-4树和红黑树的等价关系在介绍2-3树的时候我们也讲解了2-3树和红黑树的等价关系,由于2-3树和2-3-4树非常类似,所以2-3-4树和红黑树的等价关系也是类似的。不同的是2-3-4的 4-结点 分解后的结点颜色变成如下形式:所以可以得出下面一棵2-3-4树和红黑树的等价关系图:上面在介绍红黑树实现2-3树的时候讲解了它的插入操作:private Node<K, V> _add(Node<K, V> node, K key, V value) { //向叶子结点插入新结点 if (node == null) { size++; return new Node<>(key, value); } //二分搜索的过程 if (key.compareTo(node.key) < 0) node.left = _add(node.left, key, value); else if (key.compareTo(node.key) > 0) node.right = _add(node.right, key, value); else node.value = value; //1,如果出现右侧红色链接,左旋 if (isRed(node.right) && !isRed(node.left)) { node = rotateLeft(node); } //2,如果出现两个连续的左红色链接,右旋 if (isRed(node.left) && isRed(node.left.left)) { node = rotateRight(node); } //3,如果结点的左右子链接都是红色,颜色翻转 if (isRed(node.left) && isRed(node.right)) { flipColor(node); }}我们可以很轻松的把它改成2-3-4的插入逻辑(只需要把颜色翻转的逻辑提到二分搜索的前面即可):private Node<K, V> _add(Node<K, V> node, K key, V value) { //向叶子结点插入新结点 if (node == null) { size++; return new Node<>(key, value); } //split 4-nodes on the way down if (isRed(node.left) && isRed(node.right)) { flipColor(node); } //二分搜索的过程 if (key.compareTo(node.key) < 0) node.left = _add(node.left, key, value); else if (key.compareTo(node.key) > 0) node.right = _add(node.right, key, value); else node.value = value; //fix right-leaning reds on the way up if (isRed(node.right) && !isRed(node.left)) { node = rotateLeft(node); } //fix two reds in a row on the way up if (isRed(node.left) && isRed(node.left.left)) { node = rotateRight(node); }}//使用2-3-4树插入数据 [E,C,G,B,D,F,J,A]RB2_3_4Tree<Character, Character> rbTree = new RB2_3_4Tree<>();rbTree.add(‘E’, ‘E’);rbTree.add(‘C’, ‘C’);rbTree.add(‘G’, ‘G’);rbTree.add(‘B’, ‘B’);rbTree.add(‘D’, ‘D’);rbTree.add(‘F’, ‘F’);rbTree.add(‘J’, ‘J’);rbTree.add(‘A’, ‘A’);rbTree.levelorder(rbTree.root);//使用2-3树插入数据 [E,C,G,B,D,F,J,A]RBTree<Character, Character> rbTree = new RBTree<>();rbTree.add(‘E’, ‘E’);rbTree.add(‘C’, ‘C’);rbTree.add(‘G’, ‘G’);rbTree.add(‘B’, ‘B’);rbTree.add(‘D’, ‘D’);rbTree.add(‘F’, ‘F’);rbTree.add(‘J’, ‘J’);rbTree.add(‘A’, ‘A’);rbTree.levelorder(rbTree.root);下面是 2-3-4树 和 2-3树 插入结果的对比图:所以我们一开始用红黑树实现完美平衡的2-3树,左右结点是不会都是红色的现在用红黑树实现2-3-4树,左右结点的可以同时是红色的,这样的红黑树效率更高。因为如果遇到左右结点是红色,就进行颜色翻转,还需要对红色的父结点进行向上回溯,因为父结点染成红色了,可能父结点的父结点也是红色,可能需要进行结点旋转或者颜色翻转操作,所以说2-3-4树式的红黑树效率更高。所以回到上面我们提到《算法4》和《算法导论》在实现上的差异的问题,就很好回答了,因为《算法4》是用红黑树实现2-3树的,并不是2-3-4树。但是如果是用红黑树实现2-3-4树就和《算法导论》上介绍的红黑树一样吗?不一样。下面继续做一个测试,分别往上面红黑树实现的 2-3-4树 和 JDK TreeMap 中插入[E, D, R, O, S, X]虽然两棵树都是红黑树,但是却不一样。并且TreeMap允许右节点是红色,在2-3-4树中最多是左右子结点同时是红色的情况,不会出现左结点是黑色,右边的兄弟结点是红色的情况,为什么会有这样的差异呢?从上面的2-3-4树的插入逻辑可以看出,如果右节点是红色会执行左旋转操作,所以不会出现单独红右结点的情况也就是说只会出现单独的左结点是红色的情况,我们把这种形式的红黑树称之为左倾红黑树(Left Leaning Red Black Tree),包括上面的红黑树实现的完美平衡的2-3树也是左倾红黑树为什么在《算法4》中,作者规定所有的红色链接都是左链接,这只是人为的规定,当然也可以是右链接,规定红链接都是左链,可以使用更少的代码来实现黑色平衡,需要考虑的情况会更少,就如上面我们介绍的插入操作,我们只需要考虑3中情况即可。但是一般意义上的红黑树是不需要维持红色左倾的这个性质的,所以为什么TreeMap是允许单独右红结点的如果还需要维护左倾情况,这样的话就更多的操作,可能还需要结点旋转和颜色的翻转,性能更差一些,虽然也是符合红黑树的性质介绍完了《算法4》上的红黑树,下面就来分析下一般意义上的红黑树的 插入 和 删除 操作,也就是《算法导论》上介绍的红黑树。红黑树插入操作插入操作有两种情况是非常简单的,所以在这里单独说一下:case 1. 如果插入的结点是根结点,直接把该结点设置为黑色,整个插入操作结束如下图所示:case 2. 如果插入的结点的父结点是黑色,也无需调整,整个插入操作结束如下图所示:下面开始介绍比较复杂的情况红黑树插入操作,我们只需要处理父结点是红色的情况,因为一开始红黑树肯定是黑色平衡的,就是因为往叶子节点插入元素后可能出现两个连续的红色的结点需要注意的是,我们把新插入的结点默认设置为红色,初始的时候,正在处理的节点就是插入的结点,在不断调整的过程中,正在处理的节点会不断的变化,且叔叔、爷爷、父结点都是相对于当前正在处理的结点来说的case 3. 叔叔结点为红色,正在处理的节点可以是左也可以是右结点调整策略:由于父结点是红色,叔叔结点是红色,爷爷结点是黑色,执行颜色翻转操作然后把当前正在处理的结点设置为爷爷结点,如果爷爷的父结点是黑色插入操作结束,如果是红色继续处理case 4. 叔叔结点为黑色,正在处理的结点是右结点调整策略:由于父结点是红色,叔叔结点为黑色,那么爷爷结点肯定是黑色把正在处理的节点设置为父结点,然后左旋,形成Case5情况case 5. 叔叔结点为黑色,正在处理的结点是左孩子调整策略:由于父结点是红色,叔叔结点为黑色,那么爷爷结点肯定是黑色把父结点染黑,爷爷结点染红,然后爷爷结点右旋Case3、Case4、Case5如果单独来理解的话比较困难,就算单独为每一个Case画图,我觉得也很难完整的理解,很多博客上都是这种方式,感觉不太好理解。我将这三种情况通过一张流程图串联起来,将这三个Case形成一个整体,蓝色箭头表示正在处理的结点,如下所示:红黑树删除操作上面介绍完了红黑树的插入操作,接下来看下红黑树的删除操作红黑树的删除操作比插入操作更加复杂一些为了描述方便,我们把正在处理的结点称之为 X,父结点为 P(Parent),兄弟节点称之为 S(Sibling),左侄子称之为 LN(Left Nephew),右侄子称之为 RN(Right Nephew)如果删除的结点是黑色,那么就导致本来保持黑平衡的红黑树失衡了,从下图可以看出结点P到左子树的叶子结点经过的黑节点数量为4(2+2),到右子树的叶子节点经过的黑色节点数量是5(2+3),如下图所示:红黑树的删除操作,如果删除的是黑色会导致红黑树就不能保持黑色平衡了,需要进行调整了;如果删除的是红色,那么就无需调整,直接删除即可,因为没有没有破坏黑色平衡删除结点后,无需调整的情况case 1 删除的结点是红色结点,直接删除即可case 2 删除的节点是黑色,如果当前处理的节点X是根结点无论根结点是什么颜色,都将根结点设置为黑色case 3 删除的结点是黑色,如果当前处理的结点是红色结点,将该结点设置为黑色因为删除黑色结点后,就打破了黑色平衡,黑高少了1所以把一个红色节点设置为黑色,这样黑高又平衡了删除节点后,需要调整的情况正在处理的结点为X,要删除的结点是左结点,分为4中情况:case 4 兄弟结点为红色调整方案:兄弟设置为黑色,父结点设置为红色,父结点进行左旋转转化为 case5、case6、case7case 5 兄弟结点为黑色,左侄子LN为黑色,右侄子RN为黑色在这种条件下,还有两种情况:父结点是红色或黑色,不管是那种情况,调整方案都是一致的调整方案:将兄弟结点设置为红色,把当前处理的结点设置为父结Pcase 6 兄弟结点为黑色,左侄子为红色,右侄子RN为黑色调整方案:将左侄子结点设置为黑色,兄弟结点设置为红色,兄弟结点右旋转,这样就转化成了case7case 7 兄弟结点为黑色,左侄子不管红黑,右侄子为红色处理方式:兄弟结点变成父结点的颜色,然后父结点设置黑色,右侄子设置黑色,父结点进行左旋转和插入操作一样,下面通过一张流程图把删除需要调整的情况串联起来:上面处理的所有情况都是基于正在处理的结点是左结点如果要调整正在处理的结点是右节点的情况,就是上面的处理的镜像。插入操作也是同理,所以就省略了Java TreeMap、TreeSet源码分析TreeMap底层就是用红黑树实现的,它在插入后调整操作主要在fixAfterInsertion方法里,我为每种情况都添加注释,如下所示:/** From CLR /private void fixAfterInsertion(Entry<K,V> x) { x.color = RED; while (x != null && x != root && x.parent.color == RED) { if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { Entry<K,V> y = rightOf(parentOf(parentOf(x))); //—–Case3情况—– if (colorOf(y) == RED) { setColor(parentOf(x), BLACK); setColor(y, BLACK); setColor(parentOf(parentOf(x)), RED); x = parentOf(parentOf(x)); } else { //—–Case4情况—– if (x == rightOf(parentOf(x))) { x = parentOf(x); rotateLeft(x); } //—–Case5情况—– setColor(parentOf(x), BLACK); setColor(parentOf(parentOf(x)), RED); rotateRight(parentOf(parentOf(x))); } } else { //省略镜像情况 } } root.color = BLACK;}它的删除后调整操作主要在fixAfterDeletion方法:/* From CLR */private void fixAfterDeletion(Entry<K,V> x) { while (x != root && colorOf(x) == BLACK) { if (x == leftOf(parentOf(x))) { Entry<K,V> sib = rightOf(parentOf(x)); //—–Case4的情况—– if (colorOf(sib) == RED) { setColor(sib, BLACK); setColor(parentOf(x), RED); rotateLeft(parentOf(x)); sib = rightOf(parentOf(x)); } //—–Case5的情况—– if (colorOf(leftOf(sib)) == BLACK && colorOf(rightOf(sib)) == BLACK) { setColor(sib, RED); x = parentOf(x); } else { //—–Case6的情况—– if (colorOf(rightOf(sib)) == BLACK) { setColor(leftOf(sib), BLACK); setColor(sib, RED); rotateRight(sib); sib = rightOf(parentOf(x)); } //—–Case7的情况—– setColor(sib, colorOf(parentOf(x))); setColor(parentOf(x), BLACK); setColor(rightOf(sib), BLACK); rotateLeft(parentOf(x)); x = root; } } else { // symmetric //省略镜像的情况 } } setColor(x, BLACK);}TreeSet 底层就是用 TreeMap 来实现的,往TreeSet添加进的元素当作TreeMap的key,TreeMap的value是一个常量Object。掌握了红黑树,对于这两个集合的原理就不难理解了。最后本文从一开始讲的2-3树和红黑树的对应关系,再到2-3-4树和红黑树的对应关系,再到《算法4》和《算法导论》JDK TreeMap在红黑树上的差异然后详细介绍了红黑树的插入、删除操作,最后分析了下Java中的TreeMap和TreeSet集合类。人生当如红黑树,当过于自喜或过于自卑的时候,应当自我调整,寻求平衡。我很丑,红黑树却很美。希望本文对你有 些许帮助。参考资料:https://www.cs.princeton.edu/…http://professor.ufabc.edu.br…《算法4》、《算法导论》 ...

February 22, 2019 · 4 min · jiezi

造个轮子 | 自己用C++实现Redis

最近学习了Redis,对其内部结构较为感兴趣,为了进一步了解其运行原理,我打算自己动手用C++写一个redis。这是我第一次造轮子,所以纪念一下 ^ _ ^。源码github链接,项目现在实现了客户端与服务器的链接与交互,以及一些Redis的基本命令,下面是测试结果:(左边是服务端,右边是客户端)为了完善其功能并且锻炼一下自己的数据结构与算法,我下一阶段打算根据《Redis设计与实现》一书优化数据结构与算法从而完善自己的项目。基本结构介绍基本流程介绍首先是对服务端的初始化,包括数据库的初始化以及命令集合的初始化。在客户端连接之后,开始创建客户端对其进行初始化,并且将其与服务端对应的数据库进行连接。在客户端发送命令之后,服务端接受命令,对命令的合法性进行判断,然后在命令集合中查找相关命令并执行,最后返回执行结果给客户端。

February 20, 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

JavaScript数据结构与算法——字典

1.字典数据结构在字典中,存储的是【键,值】对,其中键名是用来查询特定元素的。字典和集合很相似,集合以【值,值】的形式存储,字典则是用【键,值】对的形式存储。字典也称作映射。2.创建字典function Dictionary() { let items = {}; // 1.has(如果某个键值存在于字典中,返回true,否则返回false)和set(向字典中添加元素)方法 this.has = function(key) { return items.hasOwnProperty(key); } this.set = function(key, value) { items[key] = value; } // 2.delete 根据传来的key删除某个元素 this.delete = function(key) { if(this.has(key)) { delete.items[key]; return true; } return false; } // 3.get和values方法 this.get = function(key) { return this.has(key) ? items[key] : undefined; } this.values = function() { let values = []; for(let k in items) { if(this.has(key)) { values.push(items[k]); } } return values; } // 4. clear,size,keys和getItems方法 // clear和size方法同集合类中的一样 this.keys = function() { return Object.keys[items]; } this.getItems = function() { return items; }} ...

February 18, 2019 · 1 min · jiezi

JavaScript数据结构与算法——集合

1.集合数据结构集合是一组无序且唯一(不能重复)的项组成的。这个数据结构使用了和有限集合相同的数学概念。2.创建集合function Set() { // 这里使用对象而不是数组来表示集合 // js对象中不允许一个键值指向两个不同属性,也保证了集合中的元素都是唯一的 let items = {}; //1.首先实现has(value)方法 this.has = function(value) { return value in items; //return items.hasOwnProperty(value); } //2.向集合添加一个项 this.add = function(value) { if (!this.has(value)) { items[value] = value; return true; } else{ return false; } } //3.移除某一项和清空集合 this.remove = function(value) { if (this.has(value)) { delete items[value]; return true; } else{ return false; } } this.clear = function() { items = {}; } //4.返回集合长度 this.size = function() { return Object.keys(items).length; } // 兼容性更好 this.sizeLegacy = function() { let count = 0; for(let key in items) { if(items.hasOwnProperty(key)) ++count; } return count; } //5.返回一个包含集合中所有值的数组 this.values = function() { let values = []; for (let i = 0, keys=Object.keys[items]; i < keys.length; i++) { values.push(items[keys[i]]) }; return values; } // 兼容性更好 this.valuesLegacy = function() { let values = []; for (let key in items) { if(items.hasOwnProperty(key)) { values.push(items[keys) } }; return values; }}集合的使用let set = new Set();set.add(1);console.log(set.values()); // [‘1’]console.log(set.has(1)); // trueconsole.log(set.size()); // 1set.add(2);console.log(set.values()); // [‘1’, ‘2’]console.log(set.has(2)); // trueconsole.log(set.size()); // 2set.remove(1);console.log(set.values()); // [‘2’]console.log(set.has(1)); // falseconsole.log(set.size()); // 13.集合的操作集合有:并集、交集、差集、子集// 1.实现并集this.union = function(otherSet) { let unionSet = new Set(); let values = this.values(); for(let i=0; i<values.length; i++ ){ unionSet.add(values[i]) } values = otherSet.values(); for(let i=0; i<values.length; i++ ){ unionSet.add(values[i]) } return unionSet;}// 2.实现交集this.intersection = function(otherSet){ let intersectionSet = new Set(); let values = this.values(); for(let i=0; i<values.length; i++ ){ if(otherSet.has(values[i])) { intersectionSet.add(values[i]) } } return intersectionSet;}// 3.实现差集this.difference = function(otherSet) { let differenceSet = new Set(); let values = this.values(); for(let i=0; i<values.length; i++ ){ if(!otherSet.has(values[i])) { differenceSet.add(values[i]) } } return differenceSet;}// 4.子集this.subset = function(otherSet) { if(this.size() > otherSet.size()) { return false; } else { let values = this.values(); for(let i=0; i<values.length; i++ ){ if(!otherSet.has(values[i])) { return true } } return true; }}在es6中新增了set类,我们也可以使用其中自带的方法。 ...

February 16, 2019 · 2 min · jiezi

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

1.链表数据结构链表存储有序的元素集合,但不同于数组,链表中的元素咋内存中并不是连续放置的每个元素有一个存储元素本身的节点和一个指向下一个元素的引用组成。下图展示了一个链表的结构:链表的优点: 链表是很常用的一种数据结构,不需要初始化容量,可以任意加减元素; 添加或者删除元素时只需要改变前后两个元素结点的指针域指向地址即可,所以添加,删除很快;缺点: 因为含有大量的指针域,占用空间较大; 查找元素需要遍历链表来查找,非常耗时。适用场景: 数据量较小,需要频繁增加,删除操作的场景2.创建链表function LinkedList() { // 创建一个node类,表示将要加入的项 element表示要添加的值,next指向列表中下一个节点项的指针 let Node = function(element) { this.element = element; this.next = null; } let length = 0; let head = null; // 下面声明链表的方法 // 1.向列表尾部添加一个新的项 this.append = function(element) { let node = new Node(element); current; // 列表为空,添加的是第一个元素 if(head === null) { head = node; // 列表不为空,向其追加 } else { current = head; // 循环列表,直到找到最后一项 while(current.next) { current = current.next; } // 找到最后一项,将其next赋为node,建立链接 current.next = node; } // 更新列表长度 length++; } // 2.从列表中移除元素 this.removeAt = function(position) { // 检查position是否超出链表范围 if(position > -1 && position < length) { let current = head, previous, index = 0; // 移除第一个元素 if(position === 0 ) { head = current.next; } else { // 循环到指定位置,找到该位置的 previous和current while(index++ < position) { previous = current; current = current.next; } // 将previous与current的下一项链接起来:跳过current previous.next = current.next; } // 链表长度减一 length–; // 返回被删除项 return current.element; } else { // 不是有效位置,返回null return null } } // 3.在任意位置插入元素 this.insert = function(element, position) { //判断是否超过链表范围 if (position >= 0 && position<= length) { let node = new Node(element), current = head, previous, index = 0; // 在首位插入元素 if (position === 0 ) { node.next = current; head = node; } else{ // x循环到position应该添加的位置,取出previous和current while(index++ < position) { previous = current; current = current.next; } // 在previous和current间插入 node.next = current; previous.next = node; }; // 更新链表长度 length++; return true; } else{ return false; }; } //4. 把LinkedList对象转化成字符串 this.toString = function() { let current = head, string = ‘’; while(current) { string += current.element + (current.next ? ’n’ : ‘’); current = current.next; } return string; } //5.链表中是否存在某个值 this.indexOf = function(element) { let current = head, index = 0; while(current) { if(element === current.element) { return index; } index++; current = current.next; } return -1; } //6.通过element实现remove元素 this.remove = function(element) { let index = this.indexOf(element); return this.removeAt(index); } //7.isEmpty size getHead方法 this.isEmpty = function() { return length === 0; } this.size = function() { return length; } this.getHead = function() { return head; } } ...

February 15, 2019 · 2 min · jiezi

JavaScript数据结构与算法——队列

队列和栈非常类似,但是使用了不同的原则,而非后进先出,是先进先出。1.队列数据结构队列遵循FIFO(先进先出,也称先来先服务)原则的一组有序的项。队列在尾部添加新元素,并从顶部移除元素。最新添加的元素必须排在队列的的末尾。队列示意图如下:2.创建队列// 创建一个类表示队列function Queue() { // 使用数组作为存储队列的数据结构 let items = []; // 下面声明队列一些可用的方法 // 1.enqueue(elements) 向队尾添加一个或多个项 this.enqueue = function(element) { items.push(element); } // 2.dequeue() 从队列移除元素 FIFO this.dequeue = function() { return item.shift(); } // 3.front() 查看队列头元素 this.front = function() { return items[0]; } // 4.isEmpty() size() 检查队列是否为空 this.isEmpty = function() { return items.length === 0; } this.size = function() { return items.length; } // 5.打印队列元素 this.print = function() { console.log(items.toString()) } }使用Queue类let queue = new Queue();console.log(queue.isEmpty()); // true// 添加元素queue.enqueue(‘june’);queue.enqueue(‘jack’);queue.print(); // ‘june,jack’console.log(queue.size()); // 2// 删除元素queue.dequeue();queue.dequeue();queue.print(); // ‘‘3.优先队列实现一个有限队列,有两种选择:设置优先级,然后在正确的位置添加元素;或者用入列操作添加元素,然后按照他们的优先级移除他们。function PriorityQueue() { let items = []; // 设置添加元素的类 function QueueElement(element, priority) { this.element = element; this.priority = priority; } // 优先级添加 this.enqueue = function(element, priority) { let queueElement = new QueueElement(element, priority); let added = false; // 遍原队列中的元素,如果新添加元素的优先级的值(优先级大,priority值小)小于当前遍历原始的优先级的值(即新添加元素优先级大于当前遍历元素的优先级),则在其前面添加新的元素 for(let i=0; i<items.length; i++) { if(queueElement.priority < items[i].priority) { items.splice(i, 0, queueElement); added = true; break; } } if(!added) { items.push(queueElement); } } // 打印 this.print = function() { for(let i=0; i<items.length; i++) { console.log(${items[i].element}-${items[i].priority}) } } // 其他方法和默认的Queue实现方式相同}4.队列的应用——击鼓传花????(循环队列)// nameList-参与的人 num-每轮传????次数function hotPotato(nameList, num) { let queue = new Queue(); // 初始化队列 for(let i=0; i<nameList.length; i++) { queue.enqueue(nameList); } // let eliminated = ‘’; // 进行循环队列的入队和出队 while(queue.size() > 1) { for(let i=0; i<num; i++) { queue.enqueue(queue.dequeue); } // 传花停止-淘汰队列第一个 eliminated = queue.dequeue(); console.log(eliminated + ‘在击鼓传花????中被淘汰。’) } // 最后一个出队列的为胜者???? return queue.dequeue();} ...

February 14, 2019 · 1 min · jiezi

JavaScript数据结构与算法—— 栈

我们可以在数组的任何位置上删除或者添加元素,但有时候我们还需要在元素的添加或删除时有更多控制的数据结构,有两种数据结构类似于数组,但在添加或删除元素时更为可控,它们就是栈和队列。本节主要介绍栈。1.栈数据结构栈是一种遵循后进先出(LIFO)原则的有序集合。新添加的或待删除的元素都保存在栈的同一端,叫做栈顶,另一端叫栈底。在栈中,新元素靠近栈顶,旧元素接近栈底。如下图所示:2.创建栈// 首先创建一类表示栈function Stack(){ let items = []; // 选择数组来保存栈中的元素 //各种属性和方法}下面要为栈声明一些方法:push() // 添加一个或多个新元素到栈顶pop() // 移除栈顶元素,同时返回被移除的元素peek() // 仅仅返回栈顶元素,不对栈做任何修改isEmpty() // 如果栈中没有元素返回true,否则返回falseclear() // 移除栈中所有元素size() // 返回栈中元素的个数(和数组length类似) 2.1 向栈添加元素this.push() = function(element) { items.push(element);}2.2 从栈移除元素this.pop = function() { items.pop();}2.3 查看栈顶元素this.peek = function() { return items[items.length - 1];}2.4 查看栈是否为空this.isEmpty = function(){ return items.length === 0;}this.size = function() { return items.lenght;}2.5 清空和打印栈元素this.clear = function() { items = [];} this.print = function() { console.log(items.toString());}经过上述的方法的添加,我们就完整的创建了一了个栈 Stack。3.栈的应用—十进制转N进制首先我们写出将十进制转为二进制:function divideBy2(decNum) { var remStack = new Stack(), rem, binaryString = ‘’; // 将十进制数除2的余数放入一个stack中 while(decNum > 0) { // 取余 rem = Math.floor(decNum % 2); // 入栈 remStack.push(rem); decNum = Math.floor(decNum / 2); } // 从栈中取出转化为字符串然后连接起来构成二进制 while(!remStack.isEmpty()) { // 出栈 binaryString += remStack.pop().toString(); } return binaryString;}下面十进制转化N进制算法function divideByN(decNum, n) { var remStack = new Stack(), rem, binaryString = ‘’, digits = ‘0123456789ABCDEF’; // 将十进制数除N的余数放入一个stack中 while(decNum > 0) { // 取余 rem = Math.floor(decNum % n); // 入栈 remStack.push(rem); decNum = Math.floor(decNum / n); } // 从栈中取出转化为字符串然后连接起来构成二进制 while(!remStack.isEmpty()) { // 出栈 使用digits方便在16进制中做个对应转化 binaryString += digits[remStack.pop()]; } return binaryString;} ...

February 14, 2019 · 1 min · jiezi

JavaScript数据结构与算法——数组

数据结构的分类数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成 。 常用的数据结构有:数组,栈,链表,队列,树,图,堆,散列表等,如图所示: 数组数组是最简单的内存数据结构,数组是可以再内存中连续存储多个元素的结构,在内存中的分配也是连续的,数组中的元素通过数组下标进行访问,数组下标从0开始。tips:数据一般存储着一系列数据类型相同的值,但在JavaScript中,可以在数组中保存不同类型的值,但一般不需要这么用。1.创建数组let daysOfWeek = new Array();let daysOfWeek = new Array(7);let daysOfWeek = new Array(‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’);2.添加元素// 初始化nums数组let nums = [0,1,2,3,4,5,6];// 指定位置添加nums[nums.length] = 7;// 使用push(),把元素添加到数组末尾nums.push(8);// 0…8nums.push(9, 10);// 0…10// 使用unshift,把元素添加到数组首位nums.unshift(-1);// -1…10nums.unshift(-3,-2);// -3…103.删除元素// pop(),删除最后一个nums.pop();//-3…9// shift(),删除第一个nums.shift();//-2…94.任何位置删除或添加元素// splice()方法nums.splice(2, 3);// 删除 index=2 开始的后的3个数 -2,-1,3…9nums.splice(2, 0, 0, 1, 2);// 从index=2开始插入0,1,2 -2…95.javascript数组方法参考concat() // 连接2个或多个数组,并返回结果every() // 对数组中的每一项运行给定函数,如果该函数对每一项都返回true,则返回truefilter() // 对数组中的每一项运行给定函数,返回该函数能返回true的项作为新数组forEach() // 对数组中的每一项运行给定函数,没有返回值join() // 按传入的字符连接成一个字符串indexOf() // 从前往后遍历,返回第一个与传入参数相等的索引值,没找到返回-1lastIndexOf() // 从后往前遍历,返回第一个与传入参数相等的索引值map() // 对数组中的每一项运行给定函数,返回每次函数调用的结果组成新的数组reverse() // 颠倒数组中元素的顺序slice() // 传入索引值,将数组对应索引值范围内的元素作为新数组返回 some() // 对数组中的每一项运行给定函数,如果某一项返回true,则返回truesort() // 按照字母顺序排序,支持传入指定排序方法的函数作为参数toString() // 将数组作为字符串返回valueOf() // 和toString类似,将数组作为字符串返回6.ES6数组新增方法@@iterator // 返回一个包含数组键值对的迭代器对象,可通过同步调用得到数组元素的键值对copyWithin() // 复制数组中一系列元素,到该数组指定的起始位置entries() // 返回包含数组所有键值对的@@iteratorincludes() // 数组中存在某个元素则返回true,否则返回false(es7新增)find() // 根据回调函数给定的条件从数组中查找元素,如果找到则返回该元素findIndex() // 根据回调函数给定的条件从数组中查找元素,如果能找到就返回该元素在数组中的索引fill() // 用传入参数填充数组from() // 根据已有数组创建一个新数组keys() // 返回包含数组所有索引的@@iteratorof() // 根据传入的参数创建一个新数组values() // 返回包含数组中所有值的@@iterator7.数组优缺点优点: (1)按照索引查询元素速度快 (2)按照索引遍历数组方便缺点: (1)数组的大小固定后就无法扩容了 (2)数组只能存储一种类型的数据 (3)添加,删除的操作慢,因为要移动其他的元素。适用场景: 频繁查询,对存储空间要求不大,很少增加和删除的情况。 ...

February 12, 2019 · 1 min · jiezi

[ JavaScript ] 数据结构与算法 —— 队列

前言JavaScript是当下最流行的编程语言之一,它可以做很多事情:数据可视化(D3.js,Three.js,Chart.js);移动端应用(React Native,Weex,AppCan,Flutter,Hybrid App,小程序);服务端(Express.js,Koa2);桌面应用(Electron,nw.js);游戏,VR,AR(LayaAir,Egret,Turbulenz,PlayCanvas);等等。。。而且目前大部分编程语言的高级应用都会用到数据结构与算法以及设计模式。本篇主要有三部分什么是队列队列的实现队列的变种什么是队列较官方解释队列是遵循FIFO(First In First Out,先进先出,也称为先来先服务)原则的一组有序的项。队列在尾部添加新元素,并从顶部移除元素。最新添加的元素必须排在队列的末尾 。注:出队入队是自己加的,不知道是不是这么叫个人理解我看有很多文档都是说队列就像买什么东西排队,我认为这个比喻用在标准队列上不恰当。 我觉得标准队列像是一段管道,每次都只能放一个球进去,上边只用来放球(入队),由于地球引力,球会从下边的口出去(出队)。队列:这段可以放球的管道就是队列 元素:管道里的球 队首:在当前管道里的球最早放进去的那个球的位置,同时也是第一个掉出去的球 队尾:在当前管道里的球最后放进去的那个球的位置,同时也是最后一个掉出去的球队列的实现添加队列成员删除队列成员返回队首的成员队列是否为空清空队列队列长度返回字符串形式的队列成员class Queue { constructor() { this.count = 0; // 整个队列下一成员的位置 this.lowestCount = 0; // 在第一位的成员位置 this.items = {}; // 用来存放的队列 } enqueue(element) { // 添加队列成员 进入队列 } dequeue() { // 删除队列成员 离开队列 } peek() { // 返回队首的成员 } isEmpty() { // 判断队列是否为空 } clear() { // 将所有的数据初始化 } size() { // 队列长度 } toString() { // 返回字符串形式的队列成员 }}添加队列成员enqueue(element) { this.items[this.count] = element; // 将元素放入队列 this.count++; // 将计数器加一}删除队列成员dequeue() { if (this.isEmpty()) { // 如果是空 return undefined; // 返回未定义 undefined } const result = this.items[this.lowestCount]; // 将队首的成员保存下 delete this.items[this.lowestCount]; // 将队首的成员删除掉 删除对象属性 this.lowestCount++; // 将队列提前一位 指向队首的指针后移一位 return result; // 返回被删除的成员}返回队首的成员peek() { if (this.isEmpty()) { // 非空才能继续处理 return undefined; } return this.items[this.lowestCount];}队列是否为空isEmpty() { // 判断长度是不是 0 return this.size() === 0;}清空队列clear() { this.count = 0; // 恢复初始值 this.lowestCount = 0; // 恢复初始值 this.items = {}; // 重新赋值空对象}队列长度size() { // 队列长度 等于 整个队列下一成员的位置 减去 在第一位的成员位置 return this.count - this.lowestCount;}返回字符串形式的队列成员toString() { if (this.isEmpty()) { return ‘’; } let objString = ${this.items[this.lowestCount]}; for (let i = this.lowestCount + 1; i < this.count; i++) { objString = ${objString},${this.items[i]}; } return objString;}队列的变种优先队列类似去医院看病,急诊,会优先一般的门诊循环队列类似抢凳子游戏,队列首位相连优先队列在添加成员时会判断优先级,class QueueElement (element, priority){ // 队列成员类 this.element = element; // 存放成员 this.priority = priority; // 存放优先级 } enqueue(element, priority){ let queueElement = new QueueElement(element, priority); // 添加成员 let added = false; // 是否已添加到队列 for (let i = 0; i < this.size(); i++){ // 遍历队列 if (queueElement.priority < items[i].priority){ // 寻找优先级低的成员,并插入到其之前 // splice start for(let j = this.size(); j > i; j–){ items[j] = items[j-1]; } items[i] = queueElement; // splice end added = true; // 标识符置为真,表示已经添加 break; // 跳出循环 } } if (!added){ // 如果没有找到优先级比新添加的成员低的,那么将其添加到队尾 items.push(queueElement); } }; 循环队列在操作时每删除一个队列成员就将删除的这个队列成员重新添加到队列中for (let i = 0; i < number; i++){ queue.enqueue(queue.dequeue());} ...

January 31, 2019 · 2 min · jiezi

《剑指offer》11.链表中倒数第k个节点

题目输入一个链表,输出该链表中倒数第k个结点。思路简单思路: 循环到链表末尾找到 length 在找到length-k节点 需要循环两次。优化:设定两个节点,间距相差k个节点,当前面的节点到达终点,取后面的节点。前面的节点到达k后,后面的节点才出发。本题目着重考察代码鲁棒性、容错率: 需要考虑head为null,k为0,k大于链表长度的情况代码 function FindKthToTail(head, k) { if (!head || !k) return null; let front = head; let behind = head; let index = 1; while (front.next) { index++; front = front.next; if (index > k) { behind = behind.next; } } return (k <= index) && behind; }

January 27, 2019 · 1 min · jiezi

JS数据结构与算法_链表

上一篇:JS数据结构与算法_栈&队列写在前面说明:JS数据结构与算法 系列文章的代码和示例均可在此找到上一篇博客发布以后,仅几天的时间竟然成为了我写博客以来点赞数最多的一篇博客。欢喜之余,不由得思考背后的原因,前端er离数据结构与算法太遥远了,论坛里也少有人去专门为数据结构与算法撰文,才使得这看似平平的文章收获如此。不过,这样也更加坚定了我继续学习数据结构与算法的决心(虽然只是入门级的)一、链表数据结构相较于之前学习的 栈/队列 只关心 栈顶/首尾 的模式,链表更加像是数组。链表和数组都是用于存储有序元素的集合,但有几点大不相同链表不同于数组,链表中的元素在内存中并不是连续放置的链表添加或移除元素不需要移动其他元素数组可以直接访问任何一个位置的元素,链表必须从表头开始迭代到指定位置访问下面是单链表的基本结构长度为3的单链表每个元素由一个存储元素本身data的节点和一个指向下一个元素的引用next(也称指针或链接)组成尾节点的引用next指向为null类比:寻宝游戏,你有一条线索,这条线索是指向寻找下一条线索的地点的指针。你顺着这条链接去下一个地点,得到另一条指向再下一处的线索。得到列表中间的线索的唯一办法,就是从起点(第一条线索)顺着列表寻找二、链表的实现链表的实现不像之前介绍的栈和队列一般依赖于数组(至少我们目前是这样实现的),它必须自己构建类并组织逻辑实现。我们先创建一个Node类// 节点基类class Node { constructor(data) { this.data = data; this.next = null; }}一般单链表有以下几种方法:append 在链表尾部添加一个元素insert 在指定位置插入元素removeAt 在指定位置删除元素getNode 获取指定位置的元素print 打印整个链表indexOf 查找链表中是否有某个元素,有则返回索引,没有则返回-1getHead 获取链表头部getTail 获取链表尾部(有些并未实现尾部)size 返回链表包含的元素个数clear 清空链表// 初始化链表class LinkedList { constructor() { this._head = null; this._tail = null; this._length = 0; } // 方法…}下面我们来实现几个重要的方法2.1 append方法在链表尾部添加一个新的元素可分为两种情况:原链表中无元素,添加元素后,head和tail均指向新元素原链表中有元素,更新tail元素(如下)代码实现// 在链表尾端添加元素append(data) { const newNode = new Node(data); if (this._length === 0) { this._head = newNode; this._tail = newNode; } else { this._tail.next = newNode; this._tail = newNode; } this._length += 1; return true;}2.2 print方法为方便验证,我们先实现print方法。方法虽简单,这里却涉及到链表遍历精髓// 打印链表print() { let ret = []; // 遍历需从链表头部开始 let currNode = this._head; // 单链表最终指向null,作为结束标志 while (currNode) { ret.push(currNode.data); // 轮询至下一节点 currNode = currNode.next; } console.log(ret.join(’ –> ‘));}// 验证const link = new LinkedList();link.append(1);link.append(2);link.append(3);link.print(); // 1 –> 2 –> 32.3 getNode方法获取指定索引位置的节点,依次遍历链表,直到指定位置返回// 获取指定位置元素getNode(index) { let currNode = this._head; let currIndex = 0; while (currIndex < index) { currIndex += 1; currNode = currNode.next; } return currNode;}// 验证【衔接上面的链表实例】console.log(link.getNode(0));// Node { data: 1, next: Node { data: 2, next: Node { data: 3, next: null } } }console.log(link.getNode(3)); // null2.4 insert方法插入元素,需要考虑三种情况插入尾部,相当于append插入首部,替代_head并指向原有头部元素中间,需要断开原有链接并重新组合(如下)代码实现// 在链表指定位置插入元素insert(index, data) { // 不满足条件的索引值 if (index < 0 || index > this._length) return false; // 插入尾部 if (index === this._length) return this.append(data); const insertNode = new Node(data); if (index === 0) { // 插入首部 insertNode.next = this._head; this._head = insertNode; } else { // 找到原有位置节点 const prevTargetNode = this.getNode(index - 1); const targetNode = prevTargetNode.next; // 重塑节点连接 prevTargetNode.next = insertNode; insertNode.next = targetNode; } this._length += 1; return true;}// 验证link.insert(0, 0);link.insert(4, 4);link.insert(5, 5);link.print(); // 0 –> 1 –> 2 –> 3 –> 4 –> 52.5 removeAt方法在指定位置删除元素同添加元素类似首部:重新定义_head其他:找到目标位置的前后元素,重塑连接,如果目标位置为尾部,则重新定义_tail代码实现// 在链表指定位置移除元素removeAt(index) { if (index < 0 || index >= this._length) return false; if (index === 0) { this._head = this._head.next; } else { const prevNode = this.getNode(index - 1); const delNode = prevNode.next; const nextNode = delNode.next; // 若移除为最后一个元素 if (!nextNode) this._tail = prevNode; prevNode.next = nextNode; } this._length -= 1; return true;}// 验证link.removeAt(3);link.print(); // 0 –> 1 –> 2 –> 4 –> 52.6 其它方法完整的链表代码,可点此获取// 判断数据是否存在于链表内,存在返回index,否则返回-1indexOf(data) { let currNode = this._head; let index = 0; while (currNode) { if (currNode.data === data) return index; index += 1; currNode = currNode.next; } return -1;}getHead() { return this._head;}getTail() { return this._tail;}size() { return this._length;}isEmpty() { return !this._length;}clear() { this._head = null; this._tail = null; this._length = 0;}三、链表的应用3.1 基于链表实现的Stack和Queue基于链表实现栈class Stack { constructor() { this._link = new LinkedList(); } push(item) { this._link.append(item); } pop() { const tailIndex = this._link - 1; return this._link.removeAt(tailIndex); } peek() { if (this._link.size() === 0) return undefined; return this._link.getTail().data; } size() { return this._link.size(); } isEmpty() { return this._link.isEmpty(); } clear() { this._link.clear() }}基于链表实现队列class Queue { constructor() { this._link = new LinkedList(); } enqueue(item) { this._link.append(item); } dequeue() { return this._link.removeAt(0); } head() { if (this._link.size() === 0) return undefined; return this._link.getHead().data; } tail() { if (this._link.size() === 0) return undefined; return this._link.getTail().data; } size() { return this._link.size(); } isEmpty() { return this._link.isEmpty(); } clear() { this._link.clear() }}3.2 链表翻转【面试常考】(1)迭代法迭代法的核心就是currNode.next = prevNode,然后从头部一次向后轮询代码实现reverse() { if (!this._head) return false; let prevNode = null; let currNode = this._head; while (currNode) { // 记录下一节点并重塑连接 const nextNode = currNode.next; currNode.next = prevNode; // 轮询至下一节点 prevNode = currNode; currNode = nextNode; } // 交换首尾 let temp = this._tail; this._tail = this._head; this._head = temp; return true;}(2)递归法递归的本质就是执行到当前位置时,自己并不去解决,而是等下一阶段执行。直到递归终止条件,然后再依次向上执行function _reverseByRecusive(node) { if (!node) return null; if (!node.next) return node; // 递归终止条件 var reversedHead = _reverseByRecusive(node.next); node.next.next = node; node.next = null; return reversedHead;};_reverseByRecusive(this._head);3.3 链表逆向输出利用递归,反向输出function _reversePrint(node){ if(!node) return;// 递归终止条件 _reversePrint(node.next); console.log(node.data);};四、双向链表和循环链表4.1 双向链表双向链表和普通链表的区别在于,在链表中,一个节点只有链向下一个节点的链接,而在双向链表中,链接是双向的:一个链向下一个元素,另一个链向前一个元素,如下图正是因为这种变化,使得链表相邻节点之间不仅只有单向关系,可以通过prev来访问当前节点的上一节点。相应的,双向循环链表的基类也有变化class Node { constructor(data) { this.data = data; this.next = null; this.prev = null; }}继承单向链表后,最终的双向循环链表DoublyLinkedList如下【prev对应的更改为NEW】class DoublyLinkedList extends LinkedList { constructor() { super(); } append(data) { const newNode = new DoublyNode(data); if (this._length === 0) { this._head = newNode; this._tail = newNode; } else { newNode.prev = this._tail; // NEW this._tail.next = newNode; this._tail = newNode; } this._length += 1; return true; } insert(index, data) { if (index < 0 || index > this._length) return false; if (index === this._length) return this.append(data); const insertNode = new DoublyNode(data); if (index === 0) { insertNode.prev = null; // NEW this._head.prev = insertNode; // NEW insertNode.next = this._head; this._head = insertNode; } else { // 找到原有位置节点 const prevTargetNode = this.getNode(index - 1); const targetNode = prevTargetNode.next; // 重塑节点连接 prevTargetNode.next = insertNode; insertNode.next = targetNode; insertNode.prev = prevTargetNode; // NEW targetNode.prev = insertNode; // NEW } this._length += 1; return true; } removeAt(index) { if (index < 0 || index > this._length) return false; let delNode; if (index === 0) { delNode = this._head; this._head = this._head.next; this._head.prev = null; // NEW } else { const prevNode = this.getNode(index - 1); delNode = prevNode.next; const nextNode = delNode.next; // 若移除为最后一个元素 if (!nextNode) { this._tail = prevNode; this._tail.next = null; // NEW } else { prevNode.next = nextNode; // NEW nextNode.prev = prevNode; // NEW } } this._length -= 1; return delNode.data; }}4.2 循环链表循环链表可以像链表一样只有单向引用,也可以像双向链表一样有双向引用。循环链表和链 表之间唯一的区别在于,单向循环链表最后一个节点指向下一个节点的指针tail.next不是引用null, 而是指向第一个节点head,双向循环链表的第一个节点指向上一节点的指针head.prev不是引用null,而是指向最后一个节点tail总结链表的实现较于栈和队列的实现复杂许多,同样的,链表的功能更加强大我们可以通过链表实现栈和队列,同样也可以通过链表来实现栈和队列的问题链表更像是数组一样的基础数据结构,同时也避免了数组操作中删除或插入元素对其他元素的影响上一篇:JS数据结构与算法_栈&队列 ...

January 22, 2019 · 4 min · jiezi

数据结构与算法 | 栈的实现及应用

原文链接:https://wangwei.one/posts/jav…前面,我们实现了两种常见的线性表 —— 顺序表 和 链表 ,本篇我们来介绍另外一种常用的线性表 —— 栈。栈定义线性表中的一种特殊数据结构,数据只能从固定的一端插入数据或删除数据,另一端是封死的。特点FILO(First In Last Out): 先进后出;栈满还存会“上溢”,栈空再取会“下溢”;“上溢”:在栈已经存满数据元素的情况下,如果继续向栈内存入数据,栈存储就会出错。“下溢”:在栈内为空的状态下,如果对栈继续进行取数据的操作,就会出错。分类顺序栈特点采用数组实现,数据在物理结构上保持连续性。代码实现package one.wangwei.algorithms.datastructures.stack.impl;import one.wangwei.algorithms.datastructures.stack.IStack;import java.util.Arrays;/** * 顺序栈 * * @param <T> * @author wangwei * @date 2018/05/04 /public class ArrayStack<T> implements IStack<T> { /* * 默认大小 / private static final int DEFAULT_SIZE = 10; /* * 数组 / private T[] array = (T[]) new Object[DEFAULT_SIZE]; /* * 大小 / private int size; /* * 入栈 * * @param value * @return / @Override public boolean push(T value) { if (size >= array.length) { grow(); } array[size] = value; size++; return false; } /* * 扩容50% / private void grow() { int growSize = size + (size << 1); array = Arrays.copyOf(array, growSize); } /* * 压缩50% / private void shrink() { int shrinkSize = size >> 1; array = Arrays.copyOf(array, shrinkSize); } /* * 出栈 * * @return / @Override public T pop() { if (size <= 0) { return null; } T element = array[–size]; array[size] = null; int shrinkSize = array.length >> 1; if (shrinkSize >= DEFAULT_SIZE && shrinkSize > size) { shrink(); } return element; } /* * 查看栈顶值 * * @return / @Override public T peek() { if (size <= 0) { return null; } return array[size - 1]; } /* * 删除元素 * * @param value * @return / @Override public boolean remove(T value) { if (size <= 0) { return false; } for (int i = 0; i < size; i++) { T t = array[i]; if (value == null && t == null) { return remove(i); } if (value != null && value.equals(t)) { return remove(i); } } return false; } /* * 移除 index 处的栈值 * * @param index * @return / private boolean remove(int index) { if (index < 0 || index >= size) { throw new ArrayIndexOutOfBoundsException(“Index: " + index + “, Size: " + size); } if (index != –size) { System.arraycopy(array, index + 1, array, index, size - index); } array[size] = null; int shrinkSize = array.length >> 1; if (shrinkSize >= DEFAULT_SIZE && shrinkSize > size) { shrink(); } return true; } /* * 清空栈 / @Override public void clear() { if (size <= 0) { return; } for (int i = 0; i < size; i++) { array[i] = null; } size = 0; array = null; } /* * 是否包含元素 * * @param value * @return / @Override public boolean contains(T value) { if (size <= 0) { return false; } for (int i = 0; i < size; i++) { T t = array[i]; if (value == null && t == null) { return true; } if (value != null && value.equals(t)) { return true; } } return false; } /* * 栈大小 * * @return / @Override public int size() { return size; }}源码复杂度空间复杂度出栈和入栈的操作,只涉及一两个临时变量的存储空间,所以复杂度为O(1).时间复杂度顺序栈在出栈和入栈的操作时,最好情况时间复杂度为O(1),当需要扩容或者缩减时,需要迁移数据,此时为最坏复杂度,为O(n). 根据摊还分析法则,它们的均摊时间复杂度还是为O(1).链表栈特点用线性表的链式结构存储,数据在物理结构上非连续代码实现package one.wangwei.algorithms.datastructures.stack.impl;import one.wangwei.algorithms.datastructures.stack.IStack;/* * 链表栈 * * @param <T> * @author wangwei * @date 2018/05/04 /public class LinkedStack<T> implements IStack<T> { private Node<T> top; private int size; public LinkedStack() { this.top = null; this.size = 0; } /* * 入栈 * * @param value * @return / @Override public boolean push(T value) { Node<T> newTop = new Node<>(value); if (top == null) { top = newTop; } else { Node<T> oldTop = top; top = newTop; oldTop.above = top; top.below = oldTop; } size++; return true; } /* * 出栈 * * @return / @Override public T pop() { if (top == null) { return null; } Node<T> needTop = top; top = needTop.below; if (top != null) { top.above = null; } T needValue = needTop.element; needTop = null; size–; return needValue; } /* * 查看栈顶值 * * @return / @Override public T peek() { return top == null ? null : top.element; } /* * 删除元素 * * @param value * @return / @Override public boolean remove(T value) { if (top == null) { return false; } Node<T> x = top; if (value == null) { while (x != null && x.element != null) { x = x.below; } } else { while (x != null && !value.equals(x.element)) { x = x.below; } } return remove(x); } /* * 删除一个节点 * * @param node * @return / private boolean remove(Node<T> node) { if (node == null) { return false; } Node<T> above = node.above; Node<T> below = node.below; // 删除中间元素 if (above != null && below != null) { above.below = below; below.above = above; } // 删除top元素 else if (above == null && below != null) { top = below; top.above = null; } else if (above != null && below == null) { above.below = null; below = null; } else { top = null; } node = null; size–; return true; } /* * 清空栈 / @Override public void clear() { if (top == null) { return; } for (Node<T> x = top; x != null; ) { Node<T> below = x.below; x.element = null; x.above = null; x.below = null; x = below; } top = null; size = 0; } /* * 是否包含元素 * * @param value * @return / @Override public boolean contains(T value) { if (value == null) { for (Node<T> x = top; x != null; x = x.below) { if (x.element == null) { return true; } } } else { for (Node<T> x = top; x != null; x = x.below) { if (x.element.equals(value)) { return true; } } } return false; } /* * 栈大小 * * @return / @Override public int size() { return size; } /* * 节点 * * @param <T> */ private static class Node<T> { private T element; private Node<T> above; private Node<T> below; public Node(T element) { this.element = element; } }}源码复杂度空间复杂度出栈和入栈的操作,只涉及一两个临时变量的存储空间,所以复杂度为O(1).时间复杂度出栈和入栈的操作,不涉及数据搬迁,只是顶部元素操作,时间复杂度均为O(1).栈的应用接下来,我们看看栈在软件工程中的实际应用。函数调用操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构, 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。示例:int main() { int a = 1; int ret = 0; int res = 0; ret = add(3, 5); res = a + ret; printf("%d”, res); reuturn 0;}int add(int x, int y) { int sum = 0; sum = x + y; return sum;}main() 函数调用了 add() 函数,获取计算结果,并且与临时变量 a 相加,最后打印 res 的值。这个过程的中函数栈里的出栈、入栈操作,如下所示:思考:为什么函数要用栈来保存临时变量呢?用其他数据结构不行吗?函数调用的局部状态之所以用栈来记录是因为这些状态数据的存活时间满足“后入先出”(LIFO)顺序,而栈的基本操作正好就是支持这种顺序的访问。栈是程序设计中的一种经典数据结构,每个程序都拥有自己的程序栈。栈帧也叫过程活动记录,是编译器用来实现函数调用过程的一种数据结构。C语言中,每个栈帧对应着一个未运行完的函数。从逻辑上讲,栈帧就是一个函数执行的环境:函数调用框架、函数参数、函数的局部变量、函数执行完后返回到哪里等等。栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp(base pointer)指向当前的栈帧的底部(高地址),可称为“帧指针”或“基址指针”;寄存器esp(stack pointer)指向当前的栈帧的顶部(低地址),可称为“ 栈指针”。在C和C++语言中,临时变量分配在栈中,临时变量拥有函数级的生命周期,即“在当前函数中有效,在函数外无效”。这种现象就是函数调用过程中的参数压栈,堆栈平衡所带来的。堆栈平衡是指函数调完成后,要返还所有使用过的栈空间。函数调用其实可以看做4个过程:压栈: 函数参数压栈,返回地址压栈跳转: 跳转到函数所在代码处执行执行: 执行函数代码返回: 平衡堆栈,找出之前的返回地址,跳转回之前的调用点之后,完成函数调用表达式求值以 3 + 5 x 8 - 6 为这个表达式为例,编译器是如何利用栈来实现表达式求值的呢?编译器会使用两个栈来实现,一个栈用来保存操作数,另一个栈用来保存运算符。从左向右遍历表达式,遇到数字直接压入操作数栈,遇到操作符,就与运算符栈顶元素进行比较。如果比运算符栈顶元素的优先级高,就将当前运算符压入栈;如果比运算符栈顶元素的优先级低或者相同,从运算符栈中取栈顶运算符,从操作数栈的栈顶取 2 个操作数,然后进行计算,再把计算完的结果压入操作数栈,继续比较。如图所示:此前,我们在讲 比特币脚本语言 时,提到过 逆波兰表示法 ,也是运用了栈这种数据结构特征。括号匹配栈还可以用来检测表达式中的括号是否匹配。我们假设表达式中只包含三种括号,圆括号 ()、方括号 [] 和花括号{},并且它们可以任意嵌套。比如,{[{}]}或 [{()}([])] 等都为合法格式,而{[}()] 或 [({)] 为不合法的格式。那我现在给你一个包含三种括号的表达式字符串,如何检查它是否合法呢?我们用栈来保存未匹配的左括号,从左到右依次扫描字符串。当扫描到左括号时,则将其压入栈中;当扫描到右括号时,从栈顶取出一个左括号。如果能够匹配,比如“(”跟“)”匹配,“[”跟“]”匹配,“{”跟“}”匹配,则继续扫描剩下的字符串。如果扫描的过程中,遇到不能配对的右括号,或者栈中没有数据,则说明为非法格式。当所有的括号都扫描完成之后,如果栈为空,则说明字符串为合法格式;否则,说明有未匹配的左括号,为非法格式。浏览器的前进、后退功能使用两个栈,X 和 Y,把首次浏览的页面依次压入栈 X,当点击后退按钮时,再依次从栈 X 中出栈,并将出栈的数据依次放入栈 Y。当我们点击前进按钮时,我们依次从栈 Y 中取出数据,放入栈 X 中。当栈 X 中没有数据时,那就说明没有页面可以继续后退浏览了。当栈 Y 中没有数据,那就说明没有页面可以点击前进按钮浏览了。比如你顺序查看了 a,b,c 三个页面,我们就依次把 a,b,c 压入栈,这个时候,两个栈的数据就是这个样子:当你通过浏览器的后退按钮,从页面 c 后退到页面 a 之后,我们就依次把 c 和 b 从栈 X 中弹出,并且依次放入到栈 Y。这个时候,两个栈的数据就是这个样子:这个时候你又想看页面 b,于是你又点击前进按钮回到 b 页面,我们就把 b 再从栈 Y 中出栈,放入栈 X 中。此时两个栈的数据是这个样子:这个时候,你通过页面 b 又跳转到新的页面 d 了,页面 c 就无法再通过前进、后退按钮重复查看了,所以需要清空栈 Y。此时两个栈的数据这个样子:当然,我们还可以使用双向链表来实现这个功能。区别我们都知道,JVM 内存管理中有个“堆栈”的概念。栈内存用来存储局部变量和方法调用,堆内存用来存储 Java 中的对象。那 JVM 里面的“栈”跟本篇说的“栈”是不是一回事呢?如果不是,那它为什么又叫作“栈”呢?本篇介绍的栈是一种抽象的数据结构,而JVM中的"堆栈"是一种实际存在的物理结构,有关JVM堆栈的了解,可以看我之前的文章:https://wangwei.one/posts/jav…参考资料《数据结构与算法之美》https://time.geekbang.org/col…http://data.biancheng.net/vie...https://www.jianshu.com/p/594...https://www.cnblogs.com/33deb… ...

January 14, 2019 · 6 min · jiezi

写给互联网冬天里程序员看的数据压缩

原文地址:http://blog.52sox.com/data-co…网上戏说2018是互联网的冬天,先有阿里、腾讯、华为不扩招进行人员优化,后有美团、知乎裁员,好生热闹。不得不说,中国的经济进入1个新的阶段,用官方的话就是新生态,用百姓的话说就是,我们告别了粗旷的放养,迎来了精耕细作的时代。 当我们在为互联网冬天感到担惊受怕的时候,前方传来董小姐厂加薪的捷报。然而这次又是别人家的公司。 听你说了大半天的话,就是为了跟我说这些伤心的事情?如果内里是这样想的,那么你可以关闭你网页上的标签页了。适合读者人群下面要说的内容,适合以下人群:小白程序员,无论编程语言准备年后跳槽的程序员数据开发工程师数据算法工程师在校相关专业(数分、应用数学)大学生喜欢装X的某些人作为已经入坑2家的数据人,只能从自身的经验和发展路线来讲述数据压缩。下面我们要准备开始上车了。引言在过去10多年里,我们见证通信方式的变革,而且这一过程仍在持续。这一变革既包括因特网规模持续不断的增长(从2M到100M带宽的飞跃),也包括移动通信的爆炸式发展(从2G到4G,以及即将商用的5G),还体现在视频通信重要性的不断增加。而在这个革命的所有这些领域中,数据压缩都是基础支撑技术之一。 在2013年,大数据概念的提出,Hadoop打响了第一炮。而在2016年,我们迎来大数据的元年。而2017年,迎来了人工智能AI元年。技术和工具的不断推陈出新,我们也在成长中老去。那么,你可能会提出这样的问题:为什么需要数据压缩?了解数据压缩有什么好处?我工作都不涉及这部分,跟我讲这个,你不是在浪费我的时间?为了回答这样问题,我们有必要先说下什么是数据压缩再回答这些问题也不迟。什么是数据压缩简而言之,数据压缩就是以紧凑方式表示信息的技术或科学。人们识别出数据中存在的结构特征,并加以利用,生成这些紧凑表示方式。 说完了数据压缩的概念,我们将注意力回到之前的问题上,分别进行解释。为什么需要数据压缩之所以需要数据压缩,是因为人们以数字形式生成和利用的信息越来越多。在大数据时代里,我们可以说,当前我们被大量的各种信息所包围。比如在百度中输入1个智能手环的搜索,然后你会在隔天发现各种智能手环的广告。当然,这些精准的广告营销并没有达到我们的期望,至少现在看来还不够智能。 而随着需要传输、存储的数据呈爆炸式增长,人们在致力于开发更好的传输与存储技术方面取得很大的进步,但是取得的成果还不够。相关技术也层次不穷,比如大数据处理中的Hadoop工具,关于Hadoop及其生态圈,可以参见。 根据帕金森第一法则的推论,对大容量存储与传输能力需求的增长,至少是存储与传输能力增长速度的2倍。而在有些情况下,存储与传输能力并没有显著的提高,比如通过无线电波传送的信息量会受到大气特性的限制。 而通过数据压缩,我们可以提高其传输能力,降低存储的空间。了解数据压缩有什么好处说了大白天的数据压缩的问题,你还没有告诉我们了解或学习数据压缩到底有什么用。 学习数据压缩并不一定能让你拿高薪,也不一定能让你找到新的岗位,但是可以让你具备一定的数据思维的基础。至少,在实践中会懂得应用,并选择合适的方案。 切记,数据压缩只是基础,所谓基础就是没有它,你的上层建筑是没法构建起来的。对持有浪费时间观念的辩诉这个时候,有人会说,你真是浪费我的时间。我的工作又不涉及这部分内容,跟我唠叨这东西干嘛? 如果你是这样的观点,那么你不妨问下你自己,你平时是不是不听歌、看电影?而这些都与JPEG、MP3、H.264标准息息相关。数据压缩真的有那么重要而学习数据压缩,只是1种扩展你技术的1种选择,比如选择合适的方案解决工程问题。当然,如果你已经厉害到可以设计出合理的数据压缩方案,你也可以选择自助创业。 在互联网冬天,有1家科技公司近期完成了数千万的A轮融资。如果你对图像处理领域有所了解的话,就会发现实际上就是AI+DSP的模式,并不算多大的事情。 当然这也是别人家公司的事情,该干嘛干嘛。 说了这么多,如果你觉得没有什么看点,那么可以直接关闭标签页了。如果没有,我们就继续把。数据压缩的方式在数据压缩的早期,1个典型的例子就是是摩尔斯电码的使用,它通过对电报发送的字符以点和划来编码。通过统计发现,某些字符的出现频率要高于其他字符,于是为出现频率较高的字符分配较短的序列,从而减少发送1条消息所需要的平均时间。其中,霍夫曼(Huffman)编码就是利用思想。 我们可以通过许多不同类型的结构来实现压缩,而不仅仅局限于利用统计其结构来进行压缩。 在不同类型的数据中,存在许多其他类型的结构可以为压缩技术所用。例如,在语音中,我们在说话时,喉部的物理构造决定了所能发出的声音。换句话说,产生语音的力学特征使语音具有了某种结构。因此,我们可以不直接传送语音本身,而是传送喉部构造的相关信息,而接收器利用这些信息来合成语音。这样,我们只需要很少的数据就能表示足够的喉部结构信息,从而实现压缩。而这就是压缩方法早期版本中的声码器。 除了对数据的结构的利用外,我们还可以对数据的一些特性入手。比如,在许多时候,比如传送或存储语音和图像时,这些数据最终是由人来体验的,而人的感知能力是有限的。对于这些数据中表示的一些内容无法被用户感知,那么就可以将没有必要保留这些信息。我们通过利用人类的知觉局限,通过丢弃一些不相干的信息来实现压缩。 在开始研究数据压缩技术之前,我们先对这一领域作一整体概述。什么是压缩技术对于数据压缩而言,我们需要先设计出对应的算法及方案。 压缩技术或压缩算法实际上指的是如下的2种算法组成的:压缩算法,取得输入X,生成1种需要较少二进位的表示Xc重构算法,对压缩后的表示Xc执行操作,生成重构结果y这些操作的示意图如下图所示: 我们沿循惯例,将压缩算法和重构算法结合在一起,称为压缩算法。 而根据重构需求,我们可以将数据压缩划分为2大类:无损压缩,重构结果y和X相同有损压缩,实现的压缩比通常高于无损压缩,但重构结果y可能与X不同无损压缩在无损压缩技术中,不允许存在信息的损失,该技术通常用于一些不允许原数据与重构数据之间存在任何差别的应用中,其中以文本压缩作为代表。 对于文本压缩而言,如果压缩后的文本与原文不一致,可能会造成语义的谬以千里。有损压缩有损压缩技术会造成一些信息损失,采用该技术压缩后的数据通常不能再准确还原或重构。但是,如果可以接受重构结果中存在的这种失真,那么它所实现的压缩比通常要比无损压缩高的多。 而在设计出1种数据压缩方案之后,我们还需要能够测量它的性能。而数据压缩的应用领域多种多样,所以用于描述和测量压缩性能的术语也有所不同。性能的测量我们可以用多种方式来评估一种压缩算法,我们可以从如下一些方面进行测量:算法的相对复杂度算法在给定计算机上的运行速度压缩量重构结果与原数据之间的类似程度常用的指标有:压缩比速率建模与编码而对重构结果的要求可能直接决定了应当采用有损还是无损压缩方案,但具体使用哪1种压缩方案,则可能需要许多不同因素来决定。其中最重要的一些因素就是待压缩数据的特性。比如,1种能够有效压缩文本的技术,不一定能同样有效地压缩图像。 而要针对特定数据开发数据压缩算法,我们可以分为2个阶段来进行:建模编码在第1阶段,我们通常称之为建模,我们尝试了解数据中存在的冗余情况,并用1个模型来描述这种冗余。而第2阶段,我们通过编码的方式来描述这个模型,描述数据与模型之间的差别。而数据与模型之间的差别通常称为残差。 比如在前面的摩尔斯电码中,我们先通过统计的方式了解到某个字符的频率比其他的字符高,这就是1个建模的过程。而使用较短序列分配给较高频率的字符,就是1种编码方式。结语在数据压缩中,我们有许多不同方法来描述数据的特征,而不同的特征描述方式可以得到不同的压缩方案。 参考书籍:《Introduction to Data Compression,Fourth Edition》P1-8

January 12, 2019 · 1 min · jiezi

js算法-归并排序(merge_sort)

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。归并排序归并排序是一种非常稳定的排序方法,它的时间复杂度无论是平均,最好,最坏都是NlogN。归并排序的2个步骤先拆分,一直拆分到只有一个数拆分完成后,开始递归合并拆分过程从上图可以看出,归并排序会将一个数组进行两两拆分,一直拆分到只有一个数的时候停止拆分。那么拆分的代码就很简单了,就是得到一个指向中间的指针q,将数组拆分成(start,p)和(p,end)两个部分。p表示数组的开始下标r表示数组的结束下标 function divide(p, r){ return Math.floor( (p + r) / 2 ); }合并过程合并的过程就如上图所示遍历两组数据进行对比大小较小的那个值取出来放在第一个位置举个例子:假设现在数组A的数据是[2,5,1,3]经过我们的拆分后会是(0,2),(2,4);我们通过A.slice(0,2)和A.slice(2,4)=>得到两个数组A1[2,5]和A2[1,3]然后我们对数组[2,5,1,3]进行遍历,第一次会得到两边较小的数1,第二次2,第三次3,第四次是5 function merge(A, p, q, r){ const A1 = A.slice(p, q); const A2 = A.slice(q, r); // 哨兵,往A1和A2里push一个最大值,比如防止A1里的数都比较小,导致第三次遍历某个数组里没有值 A1.push(Number.MAX_SAFE_INTEGER); A2.push(Number.MAX_SAFE_INTEGER); // 循环做比较,每次取出较小的那个值 for (let i = p, j = 0, k = 0; i < r; i++) { if (A1[j] < A2[k]) { A[i] = A1[j]; j++; } else { A[i] = A2[k]; k++; } } }主程序主程序就是做递归重复上面的操作了 function merge_sort(A, p = 0, r) { r = r || A.length; if (r - p === 1) { return; } const q = divide(p, r); merge_sort(A, p, q); merge_sort(A, q, r); merge(A, p, q, r); return A; }完整代码 function divide(p, r) { return Math.floor((p + r) / 2); } function merge(A, p, q, r) { const A1 = A.slice(p, q); const A2 = A.slice(q, r); A1.push(Number.MAX_SAFE_INTEGER); A2.push(Number.MAX_SAFE_INTEGER); for (let i = p, j = 0, k = 0; i < r; i++) { if (A1[j] < A2[k]) { A[i] = A1[j]; j++; } else { A[i] = A2[k]; k++; } } } function merge_sort(A, p = 0, r) { r = r || A.length; if (r - p === 1) { return; } const q = divide(p, r); merge_sort(A, p, q); merge_sort(A, q, r); merge(A, p, q, r); return A; }推荐阅读数据结构系列:js数据结构-栈js数据结构-链表js数据结构-队列js数据结构-二叉树(二叉堆)js数据结构-二叉树(二叉搜索树)js数据结构-散列表(哈希表) ...

January 9, 2019 · 1 min · jiezi

js算法-快速排序(Quicksort)

快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),简称快排,一种排序算法,最早由东尼·霍尔提出。在平均状况下,排序n个项目要O(nLogn)次比较。在最坏状况下则需要O(n^2)次比较,但这种状况并不常见。事实上,快速排序O(nLogn)通常明显比其他算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地达成快速排序可能大家都学过,在面试中也经常会遇到,哪怕你是做前端的也需要会写,这里会列举两种不同的快排代码进行分析快速排序的3个基本步骤:从数组中选择一个元素作为基准点排序数组,所有比基准值小的元素摆放在左边,而大于基准值的摆放在右边。每次分割结束以后基准值会插入到中间去。最后利用递归,将摆放在左边的数组和右边的数组在进行一次上述的1和2操作。为了更深入的理解,可以看下面这张图我们根据上面这张图,来用文字描述一下选择左右边的元素为基准数,7将小于7的放在左边,大于7的放在右边,然后将基准数放到中间然后再重复操作从左边的数组选择一个基准点23比2大则放到基准树的右边右边的数组也是一样选择12作为基准数,15比12大所以放到了12的右边最后出来的结果就是从左到右 2 ,3,7,12,15了以上就是快速排序基本的一个实现思想。快速排序实现方式一这是我最近看到的一种快排代码var quickSort = function(arr) { if (arr.length <= 1) { return arr; } var pivotIndex = Math.floor(arr.length / 2); var pivot = arr.splice(pivotIndex, 1)[0]; var left = []; var right = []; for (var i = 0; i < arr.length; i++) { if (arr[i] < pivot) { left.push(arr[i]); } else { right.push(arr[i]); } } return quickSort(left).concat([pivot], quickSort(right));};以上代码的实现方式是,选择一个中间的数字为基准点,用两个数组分别去保存比基准数小的值,和比基准数大的值,最后递归左边的数组和右边的数组,用concat去做一个数组的合并。对于这段代码的分析:缺点:获取基准点使用了一个splice操作,在js中splice会对数组进行一次拷贝的操作,而它最坏的情况下复杂度为O(n),而O(n)代表着针对数组规模的大小进行了一次循环操作。首先我们每次执行都会使用到两个数组空间,产生空间复杂度。concat操作会对数组进行一次拷贝,而它的复杂度也会是O(n)对大量数据的排序来说相对会比较慢优点:代码简单明了,可读性强,易于理解非常适合用于面试笔试题那么我们接下来用另外一种方式去实现快速排序快速排序的实现方式二从上面这张图,我们用一个指针i去做了一个分割初始化i = -1循环数组,找到比支点小的数就将i向右移动一个位置,同时与下标i交换位置循环结束后,最后将支点与i+1位置的元素进行交换位置最后我们会得到一个由i指针作为分界点,分割成从下标0-i,和 i+1到最后一个元素。下面我们来看一下代码的实现,整个代码分成三部分,数组交换,拆分,qsort(主函数)三个部分先写最简单的数组交换吧,这个大家应该都懂 function swap(A, i ,j){ const t = A[i]; A[i] = A[j]; A[j] = t; }下面是拆分的过程,其实就是对指针进行移动,找到最后指针所指向的位置/** * * @param {} A 数组 * @param {} p 起始下标 * @param {} r 结束下标 + 1 / function dvide(A, p, r){ // 基准点 const pivot = A[r-1]; // i初始化是-1,也就是起始下标的前一个 let i = p - 1; // 循环 for(let j = p; j < r-1; j++){ // 如果比基准点小就i++,然后交换元素位置 if(A[j] < pivot){ i++; swap(A, i, j); } } // 最后将基准点插入到i+1的位置 swap(A, i+1, r-1); // 返回最终指针i的位置 return i+1; }主程序主要是通过递归去重复的调用进行拆分,一直拆分到只有一个数字。 /* * * @param {} A 数组 * @param {} p 起始下标 * @param {} r 结束下标 + 1 */ function qsort(A, p, r){ r = r || A.length; if(p < r - 1){ const q = divide(A, p, r); qsort(A, p, q); qsort(A, q + 1, r); } return A; }总结第二段的排序算法我们减少了两个O(n)的操作,得到了一定的性能上的提升,而第一种方法数据规模足够大的情况下会相对来说比较慢一些,快速排序在面试中也常常出现,为了笔试更好写一些可能会有更多的前端会选择第一种方式,但也会有一些为难人的面试官提出一些算法中的问题。而在实际的项目中,我觉得第一种方式可以少用。推荐本人最近写的关于数据结构系列如下,欢迎大家看看点个赞哈:js数据结构-栈js数据结构-链表js数据结构-队列js数据结构-二叉树(二叉堆)js数据结构-二叉树(二叉搜索树) ...

January 8, 2019 · 1 min · jiezi

js数据结构-二叉树(二叉搜索树)

前言可能有一部分人没有读过我上一篇写的二叉堆,所以这里把二叉树的基本概念复制过来了,如果读过的人可以忽略前面针对二叉树基本概念的介绍,另外如果对链表数据结构不清楚的最好先看一下本人之前写的js数据结构-链表二叉树二叉树(Binary Tree)是一种树形结构,它的特点是每个节点最多只有两个分支节点,一棵二叉树通常由根节点,分支节点,叶子节点组成。而每个分支节点也常常被称作为一棵子树。根节点:二叉树最顶层的节点分支节点:除了根节点以外且拥有叶子节点叶子节点:除了自身,没有其他子节点常用术语在二叉树中,我们常常还会用父节点和子节点来描述,比如图中2为6和3的父节点,反之6和3是2子节点二叉树的三个性质在二叉树的第i层上,至多有2^i-1个节点i=1时,只有一个根节点,2^(i-1) = 2^0 = 1深度为k的二叉树至多有2^k-1个节点i=2时,2^k-1 = 2^2 - 1 = 3个节点对任何一棵二叉树T,如果总结点数为n0,度为2(子树数目为2)的节点数为n2,则n0=n2+1树和二叉树的三个主要差别树的节点个数至少为1,而二叉树的节点个数可以为0树中节点的最大度数(节点数量)没有限制,而二叉树的节点的最大度数为2树的节点没有左右之分,而二叉树的节点有左右之分二叉树分类二叉树分为完全二叉树(complete binary tree)和满二叉树(full binary tree)满二叉树:一棵深度为k且有2^k - 1个节点的二叉树称为满二叉树完全二叉树:完全二叉树是指最后一层左边是满的,右边可能满也可能不满,然后其余层都是满的二叉树称为完全二叉树(满二叉树也是一种完全二叉树)二叉搜索树二叉搜索树满足以下的几个性质:若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;任意节点的左、右子树也需要满足左边小右边大的性质我们来举个例子来深入理解以下一组数据:12,4,18,1,8,16,20由下图可以看出,左边的图满足了二叉树的性质,它的每个左子节点都小于父节点,右子节点大于其父节点,同时左子树的节点都小于根节点,右子树的节点都大于根节点二叉搜索树主要的几个操作:查找(search)插入(insert)遍历(transverse)二叉树搜索树的链式存储结构通过下图,可以知道二叉搜索树的节点通常包含4个域,数据元素,分别指向其左,右节点的指针和一个指向父节点的指针所构成,一般把这种存储结构称为三叉链表。用代码初始化一个二叉搜索树的结点:一个指向父亲节点的指针 parent一个指向左节点的指针 left一个指向右节点的指针 right一个数据元素,里面可以是一个key和value class BinaryTreeNode { constructor(key, value){ this.parent = null; this.left = null; this.right = null; this.key = key; this.value = value; } }接着我们再用代码去初始化一个二叉搜索树在二叉搜索树中我们会维护一个root指针,这个就相当于链表中的head指针,在没有任何节点插入的时候它指向空,在有节点插入以后它指向根节点。 class BinarySearchTree { constructor() { this.root = null; } }创建节点 static createNode(key, value) { return new BinarySearchTree(key, value); }插入操作看下面这张图,13是我们要插入的节点,它插入的具体步骤:跟根节点12做比较,比12大,所以我们确定了,这个节点是往右子树插入的而根节点的右边已经有节点,那么跟这个节点18做比较,结果小于18所以往18的左节点找位置而18的左节点也已经有节点了,所以继续跟这个节点做比较,结果小于16刚好16的左节点是空的(left=null),所以13这个节点就插入到了16的左节点通过上面的描述,我们来看看代码是怎么写的定义两个指针,分别是p和tail,最初都指向root,p是用来指向要插入的位置的父节点的指针,而tail是用来查找插入位置的,所以最后它会指向null,用上图举个例子,p最后指向了6这个节点,而tail最后指向了null(tail为null则说明已经找到了要插入的位置)循环,tail根据我们上面分析的一步一步往下找位置插入,如果比当前节点小就往左找,大则往右找,一直到tail找到一个空位置也就是null如果当前的root为null,则说明当前结构中并没有节点,所以插入的第一个节点直接为跟节点,即this.root = node将插入后的节点的parent指针指向父节点 insert(node){ let p = this.root; let tail = this.root; // 循环遍历,去找到对应的位置 while(tail) { p = tail; // 要插入的节点key比当前节点小 if (node.key < tail.key){ tail.left = tail.left; } // 要插入的节点key比当前节点大 else { tail.right = tail.right; } } // 没有根节点,则直接作为根节点插入 if(!p) { this.root = node; return; } // p是最后一个节点,也就是我们要插入的位置的父节点 // 比父节点大则往右边插入 if(p.key < node.key){ p.right = node; } // 比父节点小则往左边插入 else { p.left = node; } // 指向父节点 node.parent = p; }查找查找就很简单了,其实和插入差多,都是去别叫左右节点的大小,然后往下找如果root = null, 则二叉树中没有任何节点,直接return,或者报个错什么的。循环查找 search(key) { let p = this.root; if(!p) { return; } while(p && p.key !== key){ if(p.key<key){ p = p.right; }else{ p = p.left; } } return p; }遍历中序遍历(inorder):先遍历左节点,再遍历自己,最后遍历右节点,输出的刚好是有序的列表前序遍历(preorder):先自己,再遍历左节点,最后遍历右节点后序遍历(postorder):先左节点,再右节点,最后自己最常用的一般是中序遍历,因为中序遍历可以得到一个已经排好序的列表,这也是为什么会用二叉搜索树排序的原因根据上面对中序遍历的解释,那么代码就变的很简单,就是一个递归的过程,递归停止的条件就是节点为null先遍历左节点–>yield* this._transverse(node.left)遍历自己 –> yield* node遍历左节点 –> yield* this._transverse(node.right) transverse() { return this._transverse(this.root); } _transverse(node){ if(!node){ return; } yield this._transverse(node.left); yield node; yield* this._transverse(node.right) }看上面这张图,我们简化的来看一下,先访问左节点4,再自己12,然后右节点18,这样输出的就刚好是一个12,4,8补充:这个地方用了generater,所以返回的一个迭代器。可以通过下面这种方式得到一个有序的数组,这里的前提就当是已经有插入的节点了 const tree = new BinaryTree(); //…中间省略插入过程 // 这样就返回了一个有序的数组 var arr = […tree.transverse()].map(item=>item.key);完整代码class BinaryTreeNode { constructor(key, value) { // 指向父节点 this.p = null; // 左节点 this.left = null; // 右节点 this.right = null; // 键 this.key = key; // 值 this.value = value; }}class BinaryTree { constructor() { this.root = null; } static createNode(key, value) { return new BinaryTreeNode(key, value); } search(key) { let p = this.root; if (!p) { return; } while (p && p.key !== key) { if (p.key < key) { p = p.right; } else { p = p.left; } } return p; } insert(node) { // 尾指针的父节点指针 let p = this.root; // 尾指针 let tail = this.root; while (tail) { p = tail; if (node.key < tail.key) { tail = tail.left; } else { tail = tail.right; } } if (!p) { this.root = node; return; } // 插入 if (p.key < node.key) { p.right = node; } else { p.left = node; } node.p = p; } transverse() { return this.__transverse(this.root); } __transverse(node) { if (!node) { return; } yield this.__transverse(node.left); yield node; yield* this.__transverse(node.right); }}总结二叉查找树就讲完了哈,其实这个和链表很像的,还是操作那么几个指针,既然叫查找树了,它主要还是用来左一些搜索,还有就是排序了,另外补充一下,二叉查找树里找最大值和最小值也很方便是不是,如果你大致读懂了的话。这篇文章我写的感觉有点乱诶,因为总感觉哪里介绍的不到位,让一些基础差的人会看不懂,如果有不懂或者文章哪里写错了,欢迎评论留言哈后续后续写什么呢,这个问题我也在想,排序算法,react第三方的一些模拟实现?,做个小程序组件库?还是别的,容我再想几个小时,因为可以,有建议的朋友们也可以留言说一下哈。最后最后,最重要的请给个赞,请粉一个呢,谢谢啦 ...

January 7, 2019 · 2 min · jiezi

C+数据结构与算法之链表(一)单链表的创建与查找

1.单链表定义每个节点包括一个数据域一个指针域,节点在内存中地址可以不连续,但是节点内部地址必然连续。2.结构定义typedef struct Lnode{int data;struct Lnode *next;}Lnode,*LinkList;3.单链表的创建1)头插法//头插法建立带头结点的单链表LinkList create1(LinkList &L){ Lnode s; int x; L=(LinkList) malloc( sizeof(Lnode));//创建头结点 L->next = NULL; //初始化 printf(“往链表中添加数据,99999结束\n”); scanf("%",&x); while(x!=99999){ s = (Lnode)malloc(sizeof(Lnode));//创建新节点 s->data = x; s->next = L->next; L->next = s; scanf("%d",&x); }return L;}2)尾插法//尾插法建立单链表LinkList create2(LinkList &L){ int x; L=(LinkList) malloc( sizeof(Lnode));//创建尾结点 Lnode *s,r = L;//S为插入节点指针,r为尾指针printf(“往链表中添加数据,99999结束\n”); //scanf("%",&x); while(x!=99999){ s = (Lnode)malloc(sizeof(Lnode));//创建新节点 scanf("%d",&x); s->data = x; r->next = s; r = s; //r指向新的表尾 } r->next = NULL;//尾节点指针置空 return L;}4.单链表的查找1)按值查找//按值查找表节点,返回节点位序,查找失败返回-1int locateElem(LinkList L, int e){ Lnode *P = L->next; int j=1; while (P!=NULL&&P->data!=e){ P = P->next; j++; } if(P->next == NULL && P->data == e) return j; else if(P->next != NULL && P->data == e) return j; else return -1;}//按值查找表节点,返回节点指针,这是方便链表运算实现Lnode *locateElem2(LinkList L, int e){ Lnode *P = L->next; while (P!=NULL&&P->data!=e){ P = P->next; } return P;//失败则返回空指针}2)按序号查找//按序号查找表节点,返回节点值int getElem(LinkList L,int i){ int j = 1; Lnode *P = L->next; if(i==0) return 0;//如果i=0,则返回头结点的值,但头结点不存值故返回0 if(i<0) return -1;//错误序号则返回-1 while(P&&j<i)//P非空 { P= P->next; j++; } return P->data;}//按序号查找表节点,返回节点指针,这是方便链表运算实现Lnode *getElem1(LinkList L, int i){ int j = 1; Lnode *P = L->next; if(i==0) return L;//如果i=0,则返回头结点 if(i<0) return NULL;//错误序号则返回NULL while(P&&j<i)//P非空 { P= P->next; j++; } return P;}5.完整代码及运行#include <stdio.h>#include <stdlib.h>//malloc函数头文件//单链表定义typedef struct Lnode{int data;struct Lnode *next;}Lnode,*LinkList;//头插法建立带头结点的单链表LinkList create1(LinkList &L){ Lnode s; int x; L=(LinkList) malloc( sizeof(Lnode));//创建头结点 L->next = NULL; //初始化 printf(“往链表中添加数据,99999结束\n”); scanf("%",&x); while(x!=99999){ s = (Lnode)malloc(sizeof(Lnode));//创建新节点 s->data = x; s->next = L->next; L->next = s; scanf("%d",&x); }return L;} //尾插法建立单链表LinkList create2(LinkList &L){ int x; L=(LinkList) malloc( sizeof(Lnode));//创建尾结点 Lnode *s,r = L;//S为插入节点指针,r为尾指针printf(“往链表中添加数据,99999结束\n”); //scanf("%",&x); while(x!=99999){ s = (Lnode)malloc(sizeof(Lnode));//创建新节点 scanf("%d",&x); s->data = x; r->next = s; r = s; //r指向新的表尾 } r->next = NULL;//尾节点指针置空 return L;}//按序号查找表节点,返回节点值int getElem(LinkList L,int i){ int j = 1; Lnode *P = L->next; if(i==0) return 0;//如果i=0,则返回头结点的值,但头结点不存值故返回0 if(i<0) return -1;//错误序号则返回-1 while(P&&j<i)//P非空 { P= P->next; j++; } return P->data;}//按序号查找表节点,返回节点指针,这是方便链表运算实现Lnode *getElem1(LinkList L, int i){ int j = 1; Lnode *P = L->next; if(i==0) return L;//如果i=0,则返回头结点 if(i<0) return NULL;//错误序号则返回NULL while(P&&j<i)//P非空 { P= P->next; j++; } return P;}//按值查找表节点,返回节点位序,查找失败返回-1int locateElem(LinkList L, int e){ Lnode *P = L->next; int j=1; while (P!=NULL&&P->data!=e){ P = P->next; j++; } if(P->next == NULL && P->data == e) return j; else if(P->next != NULL && P->data == e) return j; else return -1;}//按值查找表节点,返回节点指针,这是方便链表运算实现Lnode *locateElem2(LinkList L, int e){ Lnode *P = L->next; while (P!=NULL&&P->data!=e){ P = P->next; } return P;//失败则返回空指针}int main(){ LinkList L,L1; create1(L); LinkList temp = L->next; while(temp->next != NULL) { printf("%d “, temp->data); temp = temp->next; } printf(“头插法与存数据的顺序相反\n”); create2(L1); LinkList temp2 = L1->next; while(temp2 ->next != NULL) { printf("%d “, temp2->data); temp2 = temp2->next; } printf(“尾插法与存数据的顺序相同\n”); printf(“输入取值位序\n”); int i; scanf("%d”,&i); printf(“第%d位值为%d\n”,i, getElem(L1,i)); printf(“输入查找值\n”); int e; scanf("%d”,&e); printf(“值为:%d位序为:%d\n”,e, locateElem(L1,e));return 0;}6.小结单链表作为链表中最简单的一种,摆脱了顺序表对空间的束缚,可以很好的支持动态操作。但是其问题也很明显,每个节点只能找到其直接后继,而找不到其前一节点。在查找时需要从表头开始遍历表,会增加时间复杂度。 ...

January 7, 2019 · 2 min · jiezi

C+数据结构与算法之顺序表

1.定义线性表的顺序存储称为顺序表。元素间逻辑关系相邻,内存地址相邻;支持随机访问,可以通过下标访问表中每一个元素。2.结构定义#define MaxSize 50typedef struct {int data[MaxSize];int length;}Sqlist;3.相关算法1)顺序表创建void creat(Sqlist &L){int a;printf(“线性表元素总数:\n”);scanf("%d",&a);printf(“依次输入每个元素\n”);for(int i=0;i<a;i++){scanf("%d",&L.data[i]);L.length++;}}2)指定位置插入元素int insert(Sqlist &L,int i , int e){//在某个位置插入一个元素(成功返回1,失败返回0)if(i<1||i>L.length+1)//如果插入位置小于1或者大于表长+1,则位置不合法return 0;else if(i>=MaxSize)//如果位置大于等于最大表长,则不可插入return 0;else if (i== L.length+1){L.data[L.length] = e;L.length ++;return 1;}else {for(int j = L.length;j>=i;j–)//将插入位置后的所有元素后移{ L.data[j]=L.data[j-1];//这里需注意,数组下标是从0开始的 L.data[i-1] = e;} L.length++;//表长+1 return 1;}}3)删除指定位置元素int listdelete(Sqlist &L, int i , int &e){//成功返回1失败返回0if(i<1||i>L.length)//如果删除位置小于1或者大于表长,则位置不合法return 0;e = L.data[i-1];for(int j = i; j<L.length; j++)//删除位之后所有元素前移 L.data[j-1] = L.data[j];L.length–;//表长-1return 1;}4)按值查找int find(Sqlist L,int e){int i;for (i = 0; i < L.length; i++){ if(e==L.data[i]) { return i+1; } else { return 0; }}}4.完整代码#include <stdio.h>#include <stdlib.h>//顺序表定义#define MaxSize 50typedef struct {int data[MaxSize];int length;}Sqlist;//先声明函数void creat(Sqlist &L);//建立线性表void show(Sqlist L);//显示线性表//在指定位置插入数据int insert(Sqlist &L,int i , int e){//在某个位置插入一个元素(成功返回1,失败返回0)if(i<1||i>L.length+1)//如果插入位置小于1或者大于表长+1,则位置不合法return 0;else if(i>=MaxSize)//如果位置大于等于最大表长,则不可插入return 0;else if (i== L.length+1){L.data[L.length] = e;L.length ++;return 1;}else {for(int j = L.length;j>=i;j–)//将插入位置后的所有元素后移{ L.data[j]=L.data[j-1];//这里需注意,数组下标是从0开始的 L.data[i-1] = e;} L.length++;//表长+1 return 1;}}//删除指定位置元素int listdelete(Sqlist &L, int i , int &e){//成功返回1失败返回0if(i<1||i>L.length)//如果删除位置小于1或者大于表长,则位置不合法return 0;e = L.data[i-1];for(int j = i; j<L.length; j++)//删除位之后所有元素前移 L.data[j-1] = L.data[j];L.length–;//表长-1return 1;}//按值查找成功返回位序,失败返回0int find(Sqlist L,int e){int i;for (i = 0; i < L.length; i++){ if(e==L.data[i]) { return i+1; } else { return 0; }}}int main(){Sqlist L;L.length=0;//初始化线性表creat(L);//创建线性表/printf(“在顺序表哪个位置插入元素:\n”);int i,e;scanf("%d",&i);printf(“插入元素值:\n”);scanf("%d",&e);int j = insert(L,i,e);if(j == 1){printf(“插入成功\n”);show(L);//打印线性表元素}else{printf(“插入失败\n”);}printf(“删除第几个位置的数\n”);int h,k;scanf("%d",&h);int temp = listdelete(L, h , k);if(temp == 1){printf(“成功\n”);show(L);//打印线性表元素}else{printf(“失败\n”);}///按值查找int e,temp;printf(“想查找的值:\n”);scanf("%d",&e);temp = find(L,e);if (temp == 0){printf(“失败\n”);}else{ printf(“成功\n”); printf(“该元素位序为%d\n”,temp);}return 0;}//创建void creat(Sqlist &L){int a;printf(“线性表元素总数:\n”);scanf("%d",&a);printf(“依次输入每个元素\n”);for(int i=0;i<a;i++){scanf("%d",&L.data[i]);L.length++;}}//打印void show(Sqlist L){int i;printf(“当前元素:\n”);for(i=0;i<L.length;i++)printf("%d\t",L.data[i]);printf("\n");}5.小结顺序表是最基本及简单的一种数据结构,优点明显(随机访问,算法简单)但缺点很突出,不利于内存优化,不利于动态处理,对于一些对内存要求很高的算法以及动态处理很不适用。 ...

January 7, 2019 · 1 min · jiezi

【刷算法】LeetCode.236-二叉树的最近公共祖先

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。示例 1:输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1输出: 3解释: 节点 5 和节点 1 的最近公共祖先是节点 3。示例 2:输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4输出: 5解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。 说明:所有节点的值都是唯一的。p、q 为不同节点且均存在于给定的二叉树中。/** * Definition for a binary tree node. * function TreeNode(val) { * this.val = val; * this.left = this.right = null; * } //* * @param {TreeNode} root * @param {TreeNode} p * @param {TreeNode} q * @return {TreeNode} */var lowestCommonAncestor = function(r, a, b) { if(r === null || r === a || r === b) return r; let left = lowestCommonAncestor(r.left, a, b); let right = lowestCommonAncestor(r.right, a, b); if(left !== null && right !== null) return r; return left !== null ? left : right;}; ...

January 6, 2019 · 1 min · jiezi

js数据结构-二叉树(二叉堆)

二叉树二叉树(Binary Tree)是一种树形结构,它的特点是每个节点最多只有两个分支节点,一棵二叉树通常由根节点,分支节点,叶子节点组成。而每个分支节点也常常被称作为一棵子树。根节点:二叉树最顶层的节点分支节点:除了根节点以外且拥有叶子节点叶子节点:除了自身,没有其他子节点常用术语在二叉树中,我们常常还会用父节点和子节点来描述,比如图中2为6和3的父节点,反之6和3是2子节点二叉树的三个性质在二叉树的第i层上,至多有2^i-1个节点i=1时,只有一个根节点,2^(i-1) = 2^0 = 1深度为k的二叉树至多有2^k-1个节点i=2时,2^k-1 = 2^2 - 1 = 3个节点对任何一棵二叉树T,如果总结点数为n0,度为2(子树数目为2)的节点数为n2,则n0=n2+1树和二叉树的三个主要差别树的节点个数至少为1,而二叉树的节点个数可以为0树中节点的最大度数(节点数量)没有限制,而二叉树的节点的最大度数为2树的节点没有左右之分,而二叉树的节点有左右之分二叉树分类二叉树分为完全二叉树(complete binary tree)和满二叉树(full binary tree)满二叉树:一棵深度为k且有2^k - 1个节点的二叉树称为满二叉树完全二叉树:完全二叉树是指最后一层左边是满的,右边可能满也可能不满,然后其余层都是满的二叉树称为完全二叉树(满二叉树也是一种完全二叉树)二叉树的数组表示用一个数组来表示二叉树的结构,将一组数组从根节点开始从上到下,从左到右依次填入到一棵完全二叉树中,如下图所示通过上图我们可以分析得到数组表示的完全二叉树拥有以下几个性质:left = index * 2 + 1,例如:根节点的下标为0,则左节点的值为下标array[0*2+1]=1right = index * 2 + 2,例如:根节点的下标为0,则右节点的值为下标array[0*2+2]=2序数 >= floor(N/2)都是叶子节点,例如:floor(9/2) = 4,则从下标4开始的值都为叶子节点二叉堆二叉堆由一棵完全二叉树来表示其结构,用一个数组来表示,但一个二叉堆需要满足如下性质:二叉堆的父节点的键值总是大于或等于(小于或等于)任何一个子节点的键值当父节点的键值大于或等于(小于或等于)它的每一个子节点的键值时,称为最大堆(最小堆)从上图可以看出:左图:父节点总是大于或等于其子节点,所以满足了二叉堆的性质,右图:分支节点7作为2和12的父节点并没有满足其性质(大于或等于子节点)。二叉堆的主要操作insert:插入节点delete:删除节点max-hepify:调整分支节点堆性质rebuildHeap:重新构建整个二叉堆sort:排序初始化一个二叉堆从上面简单的介绍,我们可以知道,一个二叉堆的初始化非常的简单,它就是一个数组初始化一个数组结构保存数组长度 class Heap{ constructor(arr){ this.data = […arr]; this.size = this.data.length; } }max-heapify最大堆操作max-heapify是把每一个不满足最大堆性质的分支节点进行调整的一个操作。 如上图:调整分支节点2(分支节点2不满足最大堆的性质)默认该分支节点为最大值将2与左右分支比较,从2,12,5中找出最大值,然后和2交换位置根据上面所将的二叉堆性质,分别得到分支节点2的左节点和右节点比较三个节点,得到最大值的下标max如果该节点本身就是最大值,则停止操作将max节点与父节点进行交换重复step2的操作,从2,4,7中找出最大值与2做交换递归 maxHeapify(i) { let max = i; if(i >= this.size){ return; } // 当前序号的左节点 const l = i * 2 + 1; // 当前需要的右节点 const r = i * 2 + 2; // 求当前节点与其左右节点三者中的最大值 if(l < this.size && this.data[l] > this.data[max]){ max = l; } if(r < this.size && this.data[r] > this.data[max]){ max = r; } // 最终max节点是其本身,则已经满足最大堆性质,停止操作 if(max === i) { return; } // 父节点与最大值节点做交换 const t = this.data[i]; this.data[i] = this.data[max]; this.data[max] = t; // 递归向下继续执行 return this.maxHeapify(max); }重构堆我们可以看到,刚初始化的堆由数组表示,这个时候它可能并不满足一个最大堆或最小堆的性质,这个时候我们可能需要去将整个堆构建成我们想要的。上面我们做了max-heapify操作,而max-heapify只是将某一个分支节点进行调整,而要将整个堆构建成最大堆,则需要将所有的分支节点都进行一次max-heapify操作,如下图,我们需要依次对12,3,2,15这4个分支节点进行max-hepify操作具体步骤:找到所有分支节点:上面堆的性质提到过叶子节点的序号>=Math.floor(n/2),因此小于Math.floor(n/2)序号的都是我们需要调整的节点。例如途中所示数组为[15,2,3,12,5,2,8,4,7] => Math.floor(9/2)=4 => index小于4的分别是15,2,3,12(需要调整的节点),而5,2,8,4,7为叶子节点。将找到的节点都进行maxHeapify操作 rebuildHeap(){ // 叶子节点 const L = Math.floor(this.size / 2); for(let i = L - 1; i>=0; i–){ this,maxHeapify(i); } }最大堆排序最大堆的排序,如上图所示:交换首尾位置将最后个元素从堆中拿出,相当于堆的size-1然后在堆根节点进行一次max-heapify操作重复以上三个步骤,知道size=0 (这个边界条件我们在max-heapify函数里已经做了) sort() { for(let i = this.size - 1; i > 0; i–){ swap(this.data, 0, i); this.size–; this.maxHeapify(0); } }插入和删除这个的插入和删除就相对比较简单了,就是对一个数组进行插入和删除的操作往末尾插入堆长度+1判断插入后是否还是一个最大堆不是则进行重构堆 insert(key) { this.data[this.size] = key; this.size++ if (this.isHeap()) { return; } this.rebuildHeap(); }删除数组中的某个元素堆长度-1判断是否是一个堆不是则重构堆 delete(index) { if (index >= this.size) { return; } this.data.splice(index, 1); this.size–; if (this.isHeap()) { return; } this.rebuildHeap(); }完整代码/** * 最大堆 /function left(i) { return i * 2 + 1;}function right(i) { return i * 2 + 2;}function swap(A, i, j) { const t = A[i]; A[i] = A[j]; A[j] = t;}class Heap { constructor(arr) { this.data = […arr]; this.size = this.data.length; } /* * 重构堆 / rebuildHeap() { const L = Math.floor(this.size / 2); for (let i = L - 1; i >= 0; i–) { this.maxHeapify(i); } } isHeap() { const L = Math.floor(this.size / 2); for (let i = L - 1; i >= 0; i++) { const l = this.data[left(i)] || Number.MIN_SAFE_INTEGER; const r = this.data[right(i)] || Number.MIN_SAFE_INTEGER; const max = Math.max(this.data[i], l, r); if (max !== this.data[i]) { return false; } return true; } } sort() { for (let i = this.size - 1; i > 0; i–) { swap(this.data, 0, i); this.size–; this.maxHeapify(0); } } insert(key) { this.data[this.size++] = key; if (this.isHeap()) { return; } this.rebuildHeap(); } delete(index) { if (index >= this.size) { return; } this.data.splice(index, 1); this.size–; if (this.isHeap()) { return; } this.rebuildHeap(); } /* * 堆的其他地方都满足性质 * 唯独跟节点,重构堆性质 * @param {*} i */ maxHeapify(i) { let max = i; if (i >= this.size) { return; } // 求左右节点中较大的序号 const l = left(i); const r = right(i); if (l < this.size && this.data[l] > this.data[max]) { max = l; } if (r < this.size && this.data[r] > this.data[max]) { max = r; } // 如果当前节点最大,已经是最大堆 if (max === i) { return; } swap(this.data, i, max); // 递归向下继续执行 return this.maxHeapify(max); }}module.exports = Heap;总结堆讲到这里就结束了,堆在二叉树里相对会比较简单,常常被用来做排序和优先级队列等。堆中比较核心的还是max-heapify这个操作,以及堆的三个性质。后续下一篇应该会介绍二叉搜索树。欢迎大家指出文章的错误,如果有什么写作建议也可以提出。我会持续的去写关于前端的一些技术文章,如果大家喜欢的话可以关注一和点个赞,你的赞是我写作的动力。顺便再提一下,我在等第一个粉丝哈哈 ...

January 4, 2019 · 3 min · jiezi

js数据结构-散列表(哈希表)

散列表散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。我们从上图开始分析有一个集合U,里面分别是1000,10,152,9733,1555,997,1168右侧是一个10个插槽的列表(散列表),我们需要把集合U中的整数存放到这个列表中怎么存放,分别存在哪个槽里?这个问题就是需要通过一个散列函数来解决了。我的存放方式是取10的余数,我们对应这图来看1000%10=0,10%10=0 那么1000和10这两个整数就会被存储到编号为0的这个槽中152%10=2那么就存放到2的槽中9733%10=3 存放在编号为3的槽中通过上面简单的例子,应该会有一下几点一个大致的理解集合U,就是可能会出现在散列表中的键散列函数,就是你自己设计的一种如何将集合U中的键值通过某种计算存放到散列表中,如例子中的取余数散列表,存放着通过计算后的键那么我们在接着看一般我们会怎么去取值呢?比如我们存储一个key为1000,value为’张三’ —> {key:1000,value:‘张三’}从我们上述的解释,它是不是应该存放在1000%10的这个插槽里。当我们通过key想要找到value张三,是不是到key%10这个插槽里找就可以了呢?到了这里你可以停下来思考一下。散列的一些术语(可以简单的看一下)散列表中所有可能出现的键称作全集U用M表示槽的数量给定一个键,由散列函数计算它应该出现在哪个槽中,上面例子的散列函数h=k%M,散列函数h就是键k到槽的一个映射。1000和10都被存到了编号0的这个槽中,这种情况称之为碰撞。看到这里不知道你是否大致理解了散列函数是什么了没。通过例子,再通过你的思考,你可以回头在读一遍文章头部关于散列表的定义。如果你能读懂了,那么我估计你应该是懂了。常用的散列函数处理整数 h=>k%M (也就是我们上面所举的例子)处理字符串: function h_str(str,M){ return […str].reduce((hash,c)=>{ hash = (31*hash + c.charCodeAt(0)) % M },0) }hash算法不是这里的重点,我也没有很深入的去研究,这里主要还是去理解散列表是个怎样的数据结构,它有哪些优点,它具体做了怎样一件事。而hash函数它只是通过某种算法把key映射到列表中。构建散列表通过上面的解释,我们这里做一个简单的散列表散列表的组成M个槽有个hash函数有一个add方法去把键值添加到散列表中有一个delete方法去做删除有一个search方法,根据key去找到对应的值初始化- 初始化散列表有多少个槽- 用一个数组来创建M个槽 class HashTable { constructor(num=1000){ this.M = num; this.slots = new Array(num); } }散列函数处理字符串的散列函数,这里使用字符串是因为,数值也可以传换成字符串比较通用一些先将传递过来的key值转为字符串,为了能够严谨一些将字符串转换为数组, 例如’abc’ => […‘abc’] => [‘a’,‘b’,‘c’]分别对每个字符的charCodeAt进行计算,取M余数是为了刚好对应插槽的数量,你总共就10个槽,你的数值%10 肯定会落到 0-9的槽里 h(str){ str = str + ‘’; return […str].reduce((hash,c)=>{ hash = (331 * hash + c.charCodeAt()) % this.M; return hash; },0) }添加调用hash函数得到对应的存储地址(就是我们之间类比的槽)因为一个槽中可能会存多个值,所以需要用一个二维数组去表示,比如我们计算得来的槽的编号是0,也就是slot[0],那么我们应该往slot[0] [0]里存,后面进来的同样是编号为0的槽的话就接着往slot[0] [1]里存 add(key,value) { const h = this.h(key); // 判断这个槽是否是一个二维数组, 不是则创建二维数组 if(!this.slots[h]){ this.slots[h] = []; } // 将值添加到对应的槽中 this.slots[h].push(value); } 删除通过hash算法,找到所在的槽通过过滤来删除 delete(key){ const h = this.h(key); this.slots[h] = this.slots[h].filter(item=>item.key!==key); }查找通过hash算法找到对应的槽用find函数去找同一个key的值返回对应的值 search(key){ const h = this.h(key); const list = this.slots[h]; const data = list.find(x=> x.key === key); return data ? data.value : null; }总结讲到这里,散列表的数据结构已经讲完了,其实我们每学一种数据结构或算法的时候,不是去照搬实现的代码,我们要学到的是思想,比如说散列表它究竟做了什么,它是一种存储方式,可以快速的通过键去查找到对应的值。那么我们会思考,如果我们设计的槽少了,在同一个槽里存放了大量的数据,那么这个散列表它的搜索速度肯定是会大打折扣的,这种情况又应该用什么方式去解决,又或者是否用其他的数据结构的代替它。补充一个小知识点v8引擎中的数组 arr = [1,2,3,4,5] 或 new Array(100) 我们都知道它是开辟了一块连续的空间去存储,而arr = [] , arr[100000] = 10 这样的操作它是使用的散列,因为这种操作如果连续开辟100万个空间去存储一个值,那么显然是在浪费空间。后续后续可能会去介绍一下二叉树,另外对于文章有什么写错或者写的不好的地方大家都可以提出来。我会持续的去写关于前端的一些技术文章,如果大家喜欢的话可以关注一下,点个赞哦谢谢 ...

January 1, 2019 · 1 min · jiezi

数据结构与算法 | 线性表 —— 顺序表

原文链接:https://wangwei.one/posts/jav…线性表定义将具有线性关系的数据存储到计算机中所使用的存储结构称为线性表。线性,是指数据在逻辑结构上具有线性关系。<!–more–>分类逻辑结构上相邻的数据在物理结构存储分两种形式:数据在内存中集中存储,采用顺序表示结构,称为"顺序存储";数据在内存中分散存储,采用链式表示结构,称为"链式存储";顺序表定义逻辑上具有线性关系的数据按照前后的次序全部存储在一整块连续的内存空间中,之间不存在空隙,这样的存储结构称为顺序存储结构。使用线性表的顺序存储结构生成的表,称为顺序表。实现顺序表的存放数据的特点和数组一样,所以我们这里采用数组来实现,这里我们来用数组来简单实现Java中常用的ArrayList。接口定义:package one.wangwei.algorithms.datastructures.list;/** * List Interface * * @param <T> * @author https://wangwei.one/ * @date 2018/04/27 /public interface IList<T> { /* * add element * * @param element * @return / public boolean add(T element); /* * add element at index * * @param index * @param element * @return / public boolean add(int index, T element); /* * remove element * * @param element * @return / public boolean remove(T element); /* * remove element by index * * @param index * @return / public T remove(int index); /* * set element by index * * @param index * @param element * @return old element / public T set(int index, T element); /* * get element by index * * @param index * @return / public T get(int index); /* * clear list / public void clear(); /* * contain certain element * * @param element * @return / public boolean contains(T element); /* * get list size * * @return / public int size();}源代码接口实现:package one.wangwei.algorithms.datastructures.list.impl;import one.wangwei.algorithms.datastructures.list.IList;import java.util.Arrays;/* * Array List * * @param <T> * @author https://wangwei.one * @date 2018/04/27 /public class MyArrayList<T> implements IList<T> { /* * default array size / private final int DEFAULT_SIZE = 10; /* * array size / private int size = 0; /* * array / private T[] array = (T[]) new Object[DEFAULT_SIZE]; /* * add element * * @param element * @return / @Override public boolean add(T element) { return add(size, element); } /* * add element at index * * @param index * @param element * @return / @Override public boolean add(int index, T element) { if (index < 0 || index > size) { throw new ArrayIndexOutOfBoundsException(“Index: " + index + “, Size: " + size); } // need grow if (size >= array.length) { grow(); } // copy array element if (index < size) { System.arraycopy(array, index, array, index + 1, size - index); } array[index] = element; size++; return true; } /* * grow 50% / private void grow() { int growSize = size + (size >> 1); array = Arrays.copyOf(array, growSize); } /* * remove element * * @param element * @return / @Override public boolean remove(T element) { for (int i = 0; i < size; i++) { if (element == null) { if (array[i] == null) { remove(i); return true; } } else { if (array[i].equals(element)) { remove(i); return true; } } } return false; } /* * remove element by index * * @param index * @return / @Override public T remove(int index) { checkPositionIndex(index); T oldElement = array[index]; // need copy element if (index != (size - 1)) { System.arraycopy(array, index + 1, array, index, size - index - 1); } –size; array[size] = null; // shrink 25% int shrinkSize = size - (size >> 2); if (shrinkSize >= DEFAULT_SIZE && shrinkSize > size) { shrink(); } return oldElement; } /* * shrink 25% / private void shrink() { int shrinkSize = size - (size >> 2); array = Arrays.copyOf(array, shrinkSize); } /* * set element by index * * @param index * @param element * @return / @Override public T set(int index, T element) { checkPositionIndex(index); T oldElement = array[index]; array[index] = element; return oldElement; } /* * get element by index * * @param index * @return / @Override public T get(int index) { checkPositionIndex(index); return array[index]; } /* * check index * * @param index / private void checkPositionIndex(int index) { if (index < 0 || index >= size) { throw new IndexOutOfBoundsException(“Index: " + index + “, Size: " + size); } } /* * clear list / @Override public void clear() { for (int i = 0; i < size; i++) { array[i] = null; } size = 0; } /* * contain certain element * * @param element / @Override public boolean contains(T element) { for (int i = 0; i < size; i++) { if (element == null) { if (array[i] == null) { return true; } } else { if (array[i].equals(element)) { return true; } } } return false; } /* * get list size * * @return */ @Override public int size() { return size; }}源代码主要注意以下几点:添加元素时 ,判断是否需要对Array进行扩容;删除元素时,判断是否需要对Array进行收缩;remove与contains接口,注意element为null的情况;特点对数据进行遍历的时候,数据在连续的物理空间中进行存放,CPU的内部缓存结构会缓存连续的内存片段,可以大幅降低读取内存的性能开销,所以查询比较快;删除线性表中的元素的时候,后面的元素会整体向前移动,所以删除的效率较低,插入类似,时间复杂度为O(n);参考资料http://data.biancheng.net/vie…https://time.geekbang.org/col...https://github.com/phishman35… ...

December 29, 2018 · 4 min · jiezi

Javascript数据结构和算法(分享)

分享一下自己使用JS实现的数据结构和算法案例还在更新中…, 运行环境Node.js地址https://github.com/clm960227/…目录1.栈栈实现 https://github.com/clm960227/…栈实现十进制转二进制 https://github.com/clm960227/...2. 队列队列实现 https://github.com/clm960227/…优先队列 https://github.com/clm960227/…循环队列 https://github.com/clm960227/...3. 链表单链表 https://github.com/clm960227/…双链表 https://github.com/clm960227/…循环链表 https://github.com/clm960227/...4. 集合集合实现 https://github.com/clm960227/...5. 字典字典实现 https://github.com/clm960227/...6. 树BST https://github.com/clm960227/...AST https://github.com/clm960227/...7. 图图实现 https://github.com/clm960227/...DFS https://github.com/clm960227/...BFS https://github.com/clm960227/...8. 最短路径Dijkstra https://github.com/clm960227/…

December 29, 2018 · 1 min · jiezi

1.两数之和

题目:给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。示例:给定 nums = [2, 7, 11, 15], target = 9因为 nums[0] + nums[1] = 2 + 7 = 9所以返回 [0, 1]const nums = [1,2,1,3,6,3]const towSum1 = (nums, target) => { let j; for(let i = 0, l = nums.length - 1; i < l; i++){ j = nums.indexOf(target-nums[i], i+1) if (j !== -1) { return [i, j] } }}console.time(‘1’)const re1 = towSum1(nums, 6);console.timeEnd(‘1’)console.log(re1)const twoSum2 = (nums, target) => { for(let i = 0, l = nums.length-1; i < l; i++) { for(let j = i+1; j < l+1;j++){ if(nums[i] + nums[j] === target) { return [i ,j] } } }};console.time(‘2’)const re2 = twoSum2(nums, 6)console.timeEnd(‘2’)console.log(re2)在leetcode上测试方法2比方法1要快,本地node测试基本方法1用的时间的时间是方法2的两倍. ...

December 22, 2018 · 1 min · jiezi

【刷算法】LeetCode.198-打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。示例 1:输入: [1,2,3,1]输出: 4解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。示例 2:输入: [2,7,9,3,1]输出: 12解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。 偷窃到的最高金额 = 2 + 9 + 1 = 12 。/** * @param {number[]} nums * @return {number} */var rob = function(nums) { if(nums.length === 0) return 0; if(nums.length === 1) return nums[0]; let prepre = nums[0], pre = Math.max(nums[0], nums[1]); for(let i = 2;i < nums.length;i++) { let temp = pre; pre = Math.max(prepre + nums[i], pre); prepre = temp; } return pre;}; ...

December 18, 2018 · 1 min · jiezi

【刷算法】LeetCode.53-最大子序和

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。示例:输入: [-2,1,-3,4,-1,2,1,-5,4],输出: 6解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。/** * @param {number[]} nums * @return {number} */var maxSubArray = function(nums) { if(nums.length === 0) return null; if(nums.length === 1) return nums[0]; let cur = 0, max = -Infinity; for(let i = 0;i < nums.length;i++) { cur += nums[i]; if(cur > max) max = cur; if(cur < 0) { cur = 0; } } return max;};

December 17, 2018 · 1 min · jiezi

坐下,这些都是二叉树的基本操作!

本篇为复习过程中遇到过的总结,同时也给准备面试的同学一份参考。另外,由于篇幅有限,本篇的重点在于二叉树的常见算法以及实现。常见的二叉树实现代码之前写过相关的文章,是关于如何创建及遍历二叉树的,这里不再赘述。提供链接给各位感兴趣的小伙伴,点此跳转翻转二叉树对于一棵二叉树,翻转它的左右子树,如下图所示: 下面来分析具体的实现思路:对于根结点为空的情况这种情况需要排除,因为null不是一个对象,不可能存在左右子树并且可以翻转的情况对于一棵只有一个根结点的二叉树emmm,这种情况也可以翻转,因为此时根结点左右子树为null,交换左右子树其实也就是在交换两个null,理论上是翻转了,但实际上我们看到的和没有翻转之前的结果是一样的对于一棵具有两个或两个以上结点的二叉树,此时二叉树可以表示为如下的图像:可以看出,无论是只有左子树还是只有右子树都可以进行翻转。这句话等价于,为空的子树可以和不为空的子树进行交换,也就是不对为空的子树进行特殊处理分析过程其实这样我们还是不知道二叉树是如何翻转的,我们可以用第一张图的二叉树为例子,看一下翻转的具体过程。首先我们需要对根结点进行判空处理,在根结点不为空的情况下存在左右子树(即使左右子树为空),然后交换左右子树;把根结点的左子树当成左子树的根结点,对当前根结点进行判空处理,不为空时交换左右子树;把根结的右子树当成右子树的根结点,对当前根结点进行判空处理,不为空时交换左右子树;重复步骤2、3,最后二叉树变为原来的镜像结构,结果可以参考文章第一张示意图。示例代码根据上面的推理过程我们可以得出如下的代码:function reverseTree(root){ if( root !== null){ [root.left, root.right] = [root.right, root.left] reverseTree(root.left) reverseTree(root.right) }}虽然推理过程比较复杂(也可能是写的比较啰嗦。。),但是仔细观察代码,这和遍历的代码似乎也没多大差别,只是把输出结点变为了交换结点。判断二叉树是否完全对称一棵左右完全对称的二叉树是这样的:那到底如何判断呢?根结点为空时,此时为一棵空二叉树,满足对称条件(-_-||)只有一个根结点时,左右子树都为null,满足左右对称条件只有两个结点时,此时左右子树必定有一个为空,不可能存在对称的情况结点数在三个及三个以上时,二叉树有对称的可能。按照我们正常的思维,看对称与否,首先看左边,然后看右边,最后比较左右是否相等。同时我们注意到,在二叉树深度比较大的时候,我们光是比较左右是不够的。可以观察到,我们比较完左右以后还需要比较左的左和右的右,比较左的右和右的左分析过程这么看是比较绕,接下来我们来看图分析:先比较根结点左右孩子将左子树根结点的左孩子与右子树根结点的右孩子进行比较将左子树根结点的右孩子与右子树根结点的左孩子进行比较重复以上过程…示例代码function isSymmetrical(pRoot){ // write code here if(!pRoot){ return true } return funC(pRoot.left, pRoot.right)} function funC(left, right){ if(!left){ return right === null } if(!right){ return false } if(left.val !== right.val){ return false } return funC(left.right, right.left) && funC(left.left, right.right)}求二叉树的深度分析过程只有一个根结点时,二叉树深度为1只有左子树时,二叉树深度为左子树深度加1只有右子树时,二叉树深度为右子树深度加1同时存在左右子树时,二叉树深度为左右子树中深度最大者加1示例代码function deep(root){ if(!root){ return 0 } let left = deep(root.left) let right = deep(root.right) return left > right ? left + 1 : right + 1}求二叉树的宽度二叉树的宽度是啥?我把它理解为具有最多结点数的层中包含的结点数,比如下图所示的二叉树,其实它的宽度就是为4:分析过程根据上图,我们如何算出二叉树的宽度呢?其实有个很简单的思路:算出第一层的结点数,保存算出第二层的结点数,保存一二层中较大的结点数重复以上过程示例代码根据分析过程,我们可以利用队列这种数据结构来实现这个算法,代码如下:function width(root){ if(!root){ return 0 } let queue = [root], max = 1, deep = 1 while(queue.length){ while(deep–){ let temp = queue.shift() if(temp.left){ queue.push(temp.left) } if(temp.right){ queue.push(temp.right) } } deep = queue.length max = max > deep ? max : deep } return max}重建二叉树常见的遍历前序遍历:前序遍历首先访问根结点然后遍历左子树,最后遍历右子树。中序遍历:中序遍历首先访问左子树然后遍历根节点,最后遍历右子树。后序遍历:后序遍历首先遍历左子树,然后遍历右子树,最后访问根结点题目描述根据前序遍历产生的序列和中序遍历产生的序列生成一颗二叉树思路分析假如有这么一棵二叉树:可以看出它前序遍历序列为:8 6 5 7 10 9 11,中序遍历序列为:5 6 7 8 9 10 11其中有个很明显的特征,根结点的值为前序遍历序列的第一个值,而且我们在中序遍历序列中很容易看出,根结点左右两边的结点分别为构成左子树和右子树的结点,所以我们可以得到一种解决问题的思路:获取前序遍历的第一个值,构建根结点生成左子树的前序遍历序列和中序遍历序列生成右子树的前序遍历序列和中序遍历序列重复以上过程…示例代码function reConstructBinaryTree(pre, vin){ if(!pre || !vin || !pre.length || !vin.length){ return null } let root = new TreeNode(pre[0]), tIndex = vin.indexOf(pre[0]), leftIn = [],leftPre = [],rightIn = [],rightPre = [] for(let i = 0; i < tIndex; i++){ leftIn.push(vin[i]) leftPre.push(pre[i+1]) } for(let i = tIndex+1; i < pre.length; i++){ rightIn.push(vin[i]) rightPre.push(pre[i]) } //递归 root.left = reConstructBinaryTree(leftPre, leftIn) root.right = reConstructBinaryTree(rightPre, rightIn) return root}以上思路、代码有错漏请在评论区指出!总结代码部分来自牛客网–剑指offer,相应的题目也都可以在上面找到。 ...

November 5, 2018 · 1 min · jiezi

前缀树 - 一种好玩的树型数据结构

上篇内容有在介绍 Gin 的路由实现时提到了前缀树,这次我们稍微深入探究一下前缀树的实现。本文以一道编程题为例,讲述前缀树的实现,以及前缀树的一种优化形态压缩前缀树。MapSum 问题LeetCode 上有一道编程题是这样的实现一个 MapSum 类里的两个方法,insert 和 sum。对于方法 insert,你将得到一对(字符串,整数)的键值对。字符串表示键,整数表示值。如果键已经存在,那么原来的键值对将被替代成新的键值对。对于方法 sum,你将得到一个表示前缀的字符串,你需要返回所有以该前缀开头的键的值的总和。示例 1:输入: insert(“apple”, 3), 输出: Null输入: sum(“ap”), 输出: 3输入: insert(“app”, 2), 输出: Null输入: sum(“ap”), 输出: 5前缀树根据题意,我们定义的 MapSum 的数据结构为:type MapSum struct { char byte children map[byte]MapSum val int}/* Initialize your data structure here. */func Constructor() MapSum { }func (this *MapSum) Insert(key string, val int) { }func (this *MapSum) Sum(prefix string) int { }假设输入数据为:m := Constructor()m.Insert(“inter”, 1)m.Insert(“inner”, 2)m.Insert(“in”, 2)m.Insert(“if”, 4)m.Insert(“game”, 8)则构造的前缀树应该是:前缀树特性:根节点不包含字符,除根节点外的每一个子节点都包含一个字符从根节点到某一节点的路径上的字符连接起来,就是该节点对应的字符串。每个节点的所有子节点包含的字符都不相同。Insert 函数Insert 函数的签名:func (this *MapSum) Insert(key string, val int)我们把 this 当做父节点,当插入的 key 长度为 1 时,则直接说明 key 对应的节点应该是 this 的孩子节点。if len(key) == 1 { for i, m := range this.children { // c 存在与孩子节点 // 直接更新 if i == c { m.val = val return } } // 未找到对应孩子 // 直接生成新孩子 this.children[c] = &MapSum{ char: c, val: val, children: make(map[byte]*MapSum), } return}当插入的 key 长度大于 1,则寻找 key[0] 对应的子树,如果不存在,则插入新孩子节点;设置 this = this.children[key[0]] 继续迭代;c := key[0]for i, m := range this.children { if i == c { key = key[1:] this = m continue walk }}// 未找到节点this.children[c] = &MapSum{ char: c, val: 0, children: make(map[byte]*MapSum),}this = this.children[c]key = key[1:]continue walkSum 函数Sum 函数签名:func (this *MapSum) Sum(prefix string) intSum 函数的基本思想为:先找到前缀 prefix 对应的节点,然后统计以该节点为树根的的子树的 val 和。// 先找到符合前缀的节点// 然后统计和for prefix != "" { c := prefix[0] var ok bool if this, ok = this.children[c]; ok { prefix = prefix[1:] continue } else{ // prefix 不存在 return 0 }}return this.sumNode()sumNode 函数统计了子树的 val 和,使用递归遍历树:s := this.valfor _, child := range this.children{ s += child.sumNode()}return s以上是一种标准的前缀树的做法。当字符串公用的节点比较少的时候,对于每个字符都要创建单独的节点,有点浪费空间。有一种压缩前缀树的算法,在处理前缀树问题的时候能够使用更少的节点。压缩前缀树对与上面的例子来说,压缩前缀树是这样的结果:对于该例子来说,明显少了很多节点。另外,我们的 MapSum 结构体也稍微有了变化:type MapSum struct { // 之前的 char byte 变成了 key string key string children map[byte]*MapSum val int}Insert压缩前缀树与前缀树的实现不同点在于节点的分裂。比如,当树中已经存在 “inner”, “inter” 的情况加,再加入 “info” 时,原 “in” 节点需要分裂成 “i” -> “n” 两个节点,如图:在 Insert 时,需要判断当前插入字符串 key 与 节点字符串 this.key 的最长公共前缀长度 n:minLen := min(len(key), len(this.key))// 找出最长公共前缀长度 nn := 0for n < minLen && key[n] == this.key[n] { n ++}然后拿 n 与 len(this.key) 比较,如果比 this.key 长度短,则 this.key 需要分裂,否则,不需要分裂。this 节点分裂逻辑:// 最前公共前缀 n < len(this.key)// 则该节点需要分裂child := &MapSum{ val: this.val, key: this.key[n:], children: this.children,}// 更新当前节点this.key = this.key[:n]this.val = 0this.children = make(map[byte]*MapSum)this.children[child.key[0]] = child然后再判断 n 与 len(key),如果 n == len(key),则说明 key 对应该节点。直接更新 valif n == len(key) { this.val = val return}n < len(key) 时,如果有符合条件子树,则继续迭代,否则直接插入孩子节点:key = key[n:]c := key[0]// 如果剩余 子key 的第一个字符存在与 children// 则继续向下遍历树if a, ok := this.children[c]; ok { this = a continue walk} else{ // 否则,新建节点 this.children[c] = &MapSum{ key: key, val: val, children: make(map[byte]*MapSum), } return}以上是压缩前缀树的做法。算法优化上述 MapSum 的 children 使用的是 map,但是 map 一般占用内存较大。可以使用 节点数组children + 节点前缀数组 indices 的方式维护子节点,其中 indices 与 children 一一对应。此时的结构体应该是这样的:type MapSum struct { key string indices []byte children []*MapSum val int}查找子树时,需要拿 key[:n][0] 与 indices 中的字符比较,找到下标后继续迭代子树;未找到时插入子树即可。以上。Y_xx相关内容:Gin 框架的路由结构浅析 ...

October 14, 2018 · 2 min · jiezi