王争《数据结构与算法之美-基础篇》笔记_上篇

本文详细探讨了编程中数组的下标选择、数组与链表的区别、JavaArrayList的优势、二维数组寻址、JVM的垃圾回收机制、链表的实现和应用、LRU缓存淘汰算法、栈的结构与表达式匹配、浏览器导航、队列的用途与线程池管理、递归的概念及其常见问题,以及斐波那契数列的递归求解。
摘要由CSDN通过智能技术生成

一、数组

1)为什么很多编程语言中,数组的下标都从0开始编号?

数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。因为有连续的内存空间和相同类型的数据,数组就可以实现随机访问。实现随机访问的方式是
a[i]_address = base_address + i * data_type_size
其中 data_type_size 表示数组中每个元素的大小。

如果用 a 来表示数组的首地址,a[0]就是偏移为 0 的位置,也就是首地址,a[k]就表示偏移 k 个 type_size 的位置,所以计算 a[k]的内存地址只需要用这个公式
a[k]_address = base_address + k * type_size

但是,如果数组从 1 开始计数,那我们计算数组元素 a[k]的内存地址就会变为:
a[k]_address = base_address + (k-1)*type_size
对比两个公式,我们不难发现,从 1 开始编号,每次随机访问数组元素都多了一次减法运算,对于 CPU 来说,就是多了一次减法指令。
数组作为非常基础的数据结构,通过下标随机访问数组元素又是其非常基础的编程操作,效率的优化就要尽可能做到极致。所以为了减少一次减法操作,数组选择了从 0 开始编号,而不是从 1 开始。

2)数组和链表的区别

  • 链表适合插入、删除,时间复杂度 O(1);数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。
  • 对内存要求方面:数组对内存的要求更高。因为数组需要一块连续内存空间来存放数据。(可能出现的问题就是:内存总的剩余空间足够,但是申请容量较大的数组时申请失败) 链表对内存的要求较低,是因为链表不需要连续的内存空间,它通过“指针”将一组零散的内存块串联起来使用。 但是要注意:链表虽然方便。但是内存开销比数组大了将近一倍
  • 数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致“内存不足(out of memory)”。如果声明的数组过小,则可能出现不够用的情况。这时只能再申请一个更大的内存空间,把原数组拷贝进去,非常费时。链表本身没有大小的限制,天然地支持动态扩容,我觉得这也是它与数组最大的区别。

3)容器 vs 数组:java中的ArrayList与数组相比,到底有哪些优势呢?

  • 容器优势:
    1.ArrayList 最大的优势就是可以将很多数组操作的细节封装起来。比如数组插入、删除数据时需要搬移其他数据等。
    2.支持动态扩容。
  • 数组优势:
    1.Java ArrayList 无法存储基本类型,比如 int、long,需要封装为 Integer、Long 类,而 Autoboxing、Unboxing 则有一定的性能消耗,所以如果特别关注性能,或者希望使用基本类型,就可以选用数组。
    2.如果数据大小事先已知,并且对数据的操作非常简单,用不到 ArrayList 提供的大部分方法,也可以直接使用数组。
    3.还有一个是我个人的喜好,当要表示多维数组时,用数组往往会更加直观。比如 Object[][] array;而用容器的话则需要这样定义:ArrayList > array。
  • 总结:对于业务开发,直接使用容器就足够了,省时省力。毕竟损耗一丢丢性能,完全不会影响到系统整体的性能。但如果你是做一些非常底层的开发,比如开发网络框架,性能的优化需要做到极致,这个时候数组就会优于容器,成为首选。

4)二维数组寻址

对于 m * n 的数组,a [ i ][ j ] (i < m,j < n)的地址为:address = base_address + ( i * n + j) * type_size

5)Java 中的JVM、聊一下对标记清除垃圾回收算法的理解

  • 大多数主流虚拟机采用可达性分析算法来判断对象是否存活,在标记阶段,会遍历所有 GC ROOTS,将所有 GC ROOTS 可达的对象标记为存活。只有当标记工作完成后,清理工作才会开始。
  • 不足:
    1.效率问题。标记和清理效率都不高,但是当知道只有少量垃圾产生时会很高效。
    2.空间问题。会产生不连续的内存空间碎片。

leetcode

二分查找:https://leetcode-cn.com/problems/binary-search/
测试用例:
1)[-1,0,3,5,9,12],2 返回-1
2)[-1,0,3,5,9,12],9 返回4
3) [2,5], 5 返回1

二、链表

1)三种最常见的链表结构:

  • 单链表:内存块称作“结点”、链上的下一个结点的地址称作“后继指针”。头结点用于记录链表的基地址,尾结点的后继指针指向一个空地址 NULL。单向链表的插入和删除操作,我们只需要考虑相邻结点的指针改变,所以对应的时间复杂度是 O(1)。
  • 双向链表:结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点。
  • 循环链表:循环链表的尾结点指针是指向链表的头结点。可用于解决约瑟夫问题:人们站在一个等待被处决的圈子里。 计数从圆圈中的指定点开始,并沿指定方向围绕圆圈进行。 在跳过指定数量的人之后,处刑下一个人。 对剩下的人重复该过程,从下一个人开始,朝同一方向跳过相同数量的人,直到只剩下一个人,并被释放。

2)链表应用场景

  • 缓存是一种提高数据读取性能的技术,在硬件设计、软件开发中都有着非常广泛的应用,比如常见的 CPU 缓存、数据库缓存、浏览器缓存等等。缓存的大小有限,当缓存被用满时,哪些数据应该被清理出去,哪些数据应该被保留?这就需要缓存淘汰策略来决定。常见的策略有三种:先进先出策略 FIFO(First In,First Out)、最少使用策略 LFU(Least Frequently Used)、最近最少使用策略 LRU(Least Recently Used)。

3)如何实现LRU缓存淘汰算法?

我们维护一个有序单链表,越靠近链表尾部的结点是越早之前访问的。当有一个新的数据被访问时,我们从链表头开始顺序遍历链表。1. 如果此数据之前已经被缓存在链表中了,我们遍历得到这个数据对应的结点,并将其从原来的位置删除,然后再插入到链表的头部。2. 如果此数据没有在缓存链表中,又可以分为两种情况:如果此时缓存未满,则将此结点直接插入到链表的头部;如果此时缓存已满,则链表尾结点删除,将新的数据结点插入链表的头部。

4)写链表的6个技巧

技巧一:理解指针或引用的含义
技巧二:警惕指针丢失和内存泄漏

// 错误示范:x->next指错
p->next = x;  // 将p的next指针指向x结点;
x->next = p->next;  // 将x的结点的next指针指向b结点;

技巧三:利用哨兵简化实现难度:单链表的插入,第一个节点不一样,利用哨兵可以简化操作
技巧四:重点留意边界条件处理,以下情况代码是否能够工作?

  • 如果链表为空时
  • 如果链表只包含一个结点时
  • 如果链表只包含两个结点时
  • 代码逻辑在处理头结点和尾结点的时候
    技巧五:技巧五:举例画图,辅助思考
    技巧六:多写多练,没有捷径

leetcode

  • 单链表反转(206):https://leetcode-cn.com/problems/reverse-linked-list/ 测试用例[1,2,3,4,5]、[1,2]、[1]
  • 链表中环的检测(141):https://leetcode-cn.com/problems/linked-list-cycle/
  • 两个有序的链表合并(21):https://leetcode-cn.com/problems/merge-two-sorted-lists/
  • 删除链表倒数第 n 个结点(19):https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/
  • 求链表的中间结点(876):https://leetcode-cn.com/problems/middle-of-the-linked-list/

三、栈

1)栈的结构是什么?

栈是一种操作受限的数据结构,只支持入栈和出栈操作。后进先出是它最大的特点。栈既可以通过数组实现(称为顺序栈),也可以通过链表来实现(链式栈)。不管基于数组还是链表,入栈、出栈的时间复杂度都为 O(1)。

2)栈如何完成表达式中括号的匹配?

  • 背景:假设表达式中只包含三种括号,圆括号 ()、方括号[]和花括号{},并且它们可以任意嵌套。比如,{[] ()[{}]}或[{()}([])]等都为合法格式,而{[}()]或[({)]为不合法的格式。那我现在给你一个包含三种括号的表达式字符串,如何检查它是否合法呢?
  • :当扫描到左括时,则将其压入栈中;当扫描到右括号时,从栈顶取出一个左括号。如果能够匹配,比如“(”跟“)”匹配,“[”跟“]”匹配,“{”跟“}”匹配,则继续扫描剩下的字符串。如果扫描的过程中,遇到不能配对的右括号,或者栈中没有数据,则说明为非法格式。当所有的括号都扫描完成之后,如果栈为空,则说明字符串为合法格式;否则,说明有未匹配的左括号,为非法格式。

3)如何实现浏览器的前进和后退功能?

  • 背景:当你依次访问完一串页面 a-b-c 之后,点击浏览器的后退按钮,就可以查看之前浏览过的页面 b 和 a。当你后退到页面 a,点击前进按钮,就可以重新查看页面 b 和 c。但是,如果你后退到页面 b 后,点击了新的页面 d,那就无法再通过前进、后退功能查看页面 c 了。
  • :我们使用两个栈,X 和 Y,我们把首次浏览的页面依次压入栈 X,当点击后退按钮时,再依次从栈 X 中出栈,并将出栈的数据依次放入栈 Y。当我们点击前进按钮时,我们依次从栈 Y 中取出数据,放入栈 X 中。当栈 X 中没有数据时,那就说明没有页面可以继续后退浏览了。当栈 Y 中没有数据,那就说明没有页面可以点击前进按钮浏览了。

leetcode

有效的括号 https://leetcode-cn.com/problems/valid-parentheses/

四、队列

1)队列的应用有哪些

比如高性能队列 Disruptor、Linux 环形缓存,都用到了循环并发队列;Java concurrent 并发包利用 ArrayBlockingQueue 来实现公平锁等。

2)当我们向固定大小的线程池中请求一个线程时,如果线程池中没有空闲资源了,这个时候线程池如何处理这个请求?是拒绝请求还是排队请求?各种处理策略又是怎么实现的呢?

我们一般有两种处理策略。第一种是非阻塞的处理方式,直接拒绝任务请求;另一种是阻塞的处理方式,将请求排队,等到有空闲线程时,取出排队的请求继续处理。那如何存储排队的请求呢?我们前面说过,队列有基于链表和基于数组这两种实现方式。这两种实现方式对于排队请求又有什么区别呢?基于链表的实现方式,可以实现一个支持无限排队的无界队列(unbounded queue),但是可能会导致过多的请求排队等待,请求处理的响应时间过长。所以,针对响应时间比较敏感的系统,基于链表实现的无限排队的线程池是不合适的。而基于数组实现的有界队列(bounded queue),队列的大小有限,所以线程池中排队的请求超过队列大小时,接下来的请求就会被拒绝,这种方式对响应时间敏感的系统来说,就相对更加合理。不过,设置一个合理的队列大小,也是非常有讲究的。队列太大导致等待的请求太多,队列太小会导致无法充分利用系统资源、发挥最大性能。

leetcode

  • 用队列实现栈(不是很适合用于面试) https://leetcode-cn.com/problems/implement-stack-using-queues/
  • 用栈实现队列(不是很适合用于面试)https://leetcode-cn.com/problems/implement-queue-using-stacks/

五、递归

1)什么是递归?

  1. 递归是一种非常高效、简洁的编码技巧,一种应用非常广泛的算法,比如DFS深度优先搜索、前中后序二叉树遍历等都是使用递归。
  2. 方法或函数调用自身的方式称为递归调用,调用称为递,返回称为归。
  3. 基本上,所有的递归问题都可以用递推公式来表示,比如
f(n) = f(n-1) + 1;
f(n) = f(n-1) + f(n-2);
f(n)=n*f(n-1);

2)递归的优点与缺点

  • 优点:递归代码的表达力很强,写起来非常简洁;
  • 缺点:空间复杂度高、有堆栈溢出的风险、存在重复计算、过多的函数调用会耗时较多等问题。

3)什么样的问题可以用递归来解决呢?递归的3个条件

  1. 一个问题的解可以分解为几个子问题的解
  2. 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样
  3. 存在递归终止条件

4)如何实现递归

  1. 递归代码编写
    写递归代码的关键就是找到如何将大问题分解为小问题的规律,并且基于此写出递推公式,然后再推敲终止条件,最后将递推公式和终止条件翻译成代码。
  2. 递归代码理解
    对于递归代码,若试图想清楚整个递和归的过程,实际上是进入了一个思维误区。
    那该如何理解递归代码呢?如果一个问题A可以分解为若干个子问题B、C、D,你可以假设子问题B、C、D已经解决。而且,你只需要思考问题A与子问题B、C、D两层之间的关系即可,不需要一层层往下思考子问题与子子问题,子子问题与子子子问题之间的关系。屏蔽掉递归细节,这样子理解起来就简单多了。
  • 因此,理解递归代码,就把它抽象成一个递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每个步骤。

5)递归常见问题及解决方案

1. 堆栈溢出

  • 为什么会造成堆栈溢出
    函数调用会使用栈来保存临时变量。每调用一个函数,都会将临时变量封装为栈帧压入内存栈,等函数执行完成返回时,才出栈。系统栈或者虚拟机栈空间一般都不大。如果递归求解的数据规模很大,调用层次很深,一直压入栈,就会有堆栈溢出的风险。
  • 如何预防堆栈溢出呢?
    通过在代码中限制递归调用的最大深度的方式来解决这个问题。递归调用超过一定深度(比如 1000)之后,我们就不继续往下再递归了,直接返回报错。

2. 重复计算

  • 通过某种数据结构来保存已经求解过的值,从而避免重复计算。

leetcode

斐波那契数 https://leetcode-cn.com/problems/fibonacci-number/

你想知道你的GPS是如何在几秒钟内从看起来无数多条可能路径中找到到达目的地的最快捷路径的吗?当你在网上购物时,你的信用卡账号是如何被保护的呢?答案均是算法。本书是关于计算机算法基础的权威指南。在本书中,作者展示了计算机如何通过算法解决问题。 读者将学习到什么是计算机算法,如何描述计算机算法,以及如何评估计算机算法。读者还将学习到在计算机中查找信息的简单方法;在计算机中将信息按照某个预定的顺序重排(“排序”);如何解决那些在计算机中能使用一种被称为“图”的数学结构来建模的基本问题(可用于对道路网建模,针对任务间的依赖建模,以及金融套利交易建模);如何解决关于字符串(例如DNA结构)的问题;密码学的基本原理;数据压缩的基本原理;甚至那些至今还没有人得出如何借助计算机在一段合理的时间内求解的问题。 计算机是如何解决问题的呢?小小的GPS是如何只在几秒钟内就从无数条可能路径中找出到达目的地的最快捷路径的呢?在网上购物时,又如何防止他人窃取你的信用卡账号呢?解决这些问题,以及大量其他问题的答案均是算法。我写本书的目的就是为你打开算法之门,解开算法之谜。 我是《算法导论》的合著者之一。《算法导论》是一本特别好的书(当然,这是我个人的主观评价),但是它确实相当专业。 本书并不是《算法导论》,甚至不能被称为一本教材。它既没有对计算机算法领域进行广度或深度的研究,也没有遵照惯例来讲述设计计算机算法的方法,甚至连一道需要读者自己求解的难题或者练习题也没有。 那么,这是一本什么样的书呢?如果你符合如下条件,那么就可以开始阅读之旅了: ●你对计算机如何解决问题感兴趣; ●你想知道如何评估这些解决方案的质量; ●你想了解计算方面的问题和这些问题的解决方案是如何与非计算机世界关联起来的; ●你能处理一点数学运算; ●你不需要编写过计算机程序(当然,编写过程序更好)。 一些计算机算法方面的书籍是讲述理论概念的,并涉及非常少的技术细节;一些书籍关注的全是技术细节;而另外一些书籍是介于这两者之间的。每类图书都有自己的定位,我将本书定位于介于两者之间。诚然,本书涉及了一些数学知识,并且部分地方阐述得非常仔细,但是我已经竭力避免深入阐述细节(或许除了本书的末尾部分,我无法克制住自己,阐述了一些细节知识)。 我认为本书有点像开胃菜。设想你去了一家意大利餐厅,点了一份开胃菜,直到吃完开胃菜,你才会决定是否点其余食物。开胃菜到了,你就开始用餐了。或许你不喜欢吃开胃菜,并且决定不点其他菜了。可能你喜欢吃开胃菜,但是吃完它,你就感觉饱了,因此不需要点其他菜了。或者也有可能你喜欢吃开胃菜,但你并没有吃饱,此时你便开始期待其他菜了。将本书看作开胃菜,我希望能够产生后两种结果之一:或者读完了本书,你就很满足,感觉没有必要再深入探究算法世界了;或者你非常喜欢从本书中所学到的知识,以至于你想要学习更多算法方面的内容。每一章最后一节的标题为“拓展阅读”,其中会介绍更多关于该章主题的更为深入的书籍和文章。 你将从本书中学到什么: 我无法断定你将从本书中学到什么。如下是我希望你能从本书中学到的: ●什么是计算机算法,能够采用一种方式来描述计算机算法,以及如何评估算法。 ●在计算机中查找信息的简单方式。 ●在计算机中重排信息以使其以一种预定顺序排列的方法。(我们称这一任务为“排序”。) ●如何解决那些能在计算机中以一种称为“图”的数学结构建模的基本难题。在许多应用中,利用图建模的领域包括:道路网(哪些十字路口到哪些十字路口有直接相连的道路,这些道路有多长),任务间的依赖关系(哪个任务必须在其他任务之前完成),金融关系(世界各国货币间的汇率是多少),或者是人与人之间的联系(谁认识谁?谁讨厌谁?哪个演员和哪个演员出现在同一个电影中等)。 ●如何解决关于文本字符串的问题。其中一些问题在某些领域有所应用,例如生物学领域,其中字符表示基本的分子,字符串表示DNA结构。 ●密码学背后的基本原理。即使你自己从来没有加密过一条信息,你的计算机很可能已经对它执行加密操作了(例如当你在网上购物时)。 ●数据压缩的基本概念,这远远超过了“f u cn rd ths u cn gt a gd jb n gd pay”背后的压缩原理。 ●计算机在任意合理的时间内都难以解决的一些问题,或者至少还没有人想出如何解决的问题。 为了理解本书中的内容,你需要事先了解什么 正如我之前所说的,本书中涉及部分数学知识。如果你害怕数学,那么你可以尝试着跳过它,或者你也可以尝试着阅读涉及更少专业技术知识的书籍。但是我已经尽力做到令数学部分变得容易理解了。 我假定你从来没有写过,甚至从来没有读过一个计算机程序。如果你能看懂提纲的内容,就应该能够理解我是如何表达每一步算法,以及如何将这些步骤合并在一起组合成一个完整的算法的。如果你听到过
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值