每周完成一个 ARTS:
Algorithm: 每周至少做一个 LeetCode 的算法题
Review: 阅读并点评至少一篇英文技术文章
Tips: 学习至少一个技术技巧
Share: 分享一篇有观点和思考的技术文章

题图:君の名は

Contents

Algorithm

234. Palindrome Linked List

题目:234. Palindrome Linked List

难度:Easy

题意:Given a singly linked list, determine if it is a palindrome.

示例 1:

Input: 1->2
Output: false

示例 2:

Input: 1->2->2->1
Output: true

Follow up:

Could you do it in O(n) time and O(1) space?

解法 1:

我们首先试一个稍微简单的方法。

判断回文问题,核心思想是从中间向两端扩展。我们可以使用双指针法找到中间节点,慢指针此时经过的部分就是前半部分,我们可以把这些部分保存起来,再与慢指针之后将要经过的部分作比较,就可以判断这个单链表是否为回文链表了。

代码:

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def isPalindrome(self, head: ListNode) -> bool:
        slow, fast = head, head
        stack = []

        while fast and fast.next:
            stack.append(slow.val)
            slow = slow.next
            fast = fast.next.next
        
        # When the length of the linked list is an odd number, we need to forward slow by one step
        if fast:
            slow = slow.next
        
        while slow:
            top = stack.pop()

            if top != slow.val:
                return False
            
            slow = slow.next
        
        return True

注意一个问题,如果链表的长度是奇数,我们找到的中间节点是正好的中间节点,为了往后继续比较,我们需要把慢指针再往后移一位,再与之前保存的数组相比较。

这样的解法时间复杂度和空间复杂度都是 O(n)。

解法 2:

上面第一种解法,虽然是以数组的形式存储的前半部分,但本质上还是反转前半部分的链表,我们还可以写一个反转链表的函数,来帮助我们反转,比较方便的办法是反转后半部分:

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def isPalindrome(self, head: ListNode) -> bool:
        slow, fast = head, head

        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
        
        # When the length of the linked list is an odd number, we need to forward slow by one step
        if fast:
            slow = slow.next
        
        left, right = head, self.reverse(slow)

        while right:
            if left.val != right.val:
                return False
            left = left.next
            right = right.next

        return True
    
    def reverse(self, head: ListNode) -> ListNode:
        pre, cur = None, head
        while cur:
            next = cur.next
            cur.next = pre
            pre = cur
            cur = next
        return pre

这样我们就只需要保存反转之后后半部分链表的头结点即可,实现 O(1) 的空间复杂度。

Review

100 Tips for a Better Life

本周要分享的英文文章是 LessWrong! 上的一篇文章,《100 Tips for a Better Life》。文章中,作者 Ideopunk 给出了他认为能够给我们生活带来提升的 100 条建议,有如下若干个方向:物品、烹饪、生产力、身体、成功、理性、自我提升、危险行为、杂项、人际关系、同理心、快乐。

我随便截取几个放到这里:

文章中大部分建议都给出了相应的参考链接,有理有据,值得一读。

Tips

Notion 的 Indify 插件

最近发现一个 Notion 的插件,叫 Indify,它为 Notion 提供了 8 个实用的小组件,可以让你在你的 Notion 页面中嵌入:时间、倒计时、计数器、谷歌日历、增强的图片库、生命进度条、名人名言、天气这八个小组件。

我目前是用了倒计时和天气这两个小组件:

Indify 展示

你可以在 Indify.co 注册你的账号并生成自己的小组件,然后在 Notion 中嵌入它们。

Share

使用 TikZ 在 LaTeX 中画流程图

最近写论文要画流程图,之前一直是使用「PPT 画图 -> 导出为 PDF -> 嵌入图像」这样的流程,但有时会遇到导出图像的字体和 PPT 中的字体大小不一致的情况,于是想着试试用 TikZ 在 LaTeX 中直接画图的方式。

TikZ 是 LaTeX 的一个包,用它可以制作各种复杂但高质量的图。这里我简单介绍一下如何用 TikZ 来画流程图。

要使用 TikZ 画流程图,第一步是调用 tikz 包,同时调用 TikZ 的两个标准库 shapes.geometricarrows

\usepackage{tikz}
\usetikzlibrary{shapes.geometric, arrows}

第二步,我们需要对流程图的一些基本元素的形状、颜色等属性进行定义:

\tikzstyle{startstop} = [rectangle, rounded corners, minimum width=3cm, minimum height=0.5cm, text centered, draw=black, fill=red!30]

上面这条语句,我们将「开始结束」模块定义如下:圆角矩形,最小宽度 3 厘米,最小高度 0.5 厘米,文本居中显示,黑色边框,30% 浓度红色(简单理解)填充。

\tikzstyle{io} = [trapezium, trapezium left angle=70, trapezium right angle=110, minimum width=3cm, minimum height=0.5cm, text centered, draw=black, fill=blue!30]

上面这条语句,我们定义了「输入输出」模块:梯形,左边倾斜角 70 度,右边倾斜角 110 度(这样我们就得到一个平行四边形),最小宽度 3 厘米,最小高度 0.5 厘米,文本居中显示,黑色边框,30% 浓度蓝色填充。

\tikzstyle{process} = [rectangle, minimum width=3cm, minimum height=0.5cm, text centered, draw=black, fill=orange!30]

上面这条语句,我们定义了「步骤」模块:矩形,最小宽度 3 厘米,最小高度 0.5 厘米,文本居中显示,黑色边框,30% 浓度橙色填充。

\tikzstyle{decision} = [diamond, minimum width=3cm, minimum height=0.5cm, text centered, draw=black, fill=green!30]

上面这条语句,我们定义了「判断」模块:菱形,最小宽度 3 厘米,最小高度 0.5 厘米,文本居中显示,黑色边框,30% 浓度绿色填充。

\tikzstyle{arrow} = [thick,->,>=stealth]

这条语句定义了「箭头」:粗,箭头方向是 ->,箭头类型 stealth。

上面两步需要在 \begin{document} 之前完成。

完成上面两步之后,在正文部分,我们就可以开始画图了,下面介绍一下基本的画法。

我们需要使用 \begin{tikzpicture} 开始画图,注意这个语句最好是放在 \begin{figure} 内部,这样可以进行居中操作、添加描述与标签。下面是一个常用的格式:

\begin{figure}
  \centering
  \begin{tikzpicture}[node distance=2cm]
  ...
  \end{tikzpicture}
  \caption{这是一个图片的描述}
  \label{fig:1}

上面的格式完成之后,我们就可以开始添加元素了,我们先添加几个常用的节点:

\begin{figure}
  \centering
  \begin{tikzpicture}[node distance=2cm]
    \node (start) [startstop] {Start};
    \node (proc1) [process, below of=input] {Process 1};
    \node (dec) [decision, below of=proc1] {Decision};
    \node (proc2a) [process, right of=dec] {Process 2a};
    \node (proc2b) [process, below of=dec] {Process 2b};
    \node (output) [io, below of=proc2b] {Output};
    \node (stop) [startstop, below of=output] {Stop};
  \end{tikzpicture}
  \caption{Flowchart Example.}
  \label{fig:1}
\end{figure}

排版的结果如下:

节点示意 1

我们可以看到,添加节点的语句很容易懂:

\node (node 名称) [node 类型, node 的相对位置, 其他参数] {node 中的文字};

但是有一个问题,我们的 Process 2a 与 Decision 重叠了,这个时候,我们可以在其他参数 中加上 xshift=2cm 来将其往右移动 2 厘米,如果是 xshift=-2cm 就是向左移动 2 厘米了,yshift 的用法同理。这样对应的代码就变成:

\node (proc2a) [process, right of=dec, xshift=2cm] {Process 2a};

而节点的位置也正常了:

节点示意 2

下面我们来添加箭头。我们只需要在 node 下面继续添加 arrow 即可:

\draw [arrow] (start) -- (input);
\draw [arrow] (input) -- (proc1);
\draw [arrow] (proc1) -- (dec);
\draw [arrow] (dec) -- node[anchor=south] {no} (proc2a);
\draw [arrow] (dec) -- node[anchor=east] {yes} (proc2b);
\draw [arrow] (proc2a) -- (proc1);
\draw [arrow] (proc2b) -- (output);
\draw [arrow] (output) -- (stop);

添加箭头的语句也很容易明白:

\draw [arrow] (起点) -- (终点);

而如果要在箭头旁边添加文字说明的话,就要在 --(终点) 后面添加 node[anchor=箭头的相对文字的位置(东南西北)] {文字内容}

\draw [arrow] (dec) -- node[anchor=south] {no} (proc2a);
\draw [arrow] (dec) -- node[anchor=east] {yes} (proc2b);

箭头示意 1

最后一点,我们可以看到,从 Process 2a 到 Process 1 的箭头是一条直线,并不美观,我们可以把它改成折线。

只需要把 -- 改成 |- 就可以了,它表示箭头会先竖着走,再横着走:

\draw [arrow] (proc2a) |- (proc1);

最终的代码如下:

\begin{figure}
  \centering
  \begin{tikzpicture}[node distance=2cm]
    \node (start) [startstop] {Start};
    \node (proc1) [process, below of=input] {Process 1};
    \node (dec) [decision, below of=proc1] {Decision};
    \node (proc2a) [process, right of=dec] {Process 2a};
    \node (proc2b) [process, below of=dec] {Process 2b};
    \node (output) [io, below of=proc2b] {Output};
    \node (stop) [startstop, below of=output] {Stop};

    \draw [arrow] (start) -- (input);
    \draw [arrow] (input) -- (proc1);
    \draw [arrow] (proc1) -- (dec);
    \draw [arrow] (dec) -- node[anchor=south] {no} (proc2a);
    \draw [arrow] (dec) -- node[anchor=east] {yes} (proc2b);
    \draw [arrow] (proc2a) |- (proc1);
    \draw [arrow] (proc2b) -- (output);
    \draw [arrow] (output) -- (stop);
  \end{tikzpicture}
  \caption{Flowchart Example.}
  \label{fig:1}
\end{figure}

最后的生成的流程图如下:

箭头示意 2

WeChat QRCode