Skip to content

Commit

Permalink
Linked list clean up. Add explainations to problems, fix titles and t… (
Browse files Browse the repository at this point in the history
  • Loading branch information
spring1843 authored Jan 15, 2025
1 parent 6be0c92 commit 18820b8
Show file tree
Hide file tree
Showing 11 changed files with 88 additions and 24 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Welcome to **Data Structures and Algorithms in Go**! 🎉 This project is design
* [Join Two Sorted Linked Lists](./linkedlist/join_sorted_lists_test.go)
* [Keep Repetitions](./linkedlist/keep_repetitions_test.go)
* [Copy Linked List with Random Pointer](./linkedlist/copy_linklist_with_random_pointer_test.go)
* [Implement LRU Cache](./linkedlist/lru_cache_test.go)
* [LRU Cache](./linkedlist/lru_cache_test.go)
* [Stacks](./stack/README.md)
* [Max Stack](./stack/max_stack_test.go)
* [Balancing Symbols](./stack/balancing_symbols_test.go)
Expand Down
2 changes: 1 addition & 1 deletion heap/merge_sorted_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestMergeSortedLists(t *testing.T) {
for i, test := range tests {
nodes := []*linkedlist.Node{}
for _, sortedLinkedList := range test.sortedLinkedLists {
nodes = append(nodes, linkedlist.Unserialize(sortedLinkedList))
nodes = append(nodes, linkedlist.Deserialize(sortedLinkedList))
}
got := linkedlist.Serialize(MergeSortedLists(nodes))
if got != test.merged {
Expand Down
8 changes: 4 additions & 4 deletions linkedlist/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func addToFront(node *node) {
}
```

A pointer is typically stored for the first and last items in a singly linked list. Adding items to the front or back of the list is a constant-time operation. However, deleting the last item can be challenging, as the last item's pointer needs to be updated to the second-to-last item. This is where referencing the last item in each node proves useful. In contrast, doubly linked lists maintain pointers to the previous and next nodes, making deletion operations less expensive.
A pointer is typically stored for the first and sometimes the last items in a singly linked list. Adding items to the front or back of the list is a constant-time operation. However, deleting the last item can be challenging, as the last item's pointer needs to be updated to the second-to-last item. This is where referencing the last item in each node proves useful. In contrast, doubly linked lists maintain pointers to the previous and next nodes, making deletion operations less expensive.

The Go standard library contains an implementation of [doubly linked lists](https://golang.org/pkg/container/list/). In the following example, numbers from 1 to 10 are added to the list. Even numbers are removed, and the resulting linked list containing odd numbers is printed.

Expand Down Expand Up @@ -81,11 +81,11 @@ func main() {

Adding new items to the front or back of the linked list has the time complexity of O(1).

Deleting the first item is also O(1). Deleting the last item in a singly linked list is O(n), because the node before the last node must be found, and for that, every node must be visited. Deleting the last item can be done in O(1) time in a doubly linked list since nodes are connected to the previous and next nodes.
Deleting the first item is also O(1). Deleting the last item in a singly linked list is O(n), because the node before the last node must be found, and for that, every node must be visited. Deleting the last item can be done in O(1) time in a doubly linked list since nodes are connected to the previous and next nodes, so we can simply find the node before the last node and remove its reference to the next node.

## Application

Linked lists can be useful where the order of items matters, especially if there is a need for flexible reordering of the items or having current, next, and previous items. Music players for example have playlists that play tracks in a predetermined order. At any given time, one track is playing while changing the current track with the previous or next ones is possible.
Linked lists can be useful where the order of items matters, especially if there is a need for flexible reordering of the items or having current, next, and previous items. For example music players are a good candidate to implement using linked lists because they have playlists that play tracks in a predetermined order. At any given time, one track is playing while changing the current track with the previous or next ones is possible.

## Rehearsal

Expand All @@ -94,4 +94,4 @@ Linked lists can be useful where the order of items matters, especially if there
* [Join Two Sorted Linked Lists](./join_sorted_lists_test.go), [Solution](./join_sorted_lists.go)
* [Keep Repetitions](./keep_repetitions_test.go), [Solution](./keep%20repetitions.go)
* [Copy Linked List with Random Pointer](./copy_linklist_with_random_pointer_test.go), [Solution](./copy_linklist_with_random_pointer.go)
* [Implement LRU Cache](./lru_cache_test.go), [Solution](./lru_cache.go)
* [LRU Cache](./lru_cache_test.go), [Solution](./lru_cache.go)
17 changes: 15 additions & 2 deletions linkedlist/copy_linklist_with_random_pointer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,23 @@ const (
/*
TestCopyLinkedListWithRandomPointer tests solution(s) with the following signature and problem description:
type RandomNode struct {
Val int
Next *RandomNode
Random *RandomNode
}
func CopyLinkedListWithRandomPointer(head *RandomNode) *RandomNode
Given a singly connected linked list in which each node may optionally be connected to another node in
random order, return a deep copy of the linked list.
A deep copy of a linked list is a new linked list with the same values as the original linked list in
which each node is a new node with a new memory address.
In the string representation below "1:nil->2:1->3:4->4:nil" is a linked list with 4 nodes:
* Nodes 1,2,3,4 are sequentially connected to each other with the Next pointer.
* Node 2 is randomly connected to node 1, and node 3 is randomly connected to node 4.
*/
func TestCopyLinkedListWithRandomPointer(t *testing.T) {
tests := []struct {
Expand All @@ -33,7 +46,7 @@ func TestCopyLinkedListWithRandomPointer(t *testing.T) {
}

for i, test := range tests {
list := unserializeRandomNode(test.list)
list := deserializeRandomNode(test.list)
deepCopy := CopyLinkedListWithRandomPointer(list)

if list != nil {
Expand Down Expand Up @@ -89,7 +102,7 @@ func indicesMap(node *RandomNode) map[*RandomNode]int {
return indices
}

func unserializeRandomNode(stringRepresentation string) *RandomNode {
func deserializeRandomNode(stringRepresentation string) *RandomNode {
if stringRepresentation == "" {
return nil
}
Expand Down
4 changes: 3 additions & 1 deletion linkedlist/join_sorted_lists_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ TestJoinTwoSortedLinkedLists tests solution(s) with the following signature and
func JoinTwoSortedLinkedLists(l1, l2 *Node) *Node
Given two sorted linked lists of integers, merge them into one sorted linked list.
For example if given 1->4->6 and 2->3->5->7, the output should be 1->2->3->4->5->6->7.
*/
func TestJoinTwoSortedLinkedLists(t *testing.T) {
tests := []struct {
Expand All @@ -26,7 +28,7 @@ func TestJoinTwoSortedLinkedLists(t *testing.T) {
}

for i, test := range tests {
got := Serialize(JoinTwoSortedLinkedLists(Unserialize(test.list1), Unserialize(test.list2)))
got := Serialize(JoinTwoSortedLinkedLists(Deserialize(test.list1), Deserialize(test.list2)))
if got != test.joined {
t.Fatalf("Failed test case #%d. Want %s got %s", i, test.joined, got)
}
Expand Down
6 changes: 5 additions & 1 deletion linkedlist/keep_repetitions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ TestKeepRepetitions tests solution(s) with the following signature and problem d
Given a linked list of sorted integers, create a copy of the list that contains one example of
each repeated item.
For example if the linked list is 1->1->4->4->6->6->7, the output should be 1->4->6 because
1,4,6 are items that are repeated in this list and 7 is not repeated.
*/
func TestKeepRepetitions(t *testing.T) {
tests := []struct {
Expand All @@ -21,11 +24,12 @@ func TestKeepRepetitions(t *testing.T) {
{"1->4->4->4->6", "4"},
{"1->1->4->4->4->6", "1->4"},
{"1->1->4->4->4->6->6", "1->4->6"},
{"1->1->4->4->6->6->7", "1->4->6"},
{"1->1->4->4->4->6->7->7", "1->4->7"},
}

for i, test := range tests {
got := Serialize(KeepRepetitions(Unserialize(test.list)))
got := Serialize(KeepRepetitions(Deserialize(test.list)))
if got != test.solution {
t.Fatalf("Failed test case #%d. Want %s got %s", i, test.solution, got)
}
Expand Down
11 changes: 11 additions & 0 deletions linkedlist/lru_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package linkedlist

import (
"container/list"
"fmt"
"strings"
)

type (
Expand Down Expand Up @@ -50,3 +52,12 @@ func (cache *lruCache) put(key int, value int) {
cache.list.Remove(cache.list.Front())
}
}

// String represents the cache as a string.
func (cache *lruCache) String() string {
var pairs []string
for e := cache.list.Front(); e != nil; e = e.Next() {
pairs = append(pairs, fmt.Sprintf("%d:%d", e.Value.(*element).key, e.Value.(*element).value))
}
return strings.Join(pairs, ",")
}
32 changes: 28 additions & 4 deletions linkedlist/lru_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,30 @@ import "testing"
TestLRU tests solution(s) with the following signature and problem description:
func get(key int) int
func put(key int, value int) {
func put(key int, value int)
Implement a least recently used cache with integer keys and values, where the
least recently used evicted upon insertion when cache is at full capacity.
Implement a least recently used cache with integer keys and values, where the least
recently used key is evicted upon insertion when the cache is at full capacity.
For example if we have a cache with capacity 2, and we perform:
put(0, 1) // put key 0 with value 1
put(1, 2)
get(1) // get value with key 1
put(2, 3)
When putting 2,3, the cache is at capacity and it needs to evict. Since we used key 1 by
getting it, the [1,2] key value pair which is the least used value will be evicted.
What remains in the cache
is [1:2] and [2:3].
*/
func TestLRU(t *testing.T) {
tests := []struct {
capacity int
puts []int // n(i) element is key, n(i+1) element is value
gets []int // n(i) element is the key to get, n(i+1) element is the expected value
}{
{1, []int{}, []int{1, -1}},
{1, []int{}, []int{1, -1}}, // -1 means not found
{1, []int{2, 1, 1, 1, 2, 3, 4, 1}, []int{2, -1, 4, 1}},
{2, []int{2, 1, 1, 1, 2, 3, 4, 1}, []int{1, -1, 2, 3, 4, 1}},
{3, []int{2, 1, 1, 1, 2, 3, 4, 1}, []int{1, 1, 2, 3, 4, 1}},
Expand All @@ -38,4 +50,16 @@ func TestLRU(t *testing.T) {
}
}
}

lru := newLRU(2)
lru.put(0, 1)
lru.put(1, 2)
lru.get(1)
lru.put(2, 3)

got := lru.String()
want := "1:2,2:3"
if got != want {
t.Fatalf("want %s, got %s", want, got)
}
}
9 changes: 7 additions & 2 deletions linkedlist/reverse_in_place_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ import "testing"
/*
TestReverseLinkedList tests solution(s) with the following signature and problem description:
type Node struct {
Val int
Next *Node
}
func ReverseLinkedList(head *Node) *Node
Reverse a given linked list in place.
Reverse a given linked list in place. For example if the linked list is 1->2->3 return 3->2->1.
*/
func TestReverseLinkedList(t *testing.T) {
tests := []struct {
Expand All @@ -20,7 +25,7 @@ func TestReverseLinkedList(t *testing.T) {
}

for i, test := range tests {
got := Serialize(ReverseLinkedList(Unserialize(test.list)))
got := Serialize(ReverseLinkedList(Deserialize(test.list)))
if got != test.reversed {
t.Fatalf("Failed test case #%d. Want %s got %s", i, test.reversed, got)
}
Expand Down
4 changes: 2 additions & 2 deletions linkedlist/serialization.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ func Serialize(node *Node) string {
return strings.TrimSuffix(output, separator)
}

// Unserialize solves the problem in O(n) time and O(1) space.
func Unserialize(stringRepresentation string) *Node {
// Deserialize solves the problem in O(n) time and O(1) space.
func Deserialize(stringRepresentation string) *Node {
if stringRepresentation == "" {
return nil
}
Expand Down
17 changes: 11 additions & 6 deletions linkedlist/serialization_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,28 @@ package linkedlist
import "testing"

/*
TestSerializeAndUnserializeLinkedList tests solution(s) with the following signature and problem description:
TestSerializeAndDeserializeLinkedList tests solution(s) with the following signature and problem description:
type Node struct {
Val int
Next *Node
}
func Serialize(node *Node) string
func Unserialize(stringRepresentation string) *Node
func Deserialize(stringRepresentation string) *Node
Write a function that turns a linked list into a string representation, and then a function that turns that
string representation to an actual linked list.
Write a function that turns a linked list into a string representation (Serialize), and then a function
that turns that string representation to an actual linked list (Deserialize).
*/
func TestSerializeAndUnserializeLinkedList(t *testing.T) {
func TestSerializeAndDeserializeLinkedList(t *testing.T) {
tests := []string{
"",
"1",
"1->2",
"1->2->3->4->2->1",
}
for i, test := range tests {
got := Serialize(Unserialize(test))
got := Serialize(Deserialize(test))
if got != test {
t.Fatalf("Failed test case #%d. Want %#v got %#v", i, test, got)
}
Expand Down

0 comments on commit 18820b8

Please sign in to comment.