
数据结构与算法之美
文章平均质量分 93
面向初学者的算法深入浅出之道
cjh-Java
不积跬步,无以至千里
展开
-
41 _ 动态规划理论:一篇文章带你彻底搞懂最优子结构、无后效性和重复子问题
刚刚我讲了,如何鉴别一个问题是否可以用动态规划来解决。现在,我再总结一下,动态规划解题的一般思路,让你面对动态规划问题的时候,能够有章可循,不至于束手无策。我个人觉得,解决动态规划问题,一般有两种思路。我把它们分别叫作,状态转移表法和状态转移方程法。原创 2023-11-18 07:30:00 · 163 阅读 · 0 评论 -
40 _ 初识动态规划:如何巧妙解决“双十一”购物时的凑单问题?
淘宝的“双十一”购物节有各种促销活动,比如“满200元减50元”。假设你女朋友的购物车中有n个(n>100)想买的商品,她希望从里面选几个,在凑够满减条件的前提下,让选出来的商品价格总和最大程度地接近满减条件(200元),这样就可以极大限度地“薅羊毛”。作为程序员的你,能不能编个代码来帮她搞定呢?要想高效地解决这个问题,就要用到我们今天讲的动态规划(Dynamic Programming)。原创 2023-11-18 07:15:00 · 254 阅读 · 0 评论 -
39 _ 回溯算法:从电影《蝴蝶效应》中学习回溯算法的核心思想
深度优先搜索算法利用的是回溯算法思想。这个算法思想非常简单,但是应用却非常广泛。它除了用来指导像深度优先搜索这种经典的算法设计之外,还可以用在很多实际的软件开发场景中,比如正则表达式匹配、编译原理中的语法分析等。除此之外,很多经典的数学问题都可以用回溯算法解决,比如数独、八皇后、0-1背包、图的着色、旅行商问题、全排列等等。既然应用如此广泛,我们今天就来学习一下这个算法思想,看看它是如何指导我们解决问题的。原创 2023-11-17 07:45:00 · 434 阅读 · 0 评论 -
38 _ 分治算法:谈一谈大规模计算框架MapReduce中的分治思想
MapReduce是Google大数据处理的三驾马车之一,另外两个是GFS和Bigtable。它在倒排索引、PageRank计算、网页分析等搜索引擎相关的技术中都有大量的应用。尽管开发一个MapReduce看起来很高深,感觉跟我们遥不可及。实际上,万变不离其宗,它的本质就是我们今天要学的这种算法思想,分治算法。原创 2023-11-17 07:15:00 · 270 阅读 · 0 评论 -
37 _ 贪心算法:如何用贪心算法实现Huffman压缩编码?
基础的数据结构和算法我们基本上学完了,接下来几节,我会讲几种更加基本的算法。它们分别是贪心算法、分治算法、回溯算法、动态规划。更加确切地说,它们应该是算法思想,并不是具体的算法,常用来指导我们设计具体的算法和编码等。贪心、分治、回溯、动态规划这4个算法思想,原理解释起来都很简单,但是要真正掌握且灵活应用,并不是件容易的事情。所以,接下来的这4个算法思想的讲解,我依旧不会长篇大论地去讲理论,而是结合具体的问题,让你自己感受这些算法是怎么工作的,是如何解决问题的,带你在问题中体会这些算法的本质。原创 2023-11-16 06:45:00 · 256 阅读 · 0 评论 -
35 _ Trie树:如何实现搜索引擎的搜索关键词提示功能?
Trie树,也叫“字典树”。顾名思义,它是一个树形结构。它是一种专门处理字符串匹配的数据结构,用来解决在一组字符串集合中快速查找某个字符串的问题。当然,这样一个问题可以有多种解决方法,比如散列表、红黑树,或者我们前面几节讲到的一些字符串匹配算法,但是,Trie树在这个问题的解决上,有它特有的优点。不仅如此,Trie树能解决的问题也不限于此,我们一会儿慢慢分析。现在,我们先来看下,Trie树到底长什么样子。我举个简单的例子来说明一下。原创 2023-11-16 06:30:00 · 348 阅读 · 0 评论 -
32 _ 字符串匹配基础(上):如何借助哈希算法实现高效字符串匹配?
从今天开始,我们来学习字符串匹配算法。字符串匹配这样一个功能,我想对于任何一个开发工程师来说,应该都不会陌生。我们用的最多的就是编程语言提供的字符串查找函数,比如Java中的indexOf(),Python中的find()函数等,它们底层就是依赖接下来要讲的字符串匹配算法。字符串匹配算法很多,我会分四节来讲解。今天我会讲两种比较简单的、好理解的,它们分别是:BF算法和RK算法。下一节,我会讲两种比较难理解、但更加高效的,它们是:BM算法和KMP算法。原创 2023-11-16 06:15:00 · 260 阅读 · 0 评论 -
31 _ 深度和广度优先搜索:如何找出社交网络中的三度好友关系?
我们知道,算法是作用于具体数据结构之上的,深度优先搜索算法和广度优先搜索算法都是基于“图”这种数据结构的。这是因为,图这种数据结构的表达能力很强,大部分涉及搜索的场景都可以抽象成“图”。图上的搜索算法,最直接的理解就是,在图中找出从一个顶点出发,到另一个顶点的路径。具体方法有很多,比如今天要讲的两种最简单、最“暴力”的深度优先、广度优先搜索,还有A*、IDA*等启发式搜索算法。我们上一节讲过,图有两种主要存储方法,邻接表和邻接矩阵。今天我会用邻接表来存储图。我这里先给出图的代码实现。原创 2023-11-15 07:30:00 · 367 阅读 · 0 评论 -
30 _ 图的表示:如何存储微博、微信等社交网络中的好友关系?
微博、微信、LinkedIn这些社交软件我想你肯定都玩过吧。在微博中,两个人可以互相关注;在微信中,两个人可以互加好友。那你知道,如何存储微博、微信等这些社交网络的好友关系吗?这就要用到我们今天要讲的这种数据结构:图。实际上,涉及图的算法有很多,也非常复杂,比如图的搜索、最短路径、最小生成树、二分图等等。我们今天聚焦在图存储这一方面,后面会分好几节来依次讲解图相关的算法。原创 2023-11-15 07:15:00 · 169 阅读 · 0 评论 -
28 _ 堆和堆排序:为什么说堆排序没有快速排序快?
我们今天讲另外一种特殊的树,“堆”(Heap)。堆这种数据结构的应用场景非常多,最经典的莫过于堆排序了。堆排序是一种原地的、时间复杂度为O(nlogn)的排序算法。前面我们学过快速排序,平均情况下,它的时间复杂度为O(nlogn)。尽管这两种排序算法的时间复杂度都是O(nlogn),甚至堆排序比快速排序的时间复杂度还要稳定,但是,在实际的软件开发中,快速排序的性能要比堆排序好,这是为什么呢?现在,你可能还无法回答,甚至对问题本身还有点疑惑。没关系,带着这个问题,我们来学习今天的内容。原创 2023-11-14 07:15:00 · 181 阅读 · 0 评论 -
29 _ 堆的应用:如何快速获取到Top 10最热门的搜索关键词?
搜索引擎的热门搜索排行榜功能你用过吗?你知道这个功能是如何实现的吗?实际上,它的实现并不复杂。搜索引擎每天会接收大量的用户搜索请求,它会把这些用户输入的搜索关键词记录下来,然后再离线地统计分析,得到最热门的Top 10搜索关键词。那请你思考下,假设现在我们有一个包含10亿个搜索关键词的日志文件,如何能快速获取到热门榜Top 10的搜索关键词呢?这个问题就可以用堆来解决,这也是堆这种数据结构一个非常典型的应用。原创 2023-11-14 07:30:00 · 78 阅读 · 0 评论 -
27 _ 递归树:如何借助树来求解递归算法的时间复杂度?
我们都知道,递归代码的时间复杂度分析起来很麻烦。有一个巧妙的方式是。原创 2023-11-13 07:30:00 · 247 阅读 · 0 评论 -
26 _ 红黑树(下):掌握这些技巧,你也可以实现一个红黑树
红黑树是一个让我又爱又恨的数据结构,“爱”是因为它稳定、高效的性能,“恨”是因为实现起来实在太难了。我今天讲的红黑树的实现,对于基础不太好的同学,理解起来可能会有些困难。但是,我觉得没必要去死磕它。我为什么这么说呢?因为,即便你将左右旋背得滚瓜烂熟,我保证你过不几天就忘光了。因为,学习红黑树的代码实现,对于你平时做项目开发没有太大帮助。对于绝大部分开发工程师来说,这辈子你可能都用不着亲手写一个红黑树。除此之外,它对于算法面试也几乎没什么用,一般情况下,靠谱的面试官也不会让你手写红黑树的。原创 2023-11-13 07:15:00 · 50 阅读 · 0 评论 -
25 _ 红黑树(上):为什么工程中都用红黑树这种二叉树?
平衡二叉树的严格定义是这样的:二叉树中任意一个节点的左右子树的高度相差不能大于1。从这个定义来看,上一节我们讲的完全二叉树、满二叉树其实都是平衡二叉树,但是非完全二叉树也有可能是平衡二叉树。平衡二叉查找树不仅满足上面平衡二叉树的定义,还满足二叉查找树的特点。最先被发明的平衡二叉查找树是AVL树,它严格符合我刚讲到的平衡二叉查找树的定义,即任何节点的左右子树高度相差不超过1,是一种高度平衡的二叉查找树。原创 2023-11-11 15:27:08 · 105 阅读 · 0 评论 -
24 _ 二叉树基础(下):有了如此高效的散列表,为什么还需要二叉树?
(L-1)个之间(我们假设最大层数是L)。如果我们把每一层的节点个数加起来就是总的节点个数n。原创 2023-11-11 15:03:23 · 502 阅读 · 0 评论 -
21 _ 哈希算法(上):如何防止数据库中的用户信息被脱库?
我们前面几节讲到“散列表”“散列函数”,这里又讲到“哈希算法”,你是不是有点一头雾水?实际上,不管是“散列”还是“哈希”,这都是中文翻译的差别,英文其实就是“Hash所以,我们常听到有人把“散列表”叫作“哈希表”“Hash表”,把“哈希算法”叫作“Hash算法”或者“散列算法”。那到底什么是哈希算法呢?哈希算法的定义和原理非常简单,基本上一句话就可以概括了。将任意长度的二进制值串映射为固定长度的二进制值串,这个映射的规则就是哈希算法,而通过原始数据映射之后得到的二进制值串就是哈希值。原创 2023-11-09 07:59:54 · 136 阅读 · 0 评论 -
20 _ 散列表(下):为什么散列表和链表经常会一起使用?
有两种数据结构,散列表和链表,经常会被放在一起使用。例如,如何用链表来实现LRU缓存淘汰算法,但是链表实现的LRU缓存淘汰算法的时间复杂度是O(n),当时我也提到了,通过散列表可以将这个时间复杂度降低到O(1)。Redis的有序集合是使用跳表来实现的,跳表可以看作一种改进版的链表。当时我们也提到,Redis有序集合不仅使用了跳表,还用到了散列表。除此之外,如果你熟悉Java编程语言,你会发现LinkedHashMap这样一个常用的容器,也用到了散列表和链表两种数据结构。原创 2023-11-09 07:54:10 · 122 阅读 · 0 评论 -
19 _ 散列表(中):如何打造一个工业级水平的散列表?
散列表的查询效率并不能笼统地说成是O(1)。它跟散列函数、装载因子、散列冲突等都有关系。如果散列函数设计得不好,或者装载因子过高,都可能导致散列冲突发生的概率升高,查询效率下降。在极端情况下,有些恶意的攻击者,还有可能通过精心构造的数据,使得所有的数据经过散列函数之后,都散列到同一个槽里。如果我们使用的是基于链表的冲突解决方法,那这个时候,散列表就会退化为链表,查询的时间复杂度就从O(1)急剧退化为O(n)。如果散列表中有10万个数据,退化后的散列表查询的效率就下降了10万倍。更直接点说,如果之前运行100原创 2023-11-09 07:41:08 · 87 阅读 · 0 评论 -
17 _ 跳表:为什么Redis一定要用跳表来实现有序集合?
上两节我们讲了二分查找算法。当时我讲到,因为二分查找底层依赖的是数组随机访问的特性,所以只能用数组来实现。如果数据存储在链表中,就真的没法用二分查找算法了吗?实际上,我们只需要对链表稍加改造,就可以支持类似“二分”的查找算法。我们把改造之后的数据结构叫做(Skip list),也就是今天要讲的内容。跳表这种数据结构对你来说,可能会比较陌生,因为一般的数据结构和算法书籍里都不怎么会讲。但是它确实是一种各方面性能都比较优秀的,可以支持快速地插入、删除、查找操作,写起来也不复杂,甚至可以替代。原创 2023-11-09 07:26:30 · 98 阅读 · 0 评论 -
18 _ 散列表(上):Word文档中的单词拼写检查功能是如何实现的?
散列函数计算得到的散列值是一个非负整数;如果key1 = key2,那hash(key1) == hash(key2);如果key1 ≠ key2,那hash(key1) ≠ hash(key2)。我来解释一下这三点。其中,第一点理解起来应该没有任何问题。因为数组下标是从0开始的,所以散列函数生成的散列值也要是非负整数。第二点也很好理解。相同的key,经过散列函数得到的散列值也应该是相同的。第三点理解起来可能会有问题,我着重说一下。原创 2023-11-09 07:09:28 · 547 阅读 · 0 评论 -
16 _ 二分查找(下):如何快速定位IP对应的省份地址?
前面的问题是查找第一个值等于给定值的元素,我现在把问题稍微改一下,查找最后一个值等于给定值的元素,又该如何做呢?如果你掌握了前面的写法,那这个问题你应该很轻松就能解决。你可以先试着实现一下,然后跟我写的对比一下。我们还是重点看第11行代码。如果a[mid]这个元素已经是数组中的最后一个元素了,那它肯定是我们要找的;如果a[mid]的后一个元素a[mid+1]不等于value,那也说明a[mid]就是我们要找的最后一个值等于给定值的元素。原创 2023-11-09 07:00:44 · 582 阅读 · 1 评论 -
15 _ 二分查找(上):如何用最省内存的方式实现快速查找功能?
二分查找的底层需要依赖数组这种数据结构,而数组为了支持随机访问的特性,要求内存空间连续,对内存的要求比较苛刻。比如,我们有1GB大小的数据,如果希望用数组来存储,那就需要1GB的连续内存空间。注意这里的“连续”二字,也就是说,即便有2GB的内存空间剩余,但是如果这剩余的2GB内存空间都是零散的,没有连续的1GB大小的内存空间,那照样无法申请一个1GB大小的数组。而我们的二分查找是作用在数组这种数据结构之上的,所以太大的数据用数组存储就比较吃力了,也就不能用二分查找了。原创 2023-11-05 16:42:45 · 145 阅读 · 0 评论 -
14 _ 排序优化:如何实现一个通用的、高性能的排序函数?
几乎所有的编程语言都会提供排序函数,比如C语言中qsort(),C++ STL中的sort()、stable_sort(),还有Java语言中的Collections.sort()。在平时的开发中,我们也都是直接使用这些现成的函数来实现业务逻辑中的排序功能。那你知道这些排序函数是如何实现的吗?底层都利用了哪种排序算法呢?基于这些问题,今天我们就来看排序这部分的最后一块内容:如何实现一个通用的、高性能的排序函数?原创 2023-11-05 16:35:33 · 192 阅读 · 0 评论 -
13 _ 线性排序:如何根据年龄给100万用户数据排序?
前两节中,着重分析了几种常用排序算法的原理、时间复杂度、空间复杂度、稳定性等。这节,将讲三种时间复杂度是O(n)的排序算法:桶排序、计数排序、基数排序。因为这些排序算法的时间复杂度是线性的,所以我们把这类排序算法叫作之所以能做到线性的时间复杂度,主要原因是,这三个算法是非基于比较的排序算法,都不涉及元素之间的比较操作。这几种排序算法理解起来都不难,时间、空间复杂度分析起来也很简单,但是对要排序的数据要求很苛刻,所以我们。按照惯例,我先给你出一道思考题:如何根据年龄给100万用户排序?原创 2023-11-02 08:23:21 · 109 阅读 · 0 评论 -
12 _ 排序(下):如何用快排思想在O(n)内查找第K大元素?
上一节我讲了冒泡排序、插入排序、选择排序这三种排序算法,它们的时间复杂度都是O(n),比较高,适合小规模数据的排序。这里会介绍两种时间复杂度为O(nlogn)的排序算法,和。这两种排序算法适合大规模的数据排序,比上一节讲的那三种排序算法要更常用。归并排序和快速排序都用到了分治思想,非常巧妙。乃至于,我们可以借鉴这个思想,来解决非排序的问题,比如:如何在O(n)的时间复杂度内查找一个无序数组中的第K大元素?这就要用到我们今天要讲的内容。原创 2023-11-02 07:49:54 · 233 阅读 · 0 评论 -
09 _ 队列:队列在线程池等有限资源池中的应用
我们知道,CPU资源是有限的,任务的处理速度与线程个数并不是线性正相关。相反,过多的线程反而会导致CPU频繁切换,处理性能下降。所以,线程池的大小一般都是综合考虑要处理任务的特点和硬件环境,来事先设置的。当我们向固定大小的线程池中请求一个线程时,如果线程池中没有空闲资源了,这个时候线程池如何处理这个请求?是拒绝请求还是排队请求?各种处理策略又是怎么实现的呢?实际上,这些问题并不复杂,其底层的数据结构就是我们今天要学的内容,队列(queue)。原创 2023-11-02 07:26:13 · 138 阅读 · 0 评论 -
11 _ 排序(上):为什么插入排序比冒泡排序更受欢迎?
排序对于任何一个程序员来说,可能都不会陌生。你学的第一个算法,可能就是排序。大部分编程语言中,也都提供了排序函数。在平常的项目中,我们也经常会用到排序。排序非常重要,所以我会花多一点时间来详细讲一讲经典的排序算法。排序算法太多了,有很多可能你连名字都没听说过,比如猴子排序、睡眠排序、面条排序等。我只讲众多排序算法中的一小撮,也是最经典的、最常用的:冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、桶排序。我按照时间复杂度把它们分成了三类,分三节课来讲解。原创 2023-11-01 09:04:54 · 412 阅读 · 0 评论 -
10 _ 递归:如何用三行代码找到“最终推荐人”?
推荐注册返佣金的这个功能我想你应该不陌生吧?现在很多App都有这个功能。这个功能中,用户A推荐用户B来注册,用户B又推荐了用户C来注册。我们可以说,用户C的“最终推荐人”为用户A,用户B的“最终推荐人”也为用户A,而用户A没有“最终推荐人”。一般来说,我们会通过数据库来记录这种推荐关系。在数据库表中,我们可以记录两行数据,其中actor_id表示用户id,referrer_id表示推荐人id。基于这个背景,我的问题是,给定一个用户ID,如何查找这个用户的“最终推荐人”?原创 2023-11-01 09:03:46 · 170 阅读 · 0 评论 -
08 _ 栈:如何实现浏览器的前进和后退功能?
浏览器的前进、后退功能,我想你肯定很熟悉吧?当你依次访问完一串页面a-b-c之后,点击浏览器的后退按钮,就可以查看之前浏览过的页面b和a。当你后退到页面a,点击前进按钮,就可以重新查看页面b和c。但是,如果你后退到页面b后,点击了新的页面d,那就无法再通过前进、后退功能查看页面c了。假设你是Chrome浏览器的开发工程师,你会如何实现这个功能呢?这就要用到我们今天要讲的“栈”这种数据结构。带着这个问题,我们来学习今天的内容。原创 2023-10-31 06:35:30 · 203 阅读 · 0 评论 -
07 _ 链表(下):如何轻松写出正确的链表代码?
这样代码实现起来就会很繁琐,不简洁,而且也容易因为考虑不全而出错。如何来解决这个问题呢?技巧三中提到的哨兵就要登场了。哨兵,解决的是国家之间的边界问题。同理,这里说的哨兵也是解决“边界问题”的,不直接参与业务逻辑。还记得如何表示一个空链表吗?head=null表示链表中没有结点了。其中head表示头结点指针,指向链表中的第一个结点。如果我们引入哨兵结点,在任何时候,不管链表是不是空,head指针都会一直指向这个哨兵结点。我们也把这种有哨兵结点的链表叫。原创 2023-10-28 14:50:46 · 122 阅读 · 0 评论 -
06 _ 链表(上):如何实现LRU缓存淘汰算法
今天我们来聊聊“链表(Linked list)”这个数据结构。学习链表有什么用呢?为了回答这个问题,我们先来讨论一个经典的链表应用场景,那就是LRU缓存淘汰算法。缓存是一种提高数据读取性能的技术,在硬件设计、软件开发中都有着非常广泛的应用,比如常见的CPU缓存、数据库缓存、浏览器缓存等等。缓存的大小有限,当缓存被用满时,哪些数据应该被清理出去,哪些数据应该被保留?这就需要缓存淘汰策略来决定。原创 2023-10-28 14:50:29 · 93 阅读 · 0 评论 -
05 _ 数组:为什么很多编程语言中数组都从0开始编号?
在大部分编程语言中,数组都是从0开始编号的,但你是否下意识地想过,为什么数组要从0开始编号,而不是从1开始呢?从1开始不是更符合人类的思维习惯吗?你可以带着这个问题来学习接下来的内容。原创 2023-10-28 09:40:41 · 104 阅读 · 0 评论 -
04 _ 复杂度分析(下):浅析最好、最坏、平均、均摊时间复杂度
这里会继续讲四个复杂度分析方面的知识点,最好情况时间复杂度最坏情况时间复杂度平均情况时间复杂度均摊时间复杂度如果这几个概念你都能掌握,那对你来说,复杂度分析这部分内容就没什么大问题了。原创 2023-10-28 09:18:22 · 140 阅读 · 0 评论 -
03 _ 复杂度分析(上):如何分析、统计算法的执行效率和资源消耗?
我们都知道,数据结构和算法本身解决的是“快”和“省”的问题,即如何让代码运行得更快,如何让代码更省存储空间。所以,执行效率是算法一个非常重要的考量指标。那如何来衡量你编写的算法代码的执行效率呢?这里要讲的内容:时间、空间复杂度分析。我个人认为,。原创 2023-10-28 09:15:52 · 165 阅读 · 0 评论 -
01 _ 为什么要学习数据结构和算法?
今天我们就来详细聊一聊,为什么要学习数据结构和算法。原创 2023-10-26 06:44:53 · 251 阅读 · 0 评论