LeetCode 2619 - Array Prototype Last
The problem asks us to enhance all JavaScript arrays so that they have a convenient method, last(), which returns the last element of the array. If the array is empty, it should return -1.
Difficulty: 🟢 Easy
Topics: —
Solution
Problem Understanding
The problem asks us to enhance all JavaScript arrays so that they have a convenient method, last(), which returns the last element of the array. If the array is empty, it should return -1. The input array is assumed to be a valid JSON array, meaning it can contain any valid JSON data type including numbers, strings, objects, null, or other arrays. The output is a single value: either the last element in the array or -1 if there are no elements.
Key points to note are that arrays can be empty, arrays may contain heterogeneous types, and the length of the array can be up to 1000 elements. Edge cases include an empty array and arrays where the last element is null or another falsy value, which should still be returned as-is rather than converted to -1.
The problem essentially asks for a prototype modification in JavaScript, allowing any array to call .last() directly.
Approaches
The brute-force approach would be to manually check the array length every time you want the last element and access it with arr[arr.length - 1]. While this works correctly, it requires repeating the same logic every time you need the last element, which is repetitive and error-prone.
The optimal solution leverages JavaScript's prototype system. By adding a method directly to Array.prototype, all arrays automatically gain the last() method. This is both concise and efficient because the logic is centralized, and each call is effectively constant-time access.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(1) per call | O(1) | Manually check arr.length and access arr[arr.length - 1] each time. Repetitive and manual. |
| Optimal | O(1) per call | O(1) | Add a last() method to Array.prototype so all arrays gain this functionality. Centralized and reusable. |
Algorithm Walkthrough
- Identify that JavaScript arrays inherit from
Array.prototype. This allows us to extend functionality to all arrays at once. - Define a new function called
last()that can be attached toArray.prototype. - Within the
last()function, first check ifthis.lengthis zero. This ensures we handle empty arrays. - If the array is empty (
this.length === 0), return-1. - Otherwise, return
this[this.length - 1], which is the last element of the array. - Once attached, any array can call
array.last()directly, and the logic will correctly handle empty arrays and return the last element otherwise.
Why it works: The method works because Array.prototype is the shared prototype for all arrays. The function uses this to refer to the calling array, ensuring that the check and access are always relative to the array instance. The check for length === 0 guarantees that empty arrays return -1 rather than producing an undefined result.
Python Solution
Python does not allow direct modification of built-in list methods globally like JavaScript, but we can define a helper function:
from typing import List, Any
def last(arr: List[Any]) -> Any:
if not arr:
return -1
return arr[-1]
Explanation: The function last takes a Python list as input. It checks if the list is empty using if not arr. If empty, it returns -1; otherwise, it accesses the last element using Python's negative indexing arr[-1]. This replicates the behavior required in JavaScript.
Go Solution
In Go, we cannot modify slices directly, but we can define a function that accepts a slice:
package main
func Last(arr []any) any {
if len(arr) == 0 {
return -1
}
return arr[len(arr)-1]
}
Explanation: Go slices are analogous to arrays in JavaScript. The function checks if the slice length is zero, returning -1 in that case. Otherwise, it returns the last element using arr[len(arr)-1]. The any type allows heterogeneous slice elements.
Worked Examples
Example 1: nums = [null, {}, 3]
| Step | Operation | Result |
|---|---|---|
| 1 | Check if nums.length === 0 |
False |
| 2 | Return nums[nums.length - 1] |
3 |
Example 2: nums = []
| Step | Operation | Result |
|---|---|---|
| 1 | Check if nums.length === 0 |
True |
| 2 | Return -1 |
-1 |
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(1) | Accessing the last element or checking length is constant time. |
| Space | O(1) | No extra memory is used beyond the function call. |
The reasoning is straightforward: the solution does a simple length check and array access, both of which are constant time and space operations. Modifying the prototype itself is a one-time operation with negligible cost.
Test Cases
# test cases
assert last([None, {}, 3]) == 3 # standard case with multiple elements
assert last([]) == -1 # empty array returns -1
assert last([1]) == 1 # single element array
assert last([0, False, ""]) == "" # last element can be falsy
assert last([None]) == None # last element is None
assert last([1,2,3,4,5]) == 5 # typical increasing array
| Test | Why |
|---|---|
[None, {}, 3] |
Standard heterogeneous array |
[] |
Empty array edge case |
[1] |
Single-element array |
[0, False, ""] |
Falsy values at the end |
[None] |
Single-element array with null |
[1,2,3,4,5] |
Normal increasing integer array |
Edge Cases
An empty array is a key edge case because naive implementations might try to access arr[arr.length - 1] without checking length, resulting in undefined rather than -1. Our implementation explicitly checks length first.
Arrays where the last element is null or a falsy value like 0 or "" could cause errors if someone used a truthy check instead of explicit indexing. By directly returning this[this.length - 1], the code correctly returns the actual last element without converting it.
Single-element arrays are also important because accessing arr[arr.length - 1] should return the only element and not fail or return -1. Our implementation handles this naturally through the length check.