Vectors vs. Lists in Clojure
In Clojure, two fundamental data structures, lists, and vectors, present developers with distinct capabilities and applications. While superficially similar, they serve unique purposes within Clojure's functional paradigm. We'll discuss the disparities between lists and vectors, providing clarity on their respective strengths and best practices for implementation.
Lists: Agile and immutable
Lists are implemented as linked lists, where each element points to the next. While lists sacrifice random access for immutability and efficiency in certain operations, they excel in scenarios where sequential processing or structural sharing is crucial.
Syntax
Lists are enclosed within parenthesis. For example:
(1 2 3 4)
Staple methods
Access: Accessing an element by index requires traversing the list from the beginning until the desired index is reached. The time complexity of this method is
, where is the number of elements in the list. Insertion: Vectors are immutable by default, so modifying a vector involves creating a new vector with the desired changes.
Append: We traverse the list from the beginning, till the last node. Next, we insert the new element after the last node. The time complexity of this method is
, where is the number of elements in the list. Prepend: We create a new node and point it to the current head. The time complexity of this method is
. Insert at a specified index: We traverse the list to the specified index before performing the insertion. The time complexity of this method is
, where is the specified index.
Deletion:
Delete from the end: We traverse the list to find the second-to-last node. Next, we update the next pointer of this node to NULL. The time complexity of this method is
. Delete from the start: We update the head pointer to point to the next node. The time complexoty of this method is
. Delete at a specified index: Similar to insertion at a specified index, we need to traverse the list to the specified index before performing the deletion. The time complexity of this method is
.
Code example
Here's an example of using the methods described above:
; Accessing an element by index(defn access-element [lst index](println "Accessing element at index" index ": "(nth lst index))); Insert at the end(defn insert-at-end [lst elem](println "List after inserting at the end:"(concat lst (list elem)))); Insert at the head(defn insert-at-head [lst elem](println "List after inserting at the head:"(cons elem lst))); Insert at a specified index(defn insert-at-index [lst index elem](let [before (take index lst)after (drop index lst)](println "List after inserting at index" index ":"(concat before (list elem) after)))); Delete from the end(defn delete-from-end [lst](println "List after deleting from the end:"(butlast lst))); Delete at the head(defn delete-at-head [lst](println "List after deleting at the head:"(rest lst))); Delete at a specified index(defn delete-at-index [lst index](let [before (take index lst)after (drop (inc index) lst)] ; drop one more element to skip the element at the specified index(println "List after deleting element at index" index ":"(concat before after)))); Define the original list(def my-list '(1 2 3 4 5))(println "Original list:" my-list)(access-element my-list 2) ; Perform access operation(insert-at-end my-list 6) ; Perform insertion at the end operation(insert-at-head my-list 0) ; Perform insertion at the head operation(insert-at-index my-list 2 10) ; Perform insertion at a specified index operation(delete-from-end my-list) ; Perform deletion from the end operation(delete-at-head my-list) ; Perform deletion at the head operation(delete-at-index my-list 2) ; Perform deletion at a specified index operation
Explanation
Lines 2–4: The
access-elementfunction takes two arguments,lst(a list) andindex(an integer). It prints the index and the element at that index in the listlstusing thenthfunction.Lines 7–9: The
insert-at-endfunction takes a listlstand an elementelemto be inserted at the end of the list. It prints the list after insertingelemat the end using theconcatfunction to combine the original list and a list containing the new element.Lines 12–14: The
insert-at-headfunction takes a listlstand an elementelemto be inserted at the beginning of the list. It prints the list after insertingelemat the head using theconsfunction.Lines 17–22: The
insert-at-indexfunction takes a listlst, an indexindex, and an elementelemto be inserted at the specified index. It prints the list after insertingelemat the specified index by splitting the original list into two parts (beforeandafter) at the index and then concatenating them with the new element inserted in between.Lines 25–27: The
delete-from-endfunction takes a listlstand prints the list after deleting the last element using thebutlastfunction.Lines 30–32: The
delete-at-headfunction takes a listlstand prints the list after deleting the first element using therestfunction.Lines 35–39: The
delete-at-indexfunction takes a listlstand an indexindexand prints the list after deleting the element at the specified index. It achieves this by splitting the list into two parts (beforeandafter) at the specified index and then concatenating them together, effectively skipping the element at the specified index.
Vectors: Dynamic and indexed containers
Vectors, characterized by their dynamic and indexed nature, excel in scenarios demanding efficient random access and mutable operations. They are implemented as arrays, which allows for efficient random access and fast append/prepend operations. Vectors maintain order and are indexed by integer keys.
Syntax
Vectors are enclosed within square brackets. For example:
[1 2 3 4]
Staple methods
Access: Accessing elements by index takes
time, as vectors use direct addressing. Insertion: Vectors are immutable by default, so modifying a vector involves creating a new vector with the desired changes.
Append: Appending elements to a vector (using the
conjfunction) typically has an amortized constant time complexity of. This means that, on average, appending an element to the end of a vector is a very fast operation and does not depend on the size of the vector. Prepend: We create a new vector with the elements in the desired order. We typically reverse the elements, prepend them one by one, and then reverse the resulting vector to restore the original order. The time complexity of this approach approach is
, where is the size of the original vector. Insert at a specified index: We create a new vector with the desired element inserted at the specified index. The time complexity of this method is
, where is the position at which the element is being inserted.
Deletion:
Delete from the start: We create a new vector that excludes the first element. The overall time complexity of this approach is
. Delete from the end: We create a new vector that excludes the last element. The overall time complexity of this approach is
. Delete at a specified index: We create a new vector that excludes the element at the specified index. The overall time complexity of this approach is
.
Code example
Here's an example of using the methods described above:
; Accessing an element by index(defn access-element [v index](println "Accessing element at index" index ": " (nth v index))); Insert at the end(defn insert-at-end [v elem](println "Vector after inserting at the end:" (conj v elem))); Insert at the head(defn insert-at-head [v elem](println "Vector after inserting at the head:"(into [] (reverse (conj v elem))))); Insert at a specified index(defn insert-at-index [v index elem](println "Vector after inserting at index" index ":"(vec (concat (subvec v 0 index) [elem] (subvec v index))))); Delete from the end(defn delete-from-end [v](println "Vector after deleting from the end:"(subvec v 0 (dec (count v))))); Delete at the head(defn delete-at-head [v](println "Vector after deleting at the head:"(subvec v 1))); Delete at a specified index(defn delete-at-index [v index](println "Vector after deleting at index" index ":"(vec (concat (subvec v 0 index) (subvec v (inc index)))))); Define the original vector(def my-vector [1 2 3 4 5])(println "Original vector:" my-vector)(access-element my-vector 2) ; Perform access operation(insert-at-end my-vector 6) ; Perform insertion at the end operation(insert-at-head my-vector 0) ; Perform insertion at the head operation(insert-at-index my-vector 2 10) ; Perform insertion at a specified index operation(delete-from-end my-vector) ; Perform deletion from the end operation(delete-at-head my-vector) ; Perform deletion at the head operation(delete-at-index my-vector 2) ; Perform deletion at a specified index operation
Explanation
Lines 2–3: The
access-elementfunction takes a vectorvand an indexindexas arguments. It prints the index and the element at that index in the vector using thenthfunction.Lines 6–7: This function accepts a vector
vand an elementelem. It prints the vector after inserting the element at the end using theconjfunction.Lines 10–12: The
insert-at-headfunction takes a vectorvand an elementelem. It prints the vector after inserting the element at the head by first reversing the vector, then usingconjto add the element, and finally converting it back into a vector.Lines 15–17: The
insert-at-indexfunction accepts a vectorv, an indexindex, and an elementelem. It prints the vector after inserting the element at the specified index by splitting the vector into prefix and suffix usingsubvec, inserting the element in between, and then converting it back into a vector.Lines 20–22: The
delete-from-endfunction takes a vectorvas an argument. It prints the vector after deleting the last element usingsubvecto exclude the last element.Lines 25–27: The
delete-at-headfunction accepts a vectorvas input. It prints the vector after deleting the first element usingsubvecto exclude the first element.Lines 30–32: The
delete-at-indexfunction takes a vectorvand an indexindex. It prints the vector after deleting the element at the specified index by concatenating the prefix and suffix of the vector without the element at the index and converting it back into a vector.
Use cases
Vectors are prefered in the following cases:
Accessing elements by index: If your code frequently accesses elements by index, vectors would be preferred due to their efficient indexed access. For example, if you're implementing a matrix or a grid where you need to access elements by row and column indices, vectors would be a better choice.
Efficient updates: If your code requires frequent updates to individual elements, vectors are more efficient due to their mutability at specific indices.
Lists are prefered in the following cases:
Functional transformations: Lists are preferred when you're applying functional transformations like
map,filter, orreduceto sequences. Lists' persistent nature allows for efficient creation of modified versions of the original list without mutating it.Immutable collections: Lists are immutable and persistent, making them suitable for functional programming paradigms where immutability is emphasized. They're particularly useful when you want to avoid unexpected side effects that can arise from mutable data structures.
Sequential processing: If your code primarily involves sequential processing or traversal of elements from start to end, lists are preferable due to their efficient sequential access.
Conclusion
Vectors, with their dynamic and mutable attributes, are well-suited for scenarios necessitating rapid access and mutable operations. Conversely, lists, with their immutable and sequential characteristics, thrive in scenarios demanding structural sharing and immutable data manipulation. By understanding their nuances, developers can use these data structures to optimize data manipulation tasks effectively.
Free Resources