Remember that
$$\text{Data Structure}=\text{Organization}+\text{Storage}+\text{Operation},$$base on which we develop algorithms to solve problems.
Organization & Logical Structure
The Organization, equivalently, the logical structure of data, is the intrinsic property of data, which determines the relationship between data elements and the operations on them, and it is independent from the storage.
A simple example is the hash table, which realizes the mapping
$$f:\text{key} \mapsto \text{value}$$by storing entries at locations $\{\text{Hash}(\text{key})\}$. The hash function provides only a storage structure; it does not impose any logical relationship between elements. Now consider the case where keys are taken from $([n])$ and values from a given data type
$$f: k\mapsto a_k.$$If a predecessor–successor relationship is defined(that is, we can traverse from one element to the next by $f(a_k.\texttt{next}))$, the logical structure is a linear list. If not, the logical structure is merely a set, even though the data is stored via $([n])$, which is a sequential storage structure.
This illustrates that logical structure is intrinsic, while storage structure is extrinsic. The former is determined by the nature of the data, while the latter is determined by the implementation of the data structure.
Density of Storage Structure
Linear List
Linear list is a finite tuple of $n$ ($n\ge 0$) data elements
$$L=(a_1,a_2,\ldots,a_n),$$with the concept of a head $a_1$ and a tail $a_n$, where each element has a unique predecessor and a unique successor, except for the head and tail. Fundemental operations on a linear list include
- Initialize/Destroy/Print
- Length/Empty
- Locate/Search/Insert/Delete
Generally speaking, linked lists(a storage structure) are more likely to illustrate the logical structure of a linear list.
Sequential List
A sequential list stores a linear list in a contiguous block of memory. Each element $a_i$ is placed at a physical address
$$\texttt{Loc}(a_i)=\texttt{Loc}(a_1)+(i-1)\cdot \texttt{size}(a_i).$$It can be statically allocated (fixed size) or dynamically allocated (resizable) as follows, taking a dynamic array $L$ consisting of $\texttt{ElemType}$ elements as an example:
| |
The maximal capacity $\texttt{InitSize}$ is fixed at initialization. If requiring more space, a new larger array must be allocated, and the old data copied over.
Singly Linked List
A singly linked list stores each element in a node consisting of two fields:
$$\texttt{node}=[\texttt{data}\mid\texttt{next}].$$The list $L$ is accessed via a head pointer $L$ pointing to $a_1$; the last node has $\texttt{next}=\texttt{null}$.
$$L\xrightarrow{\texttt{*}}a_1\xrightarrow{\texttt{next}}a_2\xrightarrow{\texttt{next}}\cdots\xrightarrow{\texttt{next}}a_n\xrightarrow{\texttt{next}}\texttt{null}.$$Logical order is preserved solely through the $\texttt{next}$ chain, so nodes may reside anywhere in memory.
Sometimes there is a sentinel node $a_0$ at the head, which does not store data but simplifies insert/delete operations, and can imply the list is empty when $a_0.\texttt{next}=\texttt{null}$.
$$L\xrightarrow{\texttt{*}}a_0\xrightarrow{\texttt{next}}a_1\xrightarrow{\texttt{next}}\cdots\xrightarrow{\texttt{next}}a_n\xrightarrow{\texttt{next}}\texttt{null}.$$ | |
- Insert at tail — $O(n)$ (or $O(1)$ if a tail pointer is maintained).
- Delete a given node $p$ — $O(n)$. Must locate the predecessor of $p$ (cannot go backward). If only $p$ is given, copy $p.\texttt{next}$’s data into $p$ and bypass $p.\texttt{next}$ (the lazy deletion trick, not available for the tail) in $O(1)$ time.
For a singly linked list with a sentinel node, it’s empty when $L\texttt{->next}=\texttt{null}$; for one without a sentinel node, it’s empty when $L=\texttt{null}$.
Double Linked List
A doubly linked list augments each node with a backward pointer:
$$\texttt{node}=[\texttt{prev}\mid\texttt{data}\mid\texttt{next}].$$Both forward and backward traversal are supported, there is convenience but also extra space and complexity for implementation.
| |
In some cases, there will be another sentinel node at the tail, but we will not discuss it here.
$$L\xrightarrow{\texttt{*}}a_1\xleftrightarrow{\texttt{prev}\mid\texttt{next}}a_2\xleftrightarrow{\texttt{prev}\mid\texttt{next}}\cdots\xleftrightarrow{\texttt{prev}\mid\texttt{next}}a_n.$$- Insert before/after a given node $p$ — $O(1)$. Adjust $p.\texttt{prev}$ and $p.\texttt{next}$.
- Delete a given node $p$ — $O(1)$. Unlike the singly linked case, the predecessor is immediately available as $p.\texttt{prev}$.
Circular Linked List
A circular linked list replaces the terminal $\texttt{null}$ with a pointer back to the first node, forming a ring. Both singly and doubly linked variants exist.
$$L\xrightarrow{\texttt{*}}(a_0)a_1\xrightarrow{\texttt{next}}a_2\xrightarrow{\texttt{next}}\cdots\xrightarrow{\texttt{next}}a_n\xrightarrow{\texttt{next}}a_1.$$The last node’s $\texttt{next}$ points to the sentinel if there is one. With sentinel nodes, the list is empty when the sentinel’s $\texttt{next}$ points to itself.
There is a variant of singly circular lists, requiring no sentinel, and maintaining a tail pointer $L$ pointing to the last node instead of the head.
$$L\xrightarrow{\texttt{*}}a_n\xrightarrow{\texttt{next}}a_1\xrightarrow{\texttt{next}}a_2\xrightarrow{\texttt{next}}\cdots\xrightarrow{\texttt{next}}a_n.$$Doubly circular list is similar, and it has properties as follows
- Insert/Delete at head/tail — $O(1)$. The tail pointer allows direct access to the head via $L\texttt{->next}$.
- Insert/Delete at a known node — $O(1)$.
Both singly and doubly circular lists can
- Traversal — starts from any node and stops after one full cycle (requires remembering the starting node or using a sentinel) and singly circular lists can only traverse forward.
- Merge two circular lists — $O(1)$. Swap the $\texttt{next}$ pointers of the two tail nodes.
There are no $\texttt{null}$ checks needed (with a sentinel), uniform code for all positions, natural fit for cyclic data.
Static Linked List
Static linked list stores a linear list in a fixed-size array of nodes
$$\texttt{node}[\texttt{node}[i].\texttt{next}]=\texttt{node}[j]\quad\text{if}\quad a_i\texttt{.next}=a_j.$$And the head pointer storing no data, only the index of the first node.
| |
$\texttt{node}[0]$ represents the head pointer, and
$$\texttt{next}\in \{-1,0,1,\ldots,\texttt{MaxSize}-1\},$$taking $-1$ to represent $\texttt{null}$.
The array is allocated once and reused, with a free list of available nodes maintained in the same array. This avoids dynamic memory allocation overhead but limits the maximum size of the list.
Hash List
A hash list stores the linear list in a hash table keyed by the integer index $i$:
$$\texttt{Loc}(a_i)=\texttt{Hash}(i).$$Collisions are resolved by chaining: each bucket holds a small linked list of $\langle\texttt{index},\texttt{value}\rangle$ pairs. A load factor $\alpha=n/m$ (with $m$ buckets) is kept below a constant threshold; when exceeded, the table is rehashed to a larger $m$.
Operations (average case, simple uniform hashing):
- Access by index $i$ — $O(1)$. Compute $\texttt{Hash}(i)$, walk the chain in that bucket (expected length $\alpha=O(1)$).
- Insert/Delete at index $i$ — $O(1)$. Hash $i$ and add/remove the entry in the bucket.
- Search by value — $O(n)$. Must scan all $n$ entries across all buckets.
- Traversal in order — $O(n+m)$. Either thread an auxiliary linked list through the entries in index order, or extract and sort all indices.
Why use a hash list? It offers $O(1)$ average random access and $O(1)$ average insertion/deletion — combining the best of sequential and linked storage. The price is a larger constant factor (hash computation, collision chains, rehashing) and worst-case $O(n)$ behavior under a poor hash function. It is particularly useful when the index set is sparse (e.g., $n\ll\max\{i\}$) and a sequential array would waste space.
Stack, Queue: Restricted Linear List
Array: Extended Linear List
Set
Tree
Graph
Storage & Implementation
Sequential Storage
$$f(n)=a+(n-1)d$$Linked Storage
$$f(n)=f(f(n-1))=\cdots =f^{n-1}(f(1))$$Indexed Storage
Hash Storage
$$f(n)=\text{Hash}(k)$$Application
The $n$-th Fibonacci Number
$$n\log n \log\log n$$Algorithms
KMP
Exercises
Ex.1.1. Show that the time complexity of the following algorithm is $O(n)$
| |
Ex.2.1. Given a linear list
$$L=(a_1,a_2,\ldots,a_m,b_1,b_2,\ldots,b_n),$$try to reverse it with $O(1)$ extra space and $O(m+n)$ time complexity to
$$L'=(b_1,b_2,\ldots,b_n,a_1,a_2,\ldots,a_m).$$Ex.2.2. Given two increasing linear lists
$$L_1=(a_1,a_2,\ldots,a_n),\quad L_2=(b_1,b_2,\ldots,b_n),$$try to find the median of the combined list with $O(\log n)$ time complexity and $O(1)$ extra space.
Ex.2.3. To find the principal number(appear more than $n/2$ times) of a given integer linear list with length $n$ if there exists one in $O(n)$ time complexity and $O(1)$ extra space.
Ex.2.4. Can we realize the deletion of the tail node in a singly linked list in $O(1)$ time with the head pointing to the second last node? Why?
Ex.2.5. Given a singly linked list with a sentinel node, try to reverse it in $O(n)$ time and $O(1)$ extra space.
Ex.2.6. Given two singly linked lists, try to find the first common node of them in $O(m+n)$ time and $O(1)$ extra space.
Ex.2.7. A singly linked list is said to have a cycle if there exists a node $p$ such that $p.\texttt{next}$ points to one of its predecessors. Determine whether a singly linked list has a cycle in $O(n)$ time and $O(1)$ extra space.