在前三天的刷题实战中,我们一起实作了线性的链结串列和非线性的树相关的题目。其实你仔细回顾一下,你会发现其实这三题的解法都有一些共同的观点,像是都会利用「回圈迭代」的暴力法和「递回法」来实现:
今天我们想要从「递回法」的概念延伸,介绍一种称为「堆叠(Stack)」的抽象资料结构。递回法其实是链结串列(Linked List)或树(Tree)当中典型的方法,其概念是「对资料结构中每一个节点做类似的操作,持续执行直到每一个点都跑过一轮」。但这两种资料结构的差别在於链结串列是单一且唯一方向的,但是树的结构有两种以上的分支,因此可能会有不同的遍历找法。
递回策略是一个 Function 中含有自我呼叫的行为,这个过程会将执行到一半的结果先暂存起来,先执行下一个 Function 直到终止条件之後再一个一个 return 回上层。递回中的「将执行到一半的结果先暂存起来」是由作业系统处理,当递回太深的时候可能就会造成记忆体的问题。
但是在资料结构中有一种抽象资料结构(ADT,Abstract Data Type)称为「堆叠」,堆叠是指符合先进後出(First In, Last Out)特性的有序线性容器(如下图)。但抽象资料结构的定义就是你不用管内部怎麽实作,所以一般来说可以利用阵列或是列表来实现都可以。
因为堆叠有「先进後出」特性,所以恰恰就符合倒序的概念(Reverse),只要利用 Stack 依序取出 Linked List 中的每个元素,取出的顺序就会是倒序的结果。
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head: return None
stack = []
while head:
stack.append(head)
head = head.next
current = stack.pop()
head = current
while stack:
current.next = stack.pop()
current = current.next
current.next = None
return head
var reverseList = function(head) {
if(head === null) return head;
let stack = [];
while(head !== null){
stack.push(head);
head = head.next;
}
let current = stack.pop();
head = current;
while(stack.length > 0){
current.next = stack.pop();
current = current.next;
}
current.next = null;
return head;
};
本题考的是二元搜寻树当中的元素是否符合目标值,直觉的方法就是利用二元搜寻树「左小右大」的特性一个一直找。这个是这里的一个一直找会受限於树的非线性而有不同的找法,这种方法会先将树中的资料存到 Stack 中,取出的过程进行检查。补充一下,用 Stack 去遍历树中每一个点的找法称为「深度优先搜寻(DFS,Depth-First Search)」,後续的文章会提到。
class Solution:
def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
if not root: return root
stack = [root];
while stack:
node = stack.pop();
if node.val == val:
return node
if node.val > val and node.left:
stack.append(node.left);
if node.val < val and node.right:
stack.append(node.right);
return None;
var searchBST = function(root, val) {
if (!root) return root;
const stack = [root];
while (stack.length) {
const node = stack.pop();
if (node.val === val) return node;
if (node.val > val) {
if (node.left) stack.push(node.left);
} else {
if (node.right) stack.push(node.right);
}
}
return null;
};
延续前一题的想法,遍历是指要走过树中每一点的方式,但是因为树中每一个节点可能会有左/右分支,因此会有「要先找左边还是右边」、「要找到最底还是先把周围的找完」等等的衍生议题。常见的遍历方法可以分为「前序」、「中序」跟「後序」,依据 Root 被找到的先後做排序,当中的「前序遍历(Preorder Traversal)」其实就是利用深度优先搜寻去遍历树的结果。
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root: return []
res, stack = [], [root]
while stack:
cur = stack.pop()
res.append(cur.val)
if cur.right: stack.append(cur.right)
if cur.left: stack.append(cur.left)
return res
var preorderTraversal = function(root) {
if(!root) return [];
let res = [], stack = [root];
while( stack.length > 0 ){
let cur = stack.pop();
res.push(cur.val);
if(cur.right) stack.push(cur.right);
if(cur.left) stack.push(cur.left);
}
return res;
};
如同我们在前面讲到「刻意优化」的重要性,当写完一个题目之後可以三个小时、三天,甚是是三周之後再回来看同一个题目,把它当成新的题目重新解解看,反而更容易写出不一样的解法。所以这几天我们就带大家一起体验这个过程,从认识新的资料结构开始,尝试直觉解到递回优化,经过三天之後再导入堆叠的实现,这就是一个完整的刷题「优化」体验。
嗨,大家好!我是维元,一名擅长网站开发与资料科学的双栖工程师,斜杠於程序社群【JSDC】核心成员及【资料科学家的工作日常】粉专经营。目前在 ALPHACamp 担任资料工程师,同时也在中华电信、工研院与多所学校等单位持续开课。拥有多次大型技术会议讲者与竞赛获奖经验,持续在不同的平台发表对 #资料科学、 #网页开发 或 #软件职涯 相关的分享,邀请你加入 订阅 接收最新资讯。
>>: [DAY14] 在 Azure Machine Learning 里 Label data(下)
《赖田捕手:番外篇》第 38 天:用 Netlify Functions 布署 Line Bot 从...
後来发现 , 之前说明了 Vue . React Component 如何变成 Web Compon...
HTML元素的组成 以下图为例 通常都有一个起始标签<>和一个结束标签< / &g...
适用人员: 技术人员。 适用法规: 资通安全责任等级分级办法 技术面分类提要 网路架构 端点安全防护...
一直很犹豫要不要把今天这篇和昨天那篇合在一起,最後还是分开了( ̄3 ̄)╭,觉得分开整体看起来比较统一...