Problem overview
The function isPalindrome receives head, the first node of a singly linked list, and returns true if the values read the same from front to back and from back to front. Each node holds a digit from 0 to 9, and the list has at least one node.
For [1, 2, 2, 1] the answer is true: the first and last values match, and so do the two in the middle. For [1, 2] the answer is false, because 1 and 2 differ.
Approach: make the list indexable
A palindrome check compares the first value with the last, the second with the second to last, and so on toward the middle. That is easy with an array, where any position can be read directly. A singly linked list is the problem: each node knows only the node after it, so there is no way to start at the tail and step backward.
The lesson's fix is to walk the list once and copy its values into an ArrayList. After that, two indices, one at each end, move toward each other while comparing values. The first mismatch proves the list is not a palindrome. If the indices meet without a mismatch, it is one.
The statement also asks, as a follow-up, for O(n) time and O(1) space. This lesson's copy needs O(n) extra space, so it doesn't meet that follow-up. The usual way to reach it is to reverse the second half of the list in place and compare the two halves. This lesson keeps to the simpler approach, which is easier to get right.
Step by step
- Create
vals, an emptyArrayList<Int>, withval. The variable never points to a different list, only the list's contents change. - Declare
curwithvarand start it athead. It walks the nodes, and its type isListNode?likehead. - Loop while
cur != null. Inside the loop Kotlin treatscuras non-null (a smart cast), so its fields can be read directly. - Add the node's value to
vals. The field is calledval, which is also a Kotlin keyword, so it must be written with backticks:cur.`val`. - Move
curtocur.next. After the last node,nextisnulland the loop ends. - Set
leftto 0, the index of the first value. - Set
righttovals.size - 1, the index of the last value. - Loop while
left < right. When the indices meet or cross, every value has met its mirror. In an odd-length list the middle value has no partner, and it needs no check. - Return
falseon a mismatch. Ifvals[left]differs fromvals[right], one bad pair is enough. - Move the indices inward: add 1 to
leftand subtract 1 fromright. - Return
trueafter the loop. Every pair matched.
The solution in Kotlin
class Solution {
fun isPalindrome(head: ListNode?): Boolean {
val vals = ArrayList<Int>()
var cur = head
while (cur != null) {
vals.add(cur.`val`)
cur = cur.next
}
var left = 0
var right = vals.size - 1
while (left < right) {
if (vals[left] != vals[right]) return false
left += 1
right -= 1
}
return true
}
}
The Kotlin details worth a closer look:
- Backticks around
val. A name that is a keyword can still be used as an identifier if it is wrapped in backticks.cur.valwithout them is a syntax error. ArrayList<Int>in Kotlin.ArrayListis available without an import and is aMutableList<Int>(on the JVM it is Java'sArrayList). It holds boxedIntobjects, so there is some memory overhead compared with anIntArray.vals[left]and!=. The square brackets callget. The!=operator compares the contents of the two values, withequalsbehind it, which forIntis a plain numeric comparison. In Kotlin you don't need a method like Java'sequalsfor this.- Smart cast on
cur. Aftercur != nullsucceeds,cur.`val`andcur.nextcompile without?.becausecuris a localvarthat nothing changes between the check and its use. left += 1. This changes thevar.left++andleft--are also valid Kotlin statements.
Complexity
Time: O(n). The first loop visits each node once. The second loop moves two indices toward each other and runs about n / 2 times.
Space: O(n). vals holds one entry for each node. This is the price of reading the list from both ends.
Edge cases and common mistakes
- A single node.
leftandrightare both 0, so the loop doesn't run and the function returnstrue. A single value reads the same in both directions. - Odd length. In
[1, 2, 3, 2, 1]the loop stops when both indices stand on the 3. Usingleft <= rightwould compare the 3 with itself, which is harmless but unnecessary. - Mismatch near the middle.
[1, 2, 3, 4, 1]matches at the ends and then fails on 2 against 4, so a check of only the first and last values is not enough. - Wrong side of the palindrome.
[1, 0, 0]returnsfalse: the first value 1 is compared with the last, 0, and fails at once. - Comparing nodes instead of values. Comparing nodes themselves would never match, because each node is a distinct object. Compare the stored values.
- Forgetting to advance
cur. Withoutcur = cur.next, the first loop adds the head's value forever.
Testing
[1, 2, 2, 1]givestrue.[1, 2]givesfalse.[1]givestrue.[1, 2, 3, 2, 1]givestrue.[1, 2, 3, 4, 1]givesfalse.
