Last updated
Last updated
课程的来源: 课程主要内容:数据结构与算法分析 课程运用语言:Java
这个课有。其中第一个 project 是一个完整的 2024 游戏的实现,很有意思。这些代码作业我会放在另一篇文章里,这篇文章主要是一些我在学习课程理论内容的笔记。
这一部分的内容主要是用 java 实现列表的一些基本操作。并循序渐进的对一个相对粗糙的实现进行改进。
计算机有大量存储信息的内存位置,每一个内存位置都有唯一的地址。在 java 中,每当我们声明一个变量的时候,系统都会为这个变量分配一段内存块,其中的位数刚好足以容纳该类型的变量。 如果声明一个 int
,则会得到一个 32 位的块。如果声明一个 byte
,则会得到一个 8 位的块。
一个基本的列表的实现可以这样实现:
如果我们想要创建一个[1,2,3]
的列表可以这样实现:
接下来我们要实现的是添加size 方法到IntList
类中:
但是这个方法有一个明显的缺陷,如果我们建立了一个空的列表,此方法并不能正确的计算出它的长度。所以我们需要修改一下这个方法:
这个方法使用了一个指针p
来遍历整个列表,并计算它的长度。如果p
不为空,则将totalSize
加 1,并将p
指向p.rest
,继续遍历。直到p
为空,则返回totalSize
。 我们需要这个指针的原因在于我们无法改变 this 的指向,所以我们只能通过指针来遍历整个列表。
上面所实现的
IntList
存在着诸多问题,从根本上看,这是一个裸递归数据结构,它没有任何的迭代器,也没有任何的尾指针。所以我们需要对其进行改进。
首先我们把列表想象成一条长长的链条,每一个链条上的节点都包含一个值以及一个指针,指向下一个节点。我们把这个链表的头节点称为item
,它的指针称为next
。
接下来我们就可以实现一个单向链表SLList
:
可以发现 SLList 的指针指向列表的第一个节点,也就是说我们在实现方法的时候,只需要考虑第一个节点的操作即可。 现在我们可以发现,光是从列表的建立上,SLList
就比IntList
要简单很多。
而getFirst()
方法也很简单:
通过这两个方法,我们可以对比 IntList 和 SLList 创建列表[5,10,15]的区别:
如果我们写这样一段代码:
那么第二节点就会永远指向第一个节点,而第一个节点优惠指向第二个节点,造成了无限循环。这个错误如果要分析的话,那就是我们不应该可以在类外调用 next 指针,而应该通过方法来修改指针。所以我们需要将IntNode
类改成私有类,并提供一些方法来修改指针。
这里可以看到IntNode
类前加了static
,这是因为IntNode
类没有引用外部类的任何变量和方法,所以可以将其声明为静态(static)类。这种做法可以节省内存,因为静态类只会在第一次被加载时被分配内存,而非静态类会在每次实例化时都分配内存。
我们可以用迭代的方法来实现addLast()
方法,用p
指针来遍历整个链表,直到找到最后一个节点,然后将新节点添加到最后。
同样我们用递归的方法实现size()
方法,由于方法中没有运用到任何外部变量,所以可以将其声明为静态方法。
假如我们要计算 100 长度的列表的长度要用 1s,而计算 10000 长度的列表的长度要用 100s,那么我们可以考虑使用缓存来提高效率。我们可以用一个变量来缓存size()
方法的结果,这样可以避免多次计算。
相对于 IntList,SLList 的优势还在于它可以表示空列表,只需要声明一个空的IntNode
即可。
但是同时要考虑的是,当first
为空的时候,addLast()
方法不能正常工作,问题出在while (p.next != null)
,p.next
不存在,所以我们需要对addLast()
方法进行修改。 一种解决方法是增加一个判断语句:
但是我们希望得到更加简洁的方法,这时我们可以创建一个指针和节点之间的中间节点,称为哨兵节点。
同时针对这个方法需要迭代多次的情况,我们可以考虑使用一个变量来记录最后一个节点,这样可以避免多次遍历。
接下来我们来实现双向链表DLList
以对现有的单向链表SLList
进行改进。其实对双向指针的需求来自于删除最后一个节点的实现。因为没有简便的方法能够获取倒数第二个节点。所以我们增加一个向前的指针prev
。
现在的列表只支持整数,2004 年,Java 的创建者在语言中添加了泛型,这将允许您创建保存任何引用类型的数据结构。 我们只需在申明DLList
时添加类型参数即可:
在item
的申明中,我们使用T
来表示任意的引用类型。
我们借用课程里的一张图片来说明两者的区别: 可以看出本质上,SLList 创建了一个中间体来处理节点 IntNode,这个中间体指向第一个节点,包含 addFirst 方法和 getFirst 方法。而 IntList 只是创建了一个节点,包含 first 和 rest 两个属性。
哨兵节点的思想是,创建一个始终存在的节点,他的值并不重要,但是是一个存在的节点。这样我们可以让指针从哨兵节点开始,而不用担心p.next
不存在。这里放一张课程里的图(哨兵:sentinel):
至此,我们已经了解了列表的一些基本实现,包括单向链表、双向链表、哨兵节点、缓存等。有了对列表实现的基本了解后,我们可以去查看不同语言实现列表的方式,并尝试自己实现一些数据结构。 如循环列表的实现,这里放一张示意图: