1. Introduction
    1. Logical Structure of Data
    2. Storage structure of data
    3. Basic concepts of algorithms
    4. Measurement of algorithm efficiency
      1. Time complexity
      2. Space complexity
  2. Data structure
    1. Linear list
      1. Sequential representation of a linear list
        1. Basic Operation Time Complexity
      2. Linked representation of linear list
        1. Singly linked list
          1. head node
          2. creating a singly linked list using head insertion
          3. creating a singly linked list using tail insertion
        2. doubly linked list
        3. Circular Linked List
          1. Circular Singly Linked List
        4. Circular Doubly Linked List
        5. Static Linked List
    2. stack
      1. Sequential storage structure of a stack
        1. Shared stack
      2. Linked storage structure of a stack
      3. Classic applications
        1. Bracket matching
        2. Expression evaluation
        3. Converting recursion to stack implementation
    3. Queue
      1. Sequential storage structure of a queue
        1. Circular queue
      2. Linked storage structure of queue
      3. Deque
        1. Input-restricted deque
        2. Output-restricted deque
      4. Priority queue (heap)
      5. Classic applications
        1. Level-order traversal
    4. Arrays and special matrices
      1. Array
      2. Compression of special matrices
        1. Symmetric Matrix
        2. Triangular Matrix
          1. Row-major storage formula for upper triangular matrix:
          2. Row-major storage formula for lower triangular matrix:
        3. Tridiagonal / Banded Matrix
      3. Sparse matrix
    5. Disjoint Set Union (DSU)
      1. Data Structure Definition
      2. Time Complexity
    6. String
      1. String pattern matching
        1. Naive pattern matching algorithm
        2. KMP algorithm
    7. Trees and Binary Trees
      1. Terminology
      2. Properties of Trees
      3. Binary Tree
        1. Terminology
        2. Properties
        3. Storage structure
      4. binary tree traversal
        1. preorder traversal
          1. recursive implementation
          2. non-recursive implementation
        2. inorder traversal
          1. Recursive approach
          2. Non-recursive approach
        3. Post-order traversal
          1. Recursive approach
          2. Non-recursive approach
        4. Non-recursive approaches for three traversals
        5. Level-order traversal
        6. Morris traversal
    8. Graph
      1. Terminology
      2. Storage
        1. Adjacency matrix method
        2. adjacency list method
        3. Orthogonal List Method
        4. Adjacency Multilist
      3. Graph traversal
        1. Breadth-First Search
        2. Depth-First Search
          1. Recursive form
          2. Non-recursive form
      4. Minimum spanning tree
        1. Prim’s algorithm
        2. Kruskal’s algorithm
      5. Shortest path
        1. Dijkstra’s algorithm
        2. Floyd’s algorithm
      6. Topological sorting
      7. Critical Path
    9. Hash
      1. Concept
      2. Hash function
        1. Key type
          1. Horner’s Rule
        2. uniform distribution
        3. Hash Collision
          1. Chaining
          2. Open addressing method
        4. Expansion/Shrinkage
      3. Implementation of Hash Table
  3. Common algorithms
    1. Hash
    2. Two Pointers
      1. Fast and Slow Pointers
      2. Left and Right Pointers
    3. Linked List
    4. Tree
      1. Binary Tree Traversal
      2. Trie
    5. Graph
    6. Stack
      1. Monotonic Stack
    7. Queue
      1. Monotonic Queue
    8. Matrix
      1. Difference and Prefix Sum
    9. Sliding Window
    10. Backtracking
    11. Dynamic Programming
    12. Greedy
    13. Sorting
      1. Quick Sort
        1. Lomuto partition
        2. Hoare partition
      2. Heap Sort
    14. Divide and Conquer
    15. Union-Find
Cover image for Data Structures and Algorithms

Data Structures and Algorithms


Timeline

Timeline

2025-09-28

Add Introduction

2025-10-18

Add Linear List, Stack, Queue, Array, String

2025-10-20

Add Tree, Graph

2025-11-09

Add DSU (Disjoint Set Union)

2026-03-22

Add Algorithms

This article introduces the basics of data structures and algorithms, covering the logical structure of data (linear and nonlinear structures), storage structures (sequential storage, linked storage, indexed storage, hash storage), as well as the basic concepts and efficiency measurement methods of algorithms. It also discusses in detail the sequential and linked representations of linear lists (including singly linked lists, doubly linked lists, circular linked lists, and static linked lists) and the sequential and linked storage structures of stacks.

Introduction

Logical Structure of Data

The logical structure of data refers to the relationships between data elements, i.e., describing data from a logical perspective. It is independent of data storage and is computer-independent.

  • Linear Structure
    • Linear List
      • Array
      • Stack
      • Queue
  • Non-linear structure
    • Set
    • Tree structure
      • General tree
      • Binary tree
    • Graph structure
      • Directed graph
      • Undirected graph

Storage structure of data

Storage structure refers to the representation of data structures in a computer, also known as physical structure

  • Sequential storage

Sequential storage means storing logically adjacent elements in storage units that are also physically adjacent;

  • Linked storage

Linked storage uses pointers indicating the storage addresses of elements to represent the logical relationships between elements.

  • Index storage

Index storage refers to establishing an additional index table while storing element information. Each entry in the index table is called an index item, and the general form of an index item is (key, address).

  • Hash storage

Hash storage directly calculates the storage address of an element based on its key, also known as hash storage.

Basic concepts of algorithms

  • Finiteness

Finiteness means that an algorithm must always terminate after a finite number of steps, and each step must be completed within finite time.

  • Definiteness

Definiteness means that each instruction in an algorithm must have an unambiguous meaning, and for the same input, it must produce the same output.

  • Feasibility

Feasibility means that the operations described in an algorithm can be implemented by executing already implemented basic operations a finite number of times.

  • Input

Input means that an algorithm has zero or more inputs, which are taken from a specific set of objects.

  • Output

Output means that an algorithm has one or more outputs, which are quantities that have a specific relationship with the inputs.

Measurement of algorithm efficiency

Time complexity

T(n)=O(f(n))T(n) = O(f(n))

Addition rule:

T(n)=T1(n)+T2(n)=O(f(n))+O(g(n))=O(max(f(n),g(n)))T(n) = T_1(n)+T_2(n)=O(f(n)) + O(g(n)) = O(max(f(n),g(n)))

Common asymptotic time complexities

O(1)<O(log2n)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)O(1) < O(log_2^n) < O(n) < O(nlog^n) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)

Space complexity

S(n)=O(g(n))S(n) = O(g(n))

Data structure

Linear list

Definition: A linear list is a finite sequence ofthe same data typen (n>=0) data elements.

A linear list is generally represented as:

L=(a1,a2,a3,...,ai,ai+1,...,an)L = (a_1,a_2,a_3,...,a_i,a_{i+1}, ..., a_n)

Sequential representation of a linear list

The sequential storage of a linear list is also called a sequential list, characterized by the logical order of elements being the same as their physical order. A linear list is aRandom AccessStorage Structure

  • C Language Description
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Static Allocation
#define MAX_SIZE 50
typedef struct{
ElemType data[MaxSize];
int length;
}SqList;

// Dynamic Allocation
#define InitSize 100
typedef struct{
ElemType *data;
int capacity;
int length;
}SeqList;
  • C++ Description

Generally use the standard library vector, or you can customize it

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template <typename T>
class LinearList {
private:
T *data;
int length;
int capacity;

void resize(size_t new_capacity) {
T* new_data = new T[new_capacity];
for (size_t i = 0; i < size; i++) {
new_data[i] = data[i];
}
delete[] data;
data = new_data;
capacity = new_capacity;
}

public:
SeqList() : data(nullptr), size(0), capacity(0) {}

~SeqList() {
delete[] data;
}
};
  • Rust Description

In Rust, generally use the standard library Vec; writing your own requires many unsafe APIs

Basic Operation Time Complexity

OperationBest CaseWorst CaseAverage CaseRemarks
Insert by positionO(1) (at tail)O(n) (at head)O(n)Amortized O(1) for tail insertion
Delete by positionO(1) (at tail)O(n) (at head)O(n)Tail deletion O(1)
Access by indexO(1)O(1)O(1)Array random access
Search (sequential)O(1)O(n)O(n)If unordered, only sequential search possible

Linked representation of linear list

Singly linked list

The linked storage of linear list is also called singly linked list.

  • C language description
1
2
3
4
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;

The effect of this C language definition is

partmeaning
struct Node { ... };defines a structure type with the tag nameNode(i.e.,struct Node
LNodegivesstruct Nodean aliastype alias
*LinkListgivesstruct Node*(i.e., the pointer to this structure) an alias LinkList
  • C++ description

TODO

  • Rust description

TODO

head node

usually usehead pointerto identify a singly linked list; when the head pointer is NULL, it indicates an empty list. Additionally, for operational convenience, a node is added before the first node of the singly linked list, calledhead node

  • the data field of the head node can be left empty or can store information such as the length of the list.

  • introducing a head node brings two advantages:

    • since the position of the first data node is stored in the pointer field of the head node, operations at the first position of the linked list are consistent with those at other positions, requiring no special handling
    • regardless of whether the linked list is empty, its head pointer always points to the head node as a non-null pointer (in an empty list, the pointer field of the head node is NULL), thus unifying the handling of empty and non-empty lists.
creating a singly linked list using head insertion
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <stdlib.h>

#define ElemType int

typedef struct LNode {
ElemType data;
struct LNode *next;
} LNode, *LinkList;

// creating a singly linked list using head insertion
LinkList LinkListInit_HeadInsert(void) {
LinkList L;
LNode *node;
int x;

L = (LNode *)malloc(sizeof(LNode));
L->next = NULL;

while (scanf("%d", &x) != EOF) {
node = (LNode *)malloc(sizeof(LNode));
node->data = x;
node->next = L->next;
L->next = node;
}
return L;
}

creating a singly linked list using tail insertion

TODO

doubly linked list

in a singly linked list, there is only one pointer to the successor, allowing traversal only sequentially from the head node. In a doubly linked list, each node has two pointers, prior and next, pointing to the predecessor and successor nodes respectively.

  • C language description
1
2
3
4
typedef struct DNode{
ElemType data;
struct DNode *prior, *next;
}DNode, *DLinkList;
  • C++ description

TODO

  • Rust description

TODO

Circular Linked List

Circular Singly Linked List

The difference between a circular singly linked list and a singly linked list is that the pointer of the last node in the list is not NULL, but instead points to the head node, thus forming a ring.

  • C Language Description

  • C++ Description

  • Rust Description

Circular Doubly Linked List

Compared to a circular singly linked list, the prior pointer of the head node in a circular doubly linked list points to the tail node.

  • C Language Description

  • C++ Description

  • Rust Description

Static Linked List

A static linked list uses an array to describe the chained storage structure of a linear list. Nodes also have a data field and a pointer field next. Unlike the pointers in previous linked lists, the pointer here is the relative address (array index) of the node, also known as a cursor. Similar to a sequential list, a static linked list also requires pre-allocating a contiguous block of memory address space.

For example:

Indexdatanext
04
1head node2
2103
3205
46
530-1
6-1
1
2
3
4
5
6
7
8
9
10
主链表部分:
┌──────┬──────┬──────┐ ┌──────┬──────┬──────┐ ┌──────┬──────┬──────┐
│ idx=1│ data │ next │ ---> │ idx=2│ 10 │ next │ ---> │ idx=3│ 20 │ next │ ---> [idx=5│ 30 │ -1│]
└──────┴──────┴──────┘ └──────┴──────┴──────┘ └──────┴──────┴──────┘

空闲链表部分:
┌──────┬──────┬──────┐ ┌──────┬──────┬──────┐
│ idx=0│ — │ 4 │ ---> │ idx=4│ — │ 6 │ ---> [idx=6│ — │ -1│]
└──────┴──────┴──────┘ └──────┴──────┴──────┘

C language description

1
2
3
4
5
#define MaxSize 50
typedef struct{
ElemType data;
int next;
}SLinkList[MaxSize];

C++ description

Rust description

stack

stack (Stack)is only alloweda linear list that allows insertion or deletion only at one end

  • **top of stack (Top)**the end of the linear list where insertion and deletion are allowed
  • **bottom of stack (Bottom)**fixed. The other end where insertion and deletion are not allowed

the characteristics of the stack can be clearly summarized asLast In First Out (LIFO)

mathematical properties of the stack:

The number of possible pop sequences for n distinct elements pushed onto a stack is the nth Catalan number.

The formula is:

Cn=1n+1C2nn=(2n)!n!(n+1)!C_n = \frac{1}{n+1} C_{2n}^n = \frac{(2n)!}{n! (n+1)!}

Sequential storage structure of a stack

  • C language description
1
2
3
4
5
6
#define MaxSize 50
typedef struct{
ElemType data[MaxSize];
// Stack top pointer
int top;
}SqStack;
  • C++ description

TODO

  • Rust description

TODO

Shared stack

By utilizing the property that the bottom positions of stacks are relatively fixed, two sequential stacks can share a one-dimensional array space, with the bottoms of the two stacks set at opposite ends of the shared space, and the tops extending toward the middle.

1
2
3
4
5
6
7
8
9
下标:   0   1   2   3   4   5   6   7   8   9
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
Stack1→ │10 │20 │30 │ │ │ │ │ │40 │50 │ ←Stack2
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
↑ ↑ ↑
base1 mid base2
↑ ↑
top1=2 top2=8

The shared stack is designed to utilize address space more efficiently. Stack overflow occurs if and only if the two stacks are adjacent.

Linked storage structure of a stack

A stack using linked storage is called alinked stackLinked stackadvantages

  • Facilitates multiple stacks sharing storage space and improving efficiency
  • No stack overflow situation exists

Linked stacks are typically implemented using a singly linked list, with all operations performed at the head of the list.

  • C language description

Here it is specified that the linked stack has no head node

1
2
3
4
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}*LiStack;
  • C++ description

TODO

  • Rust description

TODO

Classic applications

Bracket matching

Expression evaluation

Converting recursion to stack implementation

Queue

QueueIt is also a linear list with restricted operations, allowing insertion only at one end and deletion only at the other end.

  • FrontThe end that allows deletion, also known asFront

  • RearThe end that allows insertion

  • EnQueueInsert an element at the rear

  • DeQueueDelete an element from the front

The characteristics of a queue can be simply summarized asFirst In First Out (FIFO)

Sequential storage structure of a queue

  • C language description
1
2
3
4
5
#define MaxSize 50
typedef struct{
ElemType data[MaxSize];
int front, rear;
}SqQueue;
  • C++ description

TODO

  • Rust description

Check if the queue is empty

Q.rear == Q.front==0

Determine if the queue is full

Because:

  • Enqueue operation: when the queue is not full, first assign the value to the tail element, then increment the tail pointer by 1
  • Dequeue operation: when the queue is not empty, first retrieve the head element value, then increment the head pointer by 1

Therefore, it is not possible to use “Q.rear==MaxSize” as the condition for a full queue; the entire problem is solved using a circular queue

Circular queue

The sequential queue is conceived as a circular space, that is, the table storing queue elements is logically regarded as a ring, calledcircular queue

  • When the head pointerQ.front == MaxSize -1after that, advance one more position to 0

  • Initialization:Q.front = Q.rear = 0

  • Head pointer increments by 1:Q.front = (Q.front + 1) % MaxSize

  • Tail pointer increments by 1:Q.rear = (Q.rear + 1) % MaxSize

  • Three ways to determine if the queue is full

    • Sacrificing one unit to distinguish between empty and full queues, using one less queue unit when enqueuing, is a common practice. The convention is to treat ‘the front pointer being at the next position of the rear pointer as the full queue flag’.

      • Full queue condition : (Q.rear + 1) % MaxSize == Q.front
      • Empty queue condition: (Q.front == Q.rear)
      • Number of elements in the queue: (Q.rear - Q.front + MaxSize) % MaxSize
    • Add a data member to represent the number of elementssize, so that both empty and full queue conditions haveQ.rear == Q.front

      • Full queue condition : Q.size == MaxSize
      • Empty queue condition: Q.size = 0
      • Number of elements in the queue: size
    • Addtagdata member. When tag is 0, it indicates that a deletion was performed most recently; when tag is 1, it indicates that an insertion was performed most recently.

      • Full queue condition: Whentagequals 0, if due to deletionQ.front == Q.rearthen the queue is empty;
      • Queue empty condition: Whentagequals 1, due to insertionQ.front == Q.rearthen the queue is full;
      • Number of elements in the queue

      队列中元素个数={0,if (front==rear and tag==0)MaxSize,if (front==rear and tag==1)(rearfront+MaxSize)%MaxSize,otherwise\text{队列中元素个数} = \begin{cases} 0, & \text{if } (front == rear \text{ and } tag == 0) \\ MaxSize, & \text{if } (front == rear \text{ and } tag == 1) \\ (rear - front + MaxSize) \% MaxSize, & \text{otherwise} \end{cases}

Linked storage structure of queue

Usually, a linked queue is designed as asingly linked list with a head node.

  • C language description
1
2
3
4
5
6
7
8
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}LinkNode;

typedef struct{
LinkNode *front,*rear;
}*LinkQueue;
  • C++ description

TODO

  • Rust description

TODO

Deque

A deque is a queue that allows enqueue and dequeue operations at both ends. The two ends of the queue are called the front and the rear.

Input-restricted deque

Insertion and deletion are allowed at one end, but only insertion is allowed at the other end.

Output-restricted deque

Insertion and deletion are allowed at one end, but only deletion is allowed at the other end.

Priority queue (heap)

A heap is a one-dimensional array maintained using the structure of a complete binary tree.

Heaps can be divided intomax heapandmin heap

  • Max heap: The value of each node is greater than or equal to the values of its left and right child nodes.

  • Min heap: The value of each node is less than or equal to the values of its left and right child nodes.

Taking the max heap as an example:

The construction process of a max heap starts from the last non-leaf node and adjusts from bottom to top.

When using an array to represent the sequence to be sorted, the position of the last non-leaf node is:Array length / 2 - 1

Compare the value of the current node with that of the left subtree. If the current node is smaller than the left subtree, swap the current node and the left subtree; after swapping, check whether the left subtree still satisfies the max-heap property, and if not, readjust the subtree structure.

Then compare the value of the current node with that of the right subtree. If the current node is smaller than the right subtree, swap the current node and the right subtree; after swapping, check whether the right subtree still satisfies the max-heap property, and if not, readjust the subtree structure.

When no swapping or adjustment is needed, the max-heap construction is complete.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <vector>
using std::vector;

void heapify(vector<int> &vec, int curr, int n)
{
int left = curr * 2 + 1;
int right = curr * 2 + 2;
int largest = curr;

if (left < n && vec[left] > vec[largest]) {
largest = left;
}

if (right < n && vec[right] > vec[largest]) {
largest = right;
}

if (largest != curr) {
std::swap(vec[largest], vec[curr]);
//Since the parent node and child node have been swapped, it may affect the subtree of the child node, so the subtree of the child node needs to be adjusted.
heapify(vec, largest, n);
}
}

void buildMaxHeap(vector<int> &vec)
{
int n = vec.size();
// The position of the last node is n-1, so the position of the parent node is (n-1-1)/2.
for (int i = (n - 2) / 2; i >= 0; i--) {
heapify(vec, i, n);
}
}

#include <stdio.h>

int main()
{
vector<int> nums = { 9, 8, 2, 1, 5, 3, 0, 10, 22, 5 };
int n = nums.size();
buildMaxHeap(nums);

for (int i = 0; i < n; i++) {
// pop
std::swap(nums[0], nums[n - i - 1]);
heapify(nums, 0, n - i - 1); // Heap length is n-i-1
}
// Output the sorted array
for (int i = 0; i < n; i++)
printf("%d ", nums[i]);
printf("\n");
}

Classic applications

Level-order traversal

Arrays and special matrices

Array

Most languages provide an array data type.

Compression of special matrices

Symmetric Matrix

  • Property: MatrixA=[aij]A = [a_{ij}]satisfiesaij=ajia_{ij} = a_{ji}
  • Compression method: Only store theupper or lower triangular part, for example, store the elements of the upper triangle (including the diagonal).

If the matrix isn×nn \times n, store the upper triangle row by row:

存储顺序: a11,a12,,a1n,a22,a23,,a2n,,ann\text{存储顺序: } a_{11}, a_{12}, \dots, a_{1n}, a_{22}, a_{23}, \dots, a_{2n}, \dots, a_{nn}

  • Index mapping formula(stored in a one-dimensional arrayB[k]):

k=i(i1)2+j1(ij)k = \frac{i(i-1)}{2} + j - 1 \quad (i \le j)

wherei,ji, jstarts counting from 1.

  • Storage spacen(n+1)2\frac{n(n+1)}{2}elements.

Triangular Matrix

  • Properties
    • Upper triangular matrix: all elementsaij=0a_{ij} = 0wheni>ji > j
    • Lower triangular matrix: all elementsaij=0a_{ij} = 0wheni<ji < j
  • Compression method: store only the non-zero triangular part.
Row-major storage formula for upper triangular matrix:

k=i(i1)2+j1(ij)k = \frac{i(i-1)}{2} + j - 1 \quad (i \le j)

Row-major storage formula for lower triangular matrix:

k=i(2ni+1)2+(ji)(ij)k = \frac{i(2n-i+1)}{2} + (j-i) \quad (i \ge j)

  • Storage spacen(n+1)2\frac{n(n+1)}{2}elements.

Tridiagonal / Banded Matrix

  • Properties: The matrix has non-zero elements only on the main diagonal, super-diagonal, and sub-diagonal, with the rest being 0.

A=[a1b100c1a2b200c2a30bn1000cn1an]A = \begin{bmatrix} a_1 & b_1 & 0 & \dots & 0 \\ c_1 & a_2 & b_2 & \dots & 0 \\ 0 & c_2 & a_3 & \dots & 0 \\ \vdots & \vdots & \vdots & \ddots & b_{n-1} \\ 0 & 0 & 0 & c_{n-1} & a_n \end{bmatrix}

  • Compression method: Store three one-dimensional arrays:
    • A[1..n]stores the main diagonalaia_i
    • B[1..n-1]stores the super-diagonalbib_i
    • C[1..n-1]stores the sub-diagonalcic_i
  • Access formula

ai,i=A[i],ai,i+1=B[i],ai+1,i=C[i]a_{i,i} = A[i], \quad a_{i,i+1} = B[i], \quad a_{i+1,i} = C[i]

  • Storage space3n23n - 2elements, compared to a regularn2n^2significantly saves space.

Sparse matrix

The number of non-zero elements t in the matrix is very small relative to the total number of matrix elements s, i.e.,sts \gg tmatrix is called asparse matrix

Storing a sparse matrix using conventional methods is quite wasteful of space, so only the non-zero elements are stored. However, the distribution of non-zero elements is usually very irregular, so the row and column of each non-zero element must also be stored.

Therefore, the non-zero elements along with their corresponding rows and columns form a triple.

(行标,列标,)(\text 行标, 列标, 值)

Then store this triple according to a certain rule.After compressed storage, the sparse matrix loses the characteristic of random access.

The triple of a sparse matrix can be stored usingarray storage,or it can usethe orthogonal linked list methodfor storage.

Triple sequential list (array storage)

  • Store the non-zero elements in a sparse matrix as triples in row-major or column-major order.(行标, 列标, 值)
  • Common orders:
    • Row-major: Store by rows from top to bottom, and within each row from left to right.
    • Column-major: Store by columns from left to right, and within each column from top to bottom.
  • Suitable for cases where non-zero elements of the matrix are not frequently modified.

Cross Linked List / Orthogonal List storage

  • More suitable for dynamically modified sparse matrices.
  • Each non-zero element node contains:
    • Value
    • Row index
    • Column index
    • Row pointer (points to the next non-zero element in the same row)
    • Column pointer (points to the next non-zero element in the same column)
  • Enables fast traversal of rows and columns, suitable for operations like matrix multiplication and transposition.

Disjoint Set Union (DSU)

Disjoint Set Union,DSU / Union-Find

DSU
DSU

Disjoint Set Union is adata structure that dynamically maintains several disjoint setsand supports two main operations:

OperationsFunction Description
find(x)Find ElementxRepresentative of the Set (Root Node)
union(x, y)/join(x, y)Merge the Sets of Two Elements
(Optional)connected(x, y)Check if Two Elements Belong to the Same Set (i.e.,find(x) == find(y)

Data Structure Definition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <vector>
#include <algorithm>

using std::vector;

class DSU {
public:
vector<int> parent; // Parent Node of Each Node
vector<int> rank; // (Optional) Used for Union by Rank or Recording Set Size

DSU(int n)
{
parent.resize(n);
rank.resize(n, 0);
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}

int find(int x)
{
// Path Compression, Making Find Complexity Close to O(1)
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}

void join(int x, int y)
{
// Union by Rank
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
return;
}

if (rank[rootX] < rank[rootY]) { //The Larger Rank of the Two Sets Becomes rootX
std::swap(rootX, rootY);
}

parent[rootY] = rootX;//The Set with Smaller Rank is Attached Under the One with Larger Rank

if (rank[rootX] == rank[rootY]) { //If the Ranks of Two Sets Are Equal, the Rank of the Merged Set Increases by 1
rank[rootX]++;
}
}

bool connected(int x, int y)
{
return find(x) == find(y);
}
};

Tip:

TechniqueDescriptionEffect
Path CompressionDuringfind()the operation, point the parent of all nodes on the find path to the rootSignificantly reduces tree height, nearly amortized O(1)
Union by Rank/SizeDuringunion()the operation, attach the shorter tree under the taller treeKeeps the tree balanced, further optimizing find efficiency

Time Complexity

OperationAmortized Time Complexity
find()Approximately O(1)
union()Approximately O(1)
connected()Approximately O(1)

Strictly speaking, it is O(α(n)), but α(n) ≤ 5 for any practical size of n.

String

String pattern matching

The positioning operation of a substring is usually called stringpattern matching, which finds the position of a substring (usually called the pattern string) in the main string.

Naive pattern matching algorithm

Pattern stringpand main strings

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
S: a b a b c a b c a c b a b
P: a b c a c
↑ 从这里开始和主串对齐

步骤1:
S: a b a b c a b c a c b a b
P: a b c a c
a = a ✅
b ≠ a ❌ 失配 → 模式右移一位

步骤2:
S: a b a b c a b c a c b a b
P: a b c a c
a ≠ b ❌ → 继续右移一位

步骤3:
S: a b a b c a b c a c b a b
P: a b c a c
a = a ✅
b = b ✅
c = a ❌ → 右移一位

步骤4:
S: a b a b c a b c a c b a b
P: a b c a c
b ≠ a ❌ → 右移一位

步骤5:
S: a b a b c a b c a c b a b
P: a b c a c
c = a ❌ → 右移一位

步骤6:
S: a b a b c a b c a c b a b
P: a b c a c
a = a ✅
b = b ✅
c = c ✅
a = a ✅
c = c ✅ ✅ 匹配成功!


Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <string.h>

int naive_match(const char *s, const char *p) {
int i=0,j=0;
int s_len = strlen(s);
int p_len = strlen(p);

while(i<s_len && j<p_len){
if(s[i]==p[j]){
i++;
j++;
}else{
i = i - j + 1;//Go back and rematch
j = 0;
}
}
if(j == p_len){
return i - j;
}
return -1;
}

Worst caseO(nm)O(n \cdot m), wherennis the length of the main string,mmis the length of the pattern string.

  • For example, the main string"aaaaaaab"and the pattern string"aaaab", each match failure requires backtracking multiple characters.

Average case: usually less than the worst case, but may still approachO(nm)O(n \cdot m)

KMP algorithm

Recommended excellent explanatory article:

In the naive pattern matching algorithm, each match failure shifts the pattern string (the substring to be matched) one position to the right and starts comparing from the beginning.

Several concepts:

  • Prefix: all substrings that include the first character but not the last character
  • Suffix: All substrings that include the last letter but not the first letter
  • **PM(Partial Match)**Partial match values, i.e., the table below

Below, take “ABCDABD” as the substring to be matched as an example:

SubstringPrefix set (excluding the full string)Suffix set (excluding the full string)Length of the longest equal prefix and suffix
A(empty)(empty)0
ABAB0
ABCA, ABC, BC0
ABCDA, AB, ABCD, CD, BCD0
ABCDAA, AB, ABC, ABCDA, DA, CDA, BCDA1(A)
ABCDABA, AB, ABC, ABCD, ABCDAB, AB, DAB, CDAB, BCDAB2 (AB)
ABCDABDA, AB, ABC, ABCD, ABCDA, ABCDABD, BD, ABD, DABD, CDABD, BCDABD0

Then the PM table is

Index0123456
String[i]ABCDABD
PM0000120
  • Given the text string “BBC ABCDAB ABCDABCDABDE” and the pattern string “ABCDABD”, now match the pattern string against the text string as shown in the figure below:

img
img

    1. Since character A in the pattern string does not match characters B, B, C, and space in the text string from the start, there is no need to consider the conclusion; simply shift the pattern string right by one position repeatedly until character A in the pattern string matches the 5th character A in the text string:

img
img

    1. Continue matching forward. When the last character D of the pattern string mismatches during matching with the text string, it is obvious that the pattern string needs to be shifted right. But how many positions to shift right? Since the number of matched characters at this point is 6 (ABCDAB), and according to thePMtable, the length value corresponding to character B, which is the previous character of the mismatched character D, is 2. Therefore, based on the previous conclusion, it is known that it needs to be shifted right by 6 - 2 = 4 positions, fromPM[i] = 2That is, restart matching from index 2

img
img

We can define anextarray: when the j-th (starting from 0) character of the pattern string fails to match, continue matching from the next[j]-th character of the pattern string.

The next array in KMPThere are two ways to write it:

PM table = prefix function π(i)(also calledlps array,Longest Prefix Suffix)。

  • In some textbooks, next is equal to PM (lps)

  • In some textbooks, next is a variant of PM

    • next[0] = -1(indicating a complete restart of matching)

    • next[i] = PM[i-1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <string.h>

void GetNext(char *p, int next[])
{
int pLen = strlen(p);
next[0] = -1;
int k = -1;
int j = 0;
while (j < pLen - 1) {
//p[k] represents the prefix, p[j] represents the suffix
if (k == -1 || p[j] == p[k]) {
++k;
++j;
next[j] = k;
} else {
k = next[k];
}
}
}

int KmpSearch(char *s, char *p, int next[])
{
int i = 0;
int j = 0;
int sLen = strlen(s);
int pLen = strlen(p);
while (i < sLen && j < pLen) {
//If j = -1, or the current characters match successfully (i.e., S[i] == P[j]), then increment both i and j
if (j == -1 || s[i] == p[j]) {
i++;
j++;
} else {
//If j != -1, and the current characters fail to match (i.e., S[i] != P[j]), then keep i unchanged and set j = next[j]
//next[j] is the next value corresponding to j
j = next[j];
}
}
if (j == pLen)
return i - j;
else
return -1;
}

Trees and Binary Trees

Terminology

graph TD
    A[A] --> B[B]
    A --> C[C]
    B --> D[D]
    B --> E[E]
    C --> F[F]
    C --> G[G]
    E --> H[H]
    E --> I[I]

    style A fill:#f9f,stroke:#333,stroke-width:2px
    style D fill:#9f9,stroke:#333
    style F fill:#9f9,stroke:#333
    style G fill:#9f9,stroke:#333
    style H fill:#9f9,stroke:#333
    style I fill:#9f9,stroke:#333
  • Classification of Nodes

    • Root Node

      • The topmost node of a tree.

      • Example:Ais the root node.

    • Parent

      • The immediate upper-level node of a node.

      • Example:B’s parent node isA

    • Sibling

      • Nodes sharing the same parent node are siblings.

      • Example:DandEare siblings,FandGare siblings.

    • Child Node

      • Child Node: A node directly connected to a parent node.
      • Example:DEisB’s child node.
    • Descendant

      • All descendants of a node, including child nodes.
      • Example:HIisB’s descendant.
    • Internal Node

      • A node that has child nodes.
      • Example:ABCEare all branch nodes.
    • Leaf Node

      • A node with no children.
      • Example:DFGHIare leaf nodes.
  • Node Properties

    • Depth of a Node

      • The length (number of edges) of the path from the root node to this node. (Top-down)

      • Example:Adepth 0,Bdepth 1,Ddepth 2.

    • Height of a Node

      • The length of the longest path from this node to the deepest leaf node. (Bottom-up)

      • Example:BHeight 2 (longest path B→E→H or B→E→I).

      • Level of a Node

        • Level = Depth + 1

        • Example: Root node A is level 1, B is level 2.

      • Degree

        • The number of child nodes a node has.
        • Example:Bhas degree 2 (DE)。
  • Classification of Trees

    • Tree of degree m

      • The degree of any node is not greater than m
      • At least one node has degree m
      • Must be a non-empty tree, thus has at least m+1 nodes
    • m-ary tree

      • The degree of any node is not greater than m
      • Can be an empty tree
    • Ordered Tree / Unordered Tree

      • Ordered Tree: The subtrees of nodes in the tree are ordered from left to right and cannot be swapped, meaning child nodes have a fixed order.

      • Unordered Tree: The order of child nodes is not important.

    • Forest

      • A collection of multiple disjoint trees.

      • If the root node A is removed, two trees, tree B and tree C, are formed, constituting a forest.

  • Other Concepts

    • Path
      • The sequence of nodes traversed from one node to another.
      • Example: A→B→E→H is a path.
    • Path Length
      • The number of edges on the path.
      • Example: A→B→E→H, path length = 3.

Properties of Trees

  1. The number of nodes in a tree equals the sum of all node degrees plus 1.

    • The proof is simple: except for the root node, every other node has an edge above it.
  2. A tree of degreemmhas at mostiinodes on themi1m^{i-1}-th level (i1i \ge 1)

  3. Ahh-ary tree of heightmmhas at most mh1m1 \frac{m^h-1}{m-1} nodes

  4. Ann-ary tree withmmnodes has a minimum height of logm(n(m1)+1) \lceil \log_m (n(m-1)+1) \rceil

    • Proof: Under the condition of minimum height, try to make each node havemmchildren, then:
    • mh11m1<nmh1m1\frac{m^{h-1}-1}{m-1} < n \le \frac{m^h-1}{m-1}

Binary Tree

A binary tree is an ordered tree

Terminology

  • Full Binary Tree

    graph TD
        style A fill:#f9f,stroke:#333,stroke-width:2px
        style B fill:#9cf,stroke:#333,stroke-width:1px
        style C fill:#9cf,stroke:#333,stroke-width:1px
        style D fill:#9cf,stroke:#333,stroke-width:1px
        style E fill:#9cf,stroke:#333,stroke-width:1px
        style F fill:#9cf,stroke:#333,stroke-width:1px
        style G fill:#9cf,stroke:#333,stroke-width:1px
        style H fill:#9f9,stroke:#333,stroke-width:1px
        style I fill:#9f9,stroke:#333,stroke-width:1px
        style J fill:#9f9,stroke:#333,stroke-width:1px
        style K fill:#9f9,stroke:#333,stroke-width:1px
        style L fill:#9f9,stroke:#333,stroke-width:1px
        style M fill:#9f9,stroke:#333,stroke-width:1px
        style N fill:#9f9,stroke:#333,stroke-width:1px
        style O fill:#9f9,stroke:#333,stroke-width:1px
    
        A((1)) --> B((2))
        A --> C((3))
        B --> D((4))
        B --> E((5))
        C --> F((6))
        C --> G((7))
        D --> H((8))
        D --> I((9))
        E --> J((10))
        E --> K((11))
        F --> L((12))
        F --> M((13))
        G --> N((14))
        G --> O((15))
    
    • Heighthh, and a binary tree containing 2h12^h-1 nodes is called a full binary tree

    • In a full binary tree, every node except leaf nodes has degree 2

    • A full binary tree can be numbered in level order, for a node numberedii:

      • Left child number:2i2i

      • Right child number:2i+12i+1

      • Parent node number:i/2\lfloor i/2 \rfloor

  • complete binary tree

    graph TD
        style A fill:#f9f,stroke:#333,stroke-width:2px
        style B fill:#9cf,stroke:#333,stroke-width:1px
        style C fill:#9cf,stroke:#333,stroke-width:1px
        style D fill:#9cf,stroke:#333,stroke-width:1px
        style E fill:#9cf,stroke:#333,stroke-width:1px
        style F fill:#9cf,stroke:#333,stroke-width:1px
        style G fill:#9cf,stroke:#333,stroke-width:1px
        style H fill:#9f9,stroke:#333,stroke-width:1px
        style I fill:#9f9,stroke:#333,stroke-width:1px
        style J fill:#9f9,stroke:#333,stroke-width:1px
        style K fill:#9f9,stroke:#333,stroke-width:1px
        style L fill:#9f9,stroke:#333,stroke-width:1px
    
        A((1)) --> B((2))
        A --> C((3))
        B --> D((4))
        B --> E((5))
        C --> F((6))
        C --> G((7))
        D --> H((8))
        D --> I((9))
        E --> J((10))
        E --> K((11))
        F --> L((12))
    
    • with heighthh, havingnna binary tree with nodes, if and only if each of its nodes corresponds one-to-one with nodes numbered 1 to n in a full binary tree of heighthh, it is called a complete binary tree
    • If in2 i \le \lfloor \frac{n}{2} \rfloor, then nodeiiis a branch node
    • Leaf nodes can only appear in the two largest levels, and for the largest level, leaf nodes are all arranged in the leftmost positions of that level.
    • If there is a node with degree 1, there can be at most one, and that node has only a left child and no right child.
    • After numbering by level order, once a node (numberedii) is a leaf node or has only a left child, then nodes with numbers greater thaniiare all leaf nodes
    • Ifnnis odd, then every branch node has both left and right children; ifnnis even, then the branch node with the largest number (numberedn2\frac{n}{2}) only has a left child, no right child; the remaining nodes have both left and right children
    • Of the two subtrees of a complete binary tree (the left and right subtrees of the root node), at least one is a full binary tree
  • Binary Search Tree

    • All keys in the left subtree are less than the root node’s key, and all keys in the right subtree are greater than the root node’s key; both the left and right subtrees are themselves binary search trees.
  • Balanced Binary Tree

    • On the treeThe difference in depth between the left subtree and right subtree of any node does not exceed 1

English definition
English definition

Properties

  1. The number of leaf nodes in a non-empty binary tree equals the number of nodes with degree 2 plus 1, i.e., n0=n2+1 n_0 = n_2+1

  2. On a non-empty binary tree, the k k -th level has at most2(k1)2^{(k-1)}nodes (k1 k \ge 1 )

  3. A binary tree of heighthhhas at most2h12^h-1nodes (h1h\ge1

  4. withnnnodescomplete binary treehas height log2(n+1) \lceil \log_2^{(n+1)} \rceil or log2n+1 \lfloor \log_2^n \rfloor + 1

  5. forcomplete binary treenode numbering, the following relationships hold

Item1-based numbering(root=1)0-based numbering(root=0)
left childleft(i)=2i\text{left}(i)=2i left(i)=2i+1 \text{left}(i)=2i+1
right childright(i)=2i+1 \text{right}(i)=2i+1 right(i)=2i+2 \text{right}(i)=2i+2
Parent nodeparent(i)=i/2i>1 \text{parent}(i)=\lfloor i/2 \rfloor(i>1)parent(i)=(i1)/2i>0 \text{parent}(i)=\lfloor (i-1)/2 \rfloor(i>0)
Condition for having at least a left child2in 2i \le n 2i+1<n 2i+1 < n
Condition for right child existence2i+1n 2i+1 \le n 2i+2<n 2i+2 < n
Leaf node conditioni>n/2i > \lfloor n/2 \rfloori>(n2)/2 i > \lfloor (n-2)/2 \rfloor or2i+1n2i+1 \ge n
Condition for having only a left child2i=n2i = n 2i+1<n 2i+1 < n and 2i+2n 2i+2 \ge n
Depth (root level = 1)log2i+1 \lfloor \log_2 i \rfloor +1 log2(i+1)+1 \lfloor \log_2 (i+1) \rfloor +1
Depth (root level = 0)log2i \lfloor \log_2 i \rfloor log2(i+1) \lfloor \log_2 (i+1) \rfloor

Storage structure

  • Sequential storage structure

Array storage, stored in the manner of a complete binary tree, using 0 or -1 to represent empty nodes.

  • Linked storage

It is easy to verify that in a binary linked list containingnnnodes, there aren+1n+1null pointer fields

n nodes, 2n pointer fields, n-1 nodes have a pointer pointing to themselves, so there are n+1 null pointer fields

binary tree traversal

preorder traversal

visit the root node first, then traverse the left subtree, then traverse the right subtree

recursive implementation
1
2
3
4
5
6
7
void PreOrder(BiTree T){
if(T!=NULL){
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
non-recursive implementation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void PreOrder(BiTree T){
Stack *S;
InitStack(S);
BiTree p = T;
while(p || !isEmpty(S)){//loop while the stack is not empty or p is not empty
if(p){
visit(p);
Push(S, p);
p = p->lchild;
}else{
Pop(S, p);
p = p->rchild;
}

}
DestroyStack(S);
}

description in Rust

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Definition for a binary tree node.
#[derive(Debug, PartialEq, Eq)]
pub struct TreeNode {
pub val: i32,
pub left: Option<Rc<RefCell<TreeNode>>>,
pub right: Option<Rc<RefCell<TreeNode>>>,
}

impl TreeNode {
#[inline]
pub fn new(val: i32) -> Self {
TreeNode {
val,
left: None,
right: None,
}
}
}


use std::cell::RefCell;
use std::rc::Rc;

pub fn preorder(root: &mut Option<Rc<RefCell<TreeNode>>>, visit: &impl Fn(&mut Rc<RefCell<TreeNode>>)) {
// preorder traversal
let mut stack = Vec::new();
let mut p = root.clone();

// preorder traversal
while let Some(node) = p {
visit(node);
// push the right child onto the stack
if let Some(right) = node.borrow().right.clone() {
stack.push(right);
}
// push the left child onto the stack; if the left child is null, pop the stack; otherwise, p = Some(left)
if let Some(left) = node.borrow().left.clone() {
p = Some(left);
} else {
p = stack.pop();
}
}
}

inorder traversal

visit the left subtree first, then the root node, then the right subtree

Recursive approach
1
2
3
4
5
6
7
void InOrder(BiTree T){
if(T!=NULL){
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
Non-recursive approach
  1. Push the left children of the root node onto the stack in sequence until the left child is null, indicating that a node ready to be output has been found
  2. Pop and visit the top element of the stack; if it has a right child, continue with step 1; otherwise, continue with step 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void InOrder(BiTree T){
Stack *S;
InitStack(S);
BiTree p = T;
while(p || !isEmpty(S)){//Loop while the stack is not empty or p is not null
if(p){
Push(S, p);
p = p->lchild;
}else{
Pop(S, p);
visit(p);
p = p->rchild;
}

}
DestroyStack(S);
}

Post-order traversal

Visit the left subtree first, then the right subtree, then the root node

Recursive approach
1
2
3
4
5
6
7
void PostOrder(BiTree T){
if(T!=NULL){
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
}
Non-recursive approach
  1. Push the left children of the root onto the stack in sequence until the left child is null
  2. Read the top element of the stack: if its right child is not null and has not been visited, perform step 1 on the right subtree; otherwise, pop and visit the top element

For the top element to be popped and visited, either its right subtree is null or its right subtree has already been visited (at which point the left subtree has long been visited). Therefore, we need an auxiliary pointer to indicate the most recently visited node

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void PostOrder(BiTree T){
Stack *S;
InitStack(S);
BiTNode *p = T;
BiTNode *r= NULL;
while(p || !isEmpty(S)){//Loop while the stack is not empty or p is not null
if(p){
Push(S, p);
p = p->lchild;
}else{
GetTop(S,p);
if(p->rchild && p->rchild != r){
p = p->rchild;
}else{
Pop(S, p);
visit(p);
r = p;
p = NULL;
}
}

}
DestroyStack(S);
}

In post-order traversal, when a node is about to be visited, the nodes in the stack are all the nodes on the path from the root to the current node

Non-recursive approaches for three traversals

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include <vector>
#include <stack>
using std::vector;
using std::stack;

struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode()
: val(0)
, left(nullptr)
, right(nullptr)
{
}
TreeNode(int x)
: val(x)
, left(nullptr)
, right(nullptr)
{
}
TreeNode(int x, TreeNode *left, TreeNode *right)
: val(x)
, left(left)
, right(right)
{
}
};

vector<struct TreeNode *> preorder_traverse(struct TreeNode *root)
{
stack<struct TreeNode *> stk;
TreeNode *p = root;
vector<struct TreeNode *> ret;

while (p || !stk.empty()) {
if (p) {
ret.push_back(p); // visit p
stk.push(p);
p = p->left;
} else {
p = stk.top();
stk.pop();
p = p->right;
}
}
return ret;
}

vector<struct TreeNode *> inorder_traverse(struct TreeNode *root)
{
stack<struct TreeNode *> stk;
TreeNode *p = root;
vector<struct TreeNode *> ret;

while (p || !stk.empty()) {
if (p) {
stk.push(p);
p = p->left;
} else {
p = stk.top();
stk.pop();
ret.push_back(p); // visit p
p = p->right;
}
}
return ret;
}

vector<struct TreeNode *> postorder_traverse(struct TreeNode *root)
{
stack<struct TreeNode *> stk;

TreeNode *p = root, *last = nullptr;
vector<struct TreeNode *> ret;

while (p || !stk.empty()) {
if (p) {
stk.push(p);
p = p->left;
} else {
p = stk.top();
if (p->right != nullptr && p->right != last) {
p = p->right;
} else {
stk.pop();
ret.push_back(p); // visit p
last = p;
p = nullptr;
}
}
}

return ret;
}

Level-order traversal

Traverse from the first layer to the last layer, from left to right

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void LevelOrder(BiTree T){
Queue *Q;
InitQueue(Q);
EnQueue(Q, T);
while(!isEmpty(Q)){
DeQueue(Q,p);
visit(p);
if(p->lchild!=NULL){
EnQueue(Q, p->lchild);
}
if(p->rchild!=NULL){
EnQueue(Q, p->rchild);
}
}
DestroyQueue(Q);
}

If you need to record or perform some operations when each layer is fully visited:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* Definition for a binary tree node.
**/
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode()
: val(0)
, left(nullptr)
, right(nullptr)
{
}
TreeNode(int x)
: val(x)
, left(nullptr)
, right(nullptr)
{
}
TreeNode(int x, TreeNode *left, TreeNode *right)
: val(x)
, left(left)
, right(right)
{
}
};
#include <vector>
#include <queue>
using std::vector;
using std::queue;

class Solution {
public:
vector<int> rightSideView(TreeNode *root)
{
int i, n;
vector<int> res;
queue<TreeNode *> que;
TreeNode *p;
if (root == nullptr) {
return res;
}
que.push(root);

while (!que.empty()) {
n = que.size();
res.push_back(que.back()->val);
for (i = 0; i < n; i++) {
p = que.front();
if (p->left) {
que.push(p->left);
}
if (p->right){
que.push(p->right);
}
que.pop();
}
}
return res;
}
};

Morris traversal

Morris traversal uses the idea of threaded binary trees. In the Morris method, there is no need to allocate additional pointers for each node to point to its predecessor and successor; it only needs to utilizethe left and right null pointers in leaf nodesto point to the predecessor or successor node under a certain order traversal.

graph TD

    subgraph 图2
        B1[1]
        B2[2]
        B3[3]
        B4[4]
        B5[5]
        B6[6]
        B7[7]
        
        B1 --> B2
        B1 --> B3
        B2 --> B4
        B2 --> B5
        B3 --> B6
        B3 --> B7
        
        B4 -.-> B2:::cur2
        B5 -.-> B1:::cur1
        B6 -.-> B3:::curn
        
    end
    
    subgraph 图1
        A1[1] --> A2[2]
        A1 --> A3[3]
        A2 --> A4[4]
        A2 --> A5[5]
        A3 --> A6[6]
        A3 --> A7[7]
    end

The overall idea of Morris is to start from a certain root node,find the rightmost node of its left subtree, and then connect it to this root node

We can see from Figure 2 that after such connection, the cur pointer can traverse from one node to the next completely, traversing the entire tree until node 7 has no right pointer. Code example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/**
* Definition for a binary tree node.
**/
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode()
: val(0)
, left(nullptr)
, right(nullptr)
{
}
TreeNode(int x)
: val(x)
, left(nullptr)
, right(nullptr)
{
}
TreeNode(int x, TreeNode *left, TreeNode *right)
: val(x)
, left(left)
, right(right)
{
}
};

extern void visit(TreeNode *root);

class Solution {
private:
public:
void preOrderMorris(TreeNode *root)
{
if (root == nullptr)
return;

TreeNode *curr = root; // Current node
TreeNode *curr_left = nullptr; // Root node of the left subtree of the current node

while (curr != nullptr) {
curr_left = curr->left;

if (curr_left == nullptr) {
visit(curr);
curr = curr->right; // Continuously move right during the phase of returning to the upper level
} else {
// Find the rightmost node of the current left subtree, and cannot return to the upper level along the connection
while (curr_left->right != nullptr && curr_left->right != curr)
curr_left = curr_left->right;
// The right pointer of the rightmost node does not point to the root node; create a connection and proceed to connect to the root node of the next left subtree
if (curr_left->right == nullptr) {
visit(curr);
curr_left->right = curr;
curr = curr->left;
} else {
// When the rightmost node of the left subtree points to the root node, it indicates that we have entered the phase of returning to the upper level,
// no longer the initial connection establishment phase. At the same time, when returning to the root node, we should have already processed the lower-level nodes, so we can simply disconnect the link.
curr_left->right = nullptr;
curr = curr->right; // In the phase of returning to the upper level, we keep moving to the right.
}
}
}
}
};

#include <iostream>
using std::cout;
void visit(TreeNode *root)
{
cout << root->val << " ";
}

int main()
{
/*
4
/ \
2 6
/ \ / \
1 3 5 7
*/

TreeNode *n1 = new TreeNode(1);
TreeNode *n3 = new TreeNode(3);
TreeNode *n5 = new TreeNode(5);
TreeNode *n7 = new TreeNode(7);

TreeNode *n2 = new TreeNode(2, n1, n3);
TreeNode *n6 = new TreeNode(6, n5, n7);

TreeNode *root = new TreeNode(4, n2, n6);

Solution sol;

cout << "Morris Preorder Traversal: ";
sol.preOrderMorris(root);
cout << '\n';

return 0;
}

Graph

Terminology

Basic Concepts

TerminologyExplanation
Directed GraphEdges in the graph have directions, such asABA \rightarrow B
Undirected GraphEdges have no direction, such asABA - B, indicating a bidirectional relationship

Classification by Edge Structure

TerminologyExplanation
Simple GraphDoes not allowself-loops(connecting a vertex to itself) andmultiple edges(multiple edges between two vertices)
MultigraphAllows self-loops and multiple edges

Special Graphs

TermExplanation
Complete GraphThere is an edge between any two vertices. An undirected complete graph is denoted asKnK_n, withn(n1)2\frac{n(n-1)}{2}edges
SubgraphA graph formed by taking a subset of vertices and edges from the original graph

Graph Connectivity

TermExplanation
ConnectedIn an undirected graph, any two vertices are connected by a path
Connected GraphThe graph is entirely connected
Connected ComponentA maximal connected subgraph in an undirected graph (a connected piece that cannot be enlarged)
Strongly Connected GraphIn a directed graph, any two verticesuuandvvThere exist mutually reachable paths
Strongly Connected ComponentThe largest strongly connected subgraph in a directed graph

Tree-related

TerminologyExplanation
Spanning TreeIn a connected undirected graph, a tree that includes all vertices with the minimum number of edges (n1n-1)
Spanning ForestFor a disconnected graph, a collection of spanning trees generated from each connected component

Properties of vertices and edges

TerminologyExplanation
Degree of a vertexDegree (number of edges connected to a vertex in an undirected graph)
In-degreeNumber of edges pointing to the vertex in a directed graph
Out-degreeNumber of edges pointing away from the vertex in a directed graph
Weight of an edgeAdditional value attached to an edge (e.g., distance, time, cost)
NetworkWeighted graph (graph with edge weights), also called network
Dense GraphGraph with many edges, a vague concept
Sparse GraphGraph with few edges, a vague concept

Path and Distance

TerminologyExplanation
PathA sequence of vertices where any two adjacent vertices are connected by an edge
Path lengthThe number of edges (or sum of weights) on the path
CycleA path with the same start and end point
Simple pathVertices in the path are not repeated
Simple cycleIn a cycle, vertices other than the start and end are not repeated
DistanceThe length of the shortest path between two vertices

Storage

Adjacency matrix method

Undirected graph

  • Ifiandjhave an edge:Edge[i][j] = Edge[j][i] = 1
  • If no edge:Edge[i][j] = Edge[j][i] = 0

Directed graph

  • If there isi → j’s edge:Edge[i][j] = 1
  • If not:Edge[i][j] = 0

Network (weighted graph)

  • Ifi → j’s weight isw, thenEdge[i][j] = w

  • When there is no edge, it is generally set to(or a very large number)

  • C language description

1
2
3
4
5
6
7
#define MaxVertexNum 100
typedef char VertexType;
typedef int EdgeType;
typedef struct{
VertexType Vex[MaxVertexNum]; // vertex table
EdgeType Edge[MaxVertexNum][MaxVertexNum]; //adjacency matrix
}MGraph;
  • C++ description
1
2
3
4
5
6
7
8
9
#include <unordered_map>
#include <vector>
using std::unordered_map;
using std::vector;

// node index range indefinite
unordered_map<int, vector<int>> graph;
// node index range definite
vector<vector<int>> graph;

adjacency list method

  • C language description
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define MaxVertexNum 100
typedef struct ArcNode{//edge table node
int adjvex; //vertex pointed to by this arc
struct ArcNode *next;
// InfoType info // edge weight
}ArchNode;

typedef struct VNode{//vertex table node
VertexType data; // vertex information
ArchNode *first; //pointer to the first arc incident to this vertex
}VNode,AdjList[MaxVertexNum];

typedef struct{
AdjList vertices;/// adjacency list
int vexnum, arcnum;
}ALGraph;

  • C++ Description

Orthogonal List Method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define MaxVertexNum 100

typedef struct ArcNode {
int tailvex; // Arc Tail (Starting Point)
int headvex; // Arc Head (End Point)
struct ArcNode *hlink; // Points to the next arc with the same arc head
struct ArcNode *tlink; // Points to the next arc with the same arc tail
int info; // Weight (Optional)
} ArcNode;

typedef struct VexNode {
char data; // Vertex Information
ArcNode *firstin; // Points to the first incoming arc
ArcNode *firstout; // Points to the first outgoing arc
} VexNode;

typedef struct {
VexNode xlist[MaxVertexNum]; // Vertex Table
int vexnum, arcnum; // Number of Vertices, Number of Arcs
} OLGraph; // Orthogonal List Graph

Adjacency Multilist

The adjacency multilist is another linked storage structure for undirected graphs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct ENode {
int ivex, jvex; // Vertex Numbers of the Edge
struct ENode *ilink, *jlink; // Point to the next edge connecting ivex and jvex respectively
} ENode;

typedef struct VNode {
char data; // Vertex information
ENode *firstedge; // First edge incident to this vertex
} VNode;

typedef struct {
VNode adjList[MaxVertexNum];
int vexnum, edgenum;
} AMLGraph; // Adjacency Multilist Graph

Graph traversal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
bool visited[MAX_VERTEX_NUM];

void BFSTraverse(Graph G){
Queue *Q;
for(i = 0;i < G.vexnum;i++){
visited[i] = false;
}
InitQueue(Q);
for(i = 0;i < G.vexnum;i++){//Call BFS for each connected component
if(!visited[i]){
BFS(G,i);
}
}
DestroyQueue(Q);
}

void BFS(Graph G, int v){//Starting from vertex v, perform breadth-first traversal of G
visit(v);
visited[v] = TRUE;
Enqueue(Q, v);
while(!isEmpty(Q)){
DeQueue(Q,v);
for(w = FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)){ //Enqueue all unvisited adjacent vertices of v
if(!visited[w]){
visit(w);
visited[w] = true;
EnQueue(Q,w);
}
}
}
}

If the graph is unweighted, BFS can be used to solve the single-source shortest path problem, mainly leveraging the property that breadth-first traversal visits vertices in order of increasing distance.

Recursive form
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bool visited[MAX_VERTEX_NUM];
void DFSTraverse(Graph G){
for(v = 0;v<G.vexnum;v++)
visited[v]=false;

for(v = 0;v<G.vexnum;v++){
if(!visited[v])
DFS(G,v);
}
}
void DFS(Graph G, int v){
visit(v);
visited[v] = true;
for(w = FirstNeighbor(G,v); w>=0 ; w = NextNeighbor(G,v,w)){
if(!visited[w]){
DFS(G,w);
}
}
}
Non-recursive form
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void DFS_Non_RC(AGraph &G, int v){
int w;
Stack *S;
InitStack(S);
for(i = 0;i<G.num;i++)
visited[i] = false;

Push(S,v);
visited[v] = true;//Indicates already pushed onto the stack
while(!isEmpty(S)){
k = Pop(S);
visit(k);
for(w = FirstNeighbor(G,k);w>=0;w=NextNeighbor(G,k,w)){
if(!visited[w]){
Push(S,w);
visited[w] = true;
}
}

}
DestroyStack(S);
}

Minimum spanning tree

Prim’s algorithm

Idea: Start from a point, each time selectthe edge with the smallest costadd the new vertex to the spanning tree

more suitable fordense graphs(adjacency matrix)

Kruskal’s algorithm

Idea: Sort all edges of the graph by weight, select edges from smallest to largest, as long asno cycle is formedthen select

suitable forsparse graphs

Using Union-FindAvoid forming cycles

Shortest path

Dijkstra’s algorithm

Single-source shortest path problem

Floyd’s algorithm

Shortest paths between all pairs of vertices

Topological sorting

Take the following problem as an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <vector>
#include <queue>

using std::vector;
using std::queue;

class Solution {
public:
bool canFinish(int numCourses, vector<vector<int> > &prerequisites)
{
// Build the graph
int i;
vector<vector<int> > graph(numCourses);
vector<int> indegree(numCourses, 0); // Store the in-degree of each node
vector<bool> pushed(numCourses, false);
queue<int> que;

for (vector<int> &vec : prerequisites) {
graph[vec[1]].push_back(vec[0]);
indegree[vec[0]]++;
}

// Topological sorting
// Initialize by adding edges with in-degree 0
for (i = 0; i < numCourses; i++) {
if (indegree[i] == 0) {
que.push(i);
pushed[i] = true;
}
}

while (!que.empty()) {
i = que.front();
que.pop();

// Each time find an edge with in-degree 0
for (int course : graph[i]) {
indegree[course]--;

if (!pushed[course] && indegree[course] == 0) {
que.push(course);
pushed[course] = true;
}
}
}

for (i = 0; i < numCourses; i++) {
if (!pushed[i])
return false;
}

return true;
}
};

Critical Path

Hash

Reference Article:

Hash Table (Hash Table) also known as a hash table is a data structure that uses key-value pairs (Key-Value Pair) to store data, andmaps keys to specific positions in an array via a hash function, enabling efficient search, insertion, and deletion operations.

Concept

  • Storage Structure: A hash table typically consists of an array and a hash function. Each element of the array is called a bucket, which can store one or more key-value pairs

  • Hash Function: The hash function converts a key into an array index (i.e., an integer)

  • Hashing: is the process of converting input data (usually of arbitrary length) into a fixed-length output (an index within the array range) through a hash function

  • Hash Collision: When different keys are mapped to the same index by the hash function, a hash collision occurs. Common methods to resolve hash collisions include chaining and open addressing

  • Load FactorThe ratio of the number of elements stored in the hash table to the size of the array (total number of slots)

    • The size of the hash table (number of slots) is fixed, but the number of stored elements can exceed the number of slots
    • The load factor indicates the utilization efficiency of the hash table space.A higher load factor means more elements are stored in the table, with higher space utilization
    • Whenthe load factor increases (approaching 1 or higher), the lookup efficiency of the hash table may decrease, especially when using open addressing, conflict resolution requires more probing steps
    • When using chaining, an increase in the load factor leads to longer linked lists, thereby increasing the average search time
    • In dynamic hash tables, when the load factor exceeds a certain threshold (e.g., 0.75), it typically triggers rehashing to reduce collisions and improve performance
    • If the load factor is too low, it may lead to shrinking to save space
    • Ideal load factor:
      • Open addressing: The ideal load factor is typically below 1, recommended between 0.5 and 0.75 to balance space utilization and lookup efficiency.
      • Chaining: The load factor can exceed 1 because multiple elements can be stored in the same slot. Typically, a load factor between 0.7 and 1.0 is a good choice, but it can be higher depending on the impact of chain length on performance.
  • Rehashing: When the load factor of the hash table exceeds a set threshold, rehashing may be needed. Rehashing typically involves creating a larger array and recalculating the hash values of all keys to map them into the new array.

  • Shrinking: Shrinking refers to reducing the capacity of the hash table when the amount of data decreases to a certain level, to save space and improve memory utilization efficiency.

Hash function

A hash function converts a key into an array index (i.e., an integer). A good hash function should have the following advantages:

  • Fast computation: The advantage of hash tables lies in efficiency, so quickly obtaining the corresponding hashCode is very important.

  • Uniform distribution: In hash tables, whether using chaining or open addressing, when multiple elements map to the same position, efficiency is affected. An excellent hash function should map elements to different positions as much as possible, allowing elements to be uniformly distributed in the hash table.

Key type

Ultimately, a hash function is about converting keyskeyConverted to an index value, it can serve as a keykeyThere are many types that can serve as keys: numbers, strings, objects, etc. Below, we mainly discuss strings as keys becauseHash table keys are typically implemented as strings, mainly for the following reasons

  • Consistency: Using strings as keys provides a uniform way to handle different types of keys. Even numbers and other types are ultimately converted to strings for storage

  • Hash function: Hash tables use a hash function to map keys to array indices. Strings are generally easier to handle because they can be directly hashed, while other types (such as objects) may require more processing steps

  • Ease of comparison: Strings have clear comparison rules, making lookup, insertion, and deletion operations simpler and more efficient

  • Space efficiency: In many languages, storing strings as keys is typically more compact than objects or other complex types, reducing memory usage

  • Flexibility: Strings can represent various types of information and easily adapt to different scenarios, such as database indexes, field names, etc.

There are two methods for a hash function to convert a string into a number:

  • Method one:Summing the ASCII values of the characters, the problem is that many keys may end up with the same value, for examplewas/tin/give/tend/moan/tickboth are 43

  • Method two:Multiplying each character’s ASCII value of the key by a constant, the resulting number can basically guarantee its uniqueness (explained below), the repetition rate with other words is greatly reduced, but the problem is that the obtained value is too large. Since this value is to be used as an index, creating such a large array is meaningless.

In fact, numbers greater than 10 that we commonly use can be represented by a product of powers to express their uniqueness. For example:7654 = 7*10³ + 6*10² + 5*10 + 4, words can also be represented using this scheme. For example:cats = 3*27³ + 1*27² + 20*27 + 17= 60337, 27 because there are 27 letters.

Then, for the problem of the obtained index being too large, a compression algorithm emerges, which compresses the huge integer range obtained from the product of powers scheme into an acceptable array range. A simple method is to use the modulo operator, which gives the remainder when one number is divided by another. Array indices are limited, for example from 0 to n-1 (where n is the acceptable array length). By applying the modulo operation, they can be constrained within this range.

Horner’s Rule

The method used above when calculating the hash value:cats = 3*27³ + 1*27² + 20*27 + 17= 60337, we can abstract it into an expression:a(n)x^n + a(n-1)x^(n-1) +... + a(1)x + a(0)

  • Number of multiplications:n + (n-1) +... + 1 = n(n + 1)/2

  • Number of additions:ntimes

    • Time complexity:O(N²)

Through transformation, a much faster algorithm can be obtained, which is an efficient algorithm for solving this type of evaluation problem.Horner’s Rule(Horner’s Method), known in China asQin Jiushao’s algorithm, the core of Horner’s rule is to rewrite an ordinary polynomial into nested form. Taking a cubic polynomial as an example, the common factor (x) is gradually extracted to reduce computation. The derivation process is as follows:

Original polynomial form(cubic polynomial): P(x)=a3x3+a2x2+a1x+a0 P(x) = a_3x^3 + a_2x^2 + a_1x + a_0

  1. Step 1: Extract the common factor (x) from the first two termsP(x)=x(a3x2+a2x)+a1x+a0 P(x) = x(a_3x^2 + a_2x) + a_1x + a_0
  2. Step 2: Extract the common factor (x) again from the part inside the parenthesesP(x)=x(x(a3x+a2))+a1x+a0 P(x) = x\big(x(a_3x + a_2)\big) + a_1x + a_0
  3. Step 3: Combine the remaining terms to form the final nested formP(x)=((a3x+a2)x+a1)x+a0 P(x) = \big((a_3x + a_2)x + a_1\big)x + a_0

General form of Horner’s rule (n-degree polynomial) For any (n)-degree polynomial P(x)=anxn+an1xn1++a1x+a0P(x) = a_nx^n + a_{n-1}x^{n-1} + \dots + a_1x + a_0 , it can be uniformly transformed into: P(x)=(((anx+an1)x+an2)x++a1)x+a0 P(x) = (\dots((a_nx + a_{n-1})x + a_{n-2})x + \dots + a_1)x + a_0

where:

  • Number of multiplications:ntimes
  • Number of additions:ntimes
  • Time complexity: from O(N²) reduced to O(N)

uniform distribution

When designing a hash table, the case of mapping to the same index value has already been handled: chaining or open addressing. However, both methods aim to improve efficiency, and the best scenario is to have data uniformly distributed in the hash table. Therefore,**it is necessary to use prime numbers wherever constants are used,**such as the length of the hash table and the base of the N-th power.

Why does using prime numbers make the hash table distribution more uniform?

Because the result of multiplying a prime number with other numbers is more likely to produce unique results compared to other numbers, reducing hash collisions.

For example, the base of the N-th power in Java’s HashMap is chosen as 31, and commonly used numbers are 31 or 37, which are derived from long-term observation of distribution results.

Hash Collision

When different keys are mapped to the same index by the hash function, a hash collision occurs. Common methods to resolve hash collisions include chaining and open addressing.

Chaining

Chaining is a common method for resolving collisions (also known as separate chaining). Each bucket of the hash table can store multiple elements. When multiple keys are hashed to the same index, chaining stores these elements in a linked list or other data structure.

Chaining
Chaining

  • Principle

    • of chaining**The core idea is to treat each array element as a bucket, storing multiple key-value pairs within the bucket.**This is usually implemented using linked lists or arrays.

    • Once a duplicate is found, insert the duplicate element into**the head or tail of the linked list.**That’s all.

    • When querying, first locate the corresponding position based on the hashed index value, thenretrieve the linked listand sequentially search for the desired data.

  • Efficiency

    • When implementing a hash table using the chaining method, the process of finding an element is divided into two parts:
      • Calculate the hash value and locate the slot: This step is a constant-time operation, typically 1 operation, independent of the linked list length.
      • Traverse the linked list to find the element: If multiple elements are in the same slot, the hash table searches the linked list of that slot. The average number of searches is half the linked list length, typically loadFactor / 2.
      • Therefore, a successful search may only need to examine half the linked list: 1 + loadFactor/2, while an unsuccessful search may need to traverse the entire linked list to determine failure: 1 + loadFactor.
    • The chaining method is relatively more efficient than the open addressing method, so it is more commonly used in real development. It does not cause a sharp performance drop after adding an element; for example, Java’s HashMap uses the chaining method.

Efficiency of the chaining method
Efficiency of the chaining method

Open addressing method

of open addressing methodThe main working method is to find the next empty slot in the array to store the conflicting element when a hash collision occurs, there are three ways to probe empty slots:

  • Linear probing

    • Linear probing (Linear Probing) checks the next empty slot each time a collision occursindex = (hashFunc(key) + i) % tableSizewhere i is the number of collisions
    • Efficiency

    Relationship between expected number of probes and load factor
    Relationship between expected number of probes and load factor

    • Problem: Linear probing has a serious issue called clustering

      • For example, when inserting 22-23-24-25-26 into an empty table, it means indices 0-1-2-3-4 all have elements; this series of filled cells is called clustering

      • Clustering affects the performance of the hash table in insertion, query, and deletion. For example, inserting 38 would find that consecutive cells cannot store data, requiring multiple probes during the process

  • Quadratic probing

    • Quadratic probing (Quadratic Probing) Determine the position of the next slot based on the square of the number of collisionsindex = (hashFunc(key) + i^2) % tableSize
    • Optimizing linear problems
      • Linear probing can be seen as a step size of1probing, for example, starting from the index valuexthen linear testing isx+1,x+2,x+3probing sequentially
      • Quadratic probing optimizes the step size, for example, starting from the index valuexstartingx+1²,x+2²,x+3², so that it can probe a longer distance at once, better avoiding the impact of clustering
    • Efficiency

    Performance of quadratic probing and rehashing
    Performance of quadratic probing and rehashing

    • Problem
      • Quadratic probing still has problems, for example, if consecutive insertions are32-112-82-2-192, then the step sizes are the same when accumulating sequentially
      • That is, in this caseit will cause a clustering with different step sizes, which still affects efficiency
  • Rehashing method

    • Rehashing (Double Hashing) uses a second hash function to calculate the step size and determine the position of the next slotindex = (hashFunc1(key) + i \* (constant - (hashFunc2(key) % constant))) % tableSize
    • Optimizing the quadratic probing problem:
      • The quadratic probing algorithm generates a fixed step size sequence: 1-4-9-16 and so on, while the rehashing method uses another hash function to hash the key again and uses the result of this hashing as the step size
      • The second hashing must have the following characteristics:
        • It must be different from the first hash function (do not reuse the previous hash function, otherwise the result will still be the original position)
        • It must not output 0 (otherwise there will be no step size, each probe will stay in place, and the algorithm will enter an infinite loop)
        • Computer experts have designed a well-functioning hash function:stepSize = constant - (hanshFunc(key) % constant), where constant is a prime number and smaller than the array capacity
    • Efficiency

    Performance of quadratic probing and rehashing method
    Performance of quadratic probing and rehashing method

Expansion/Shrinkage

  • Expansion: As the amount of data increases, the bucket corresponding to each index becomes longer, leading to reduced efficiency. Therefore, when appropriate, the array is expanded, for example, by doubling its size.

Expansion can simply double the capacity, but in this case, all data items must be modified simultaneously (by re-calling the hash function to obtain different positions). For example, a data item with hashCode = 12 is placed at index = 4 when arraySize = 8, and at index = 12 when arraySize = 16.

  • Shrinkage: When the load factor of the hash table (the ratio of the number of elements to the hash table capacity) falls below a certain threshold, shrinkage is considered to save space and improve memory utilization efficiency.

During shrinkage, the hash table capacity is typically reduced to half of its current capacity, and the positions of all existing elements are recalculated and reassigned.

When to expand/shrink?

A common scenario isloadFactor > 0.75expansion is performed when the load factor exceeds a certain value. For example, Java’s hash table expands when the load factor is greater than 0.75.

A lower load factor threshold is usually set, such as 0.25, below which shrinkage is triggered. However, excessive shrinkage should be avoided: a small, reasonable minimum capacity, such as 7 or 8, is typically set to ensure the hash table maintains a certain capacity even when there are few elements, thereby preventing frequent expansion and shrinkage.

Implementation of Hash Table

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <stdio.h>
#include <string.h>

#define N 100003 // Hash table size (prime number, to reduce collisions)
#define M 200003 // Total number of nodes

// int to[M], key storage, stores the key of each node
// int val[M], value storage, stores the value of each node
// int head[N], bucket array (entry), head[5] = 3 indicates the linked list with hash value 5 starts at idx = 3
// int nxt[M], linked list pointer (core), points to the index of the next node, equivalent to next in a linked list, nxt[3] = 7 means the next node of node 3 is 7

int head[N], to[M], nxt[M], val[M];
int idx;

// Hash function
static int inline hash(int x)
{
return (x % N + N) % N;
}

// Initialization
void hashtable_init()
{
memset(head, -1, sizeof(head));
idx = 0;
}

// Insert key -> value
void hashtable_put(int key, int value)
{
int h = hash(key);
to[idx] = key;
val[idx] = value;
nxt[idx] = head[h];
head[h] = idx++;
}

// Search for key, return value (return -1 if not found)
int hashtable_get(int key)
{
int h = hash(key);
for (int i = head[h]; i != -1; i = nxt[i]) {
if (to[i] == key)
return val[i];
}
return -1;
}

int main()
{
hashtable_init();

hashtable_put(1, 100);
hashtable_put(2, 200);
hashtable_put(100003, 300); // Test collision

printf("%d\n", hashtable_get(1)); // 100
printf("%d\n", hashtable_get(2)); // 200
printf("%d\n", hashtable_get(3)); // -1
}

Common algorithms

Hash

ProblemLink
P1 Two Sum✨✨✨
P49 Group Anagrams
P128 Longest Consecutive Sequence✨✨✨
P383 Ransom Note
P205 Isomorphic Strings
P290 Word Pattern
P242 Valid Anagram
P49 Group Anagrams
P202 Happy Number
P219 Contains Duplicate II✨✨✨
P966 Vowel Spellchecker
P3484 Design Spreadsheet
P380 Insert Delete GetRandom O(1)✨✨✨
P3289 The Troublemaker in Number Town
P13 Roman to Integer

Two Pointers

Fast and Slow Pointers

TitleLink
P234 Palindrome Linked List
P142 Linked List Cycle II
P80 Remove Duplicates from Sorted Array II ✨✨✨
P287 Find the Duplicate Number ✨✨✨
P19 Remove Nth Node From End of List
P160 Intersection of Two Linked Lists

Left and Right Pointers

ProblemLink
P283 Move Zeroes
P125 Valid Palindrome
P27 Remove Element
P167 Two Sum II - Input Array Is Sorted
P88 Merge Sorted Array
P392 Is Subsequence
P75 Sort Colors
P31 Next Permutation
P15 3Sum ✨✨✨
P11 Container With Most Water ✨✨✨
P611 Valid Triangle Number
P42 Trapping Rain Water ✨✨✨

Linked List

ProblemsLink
P3217 Remove Nodes from Linked List Present in Array
P141 Linked List Cycle
P2 Add Two Numbers
P21 Merge Two Sorted Lists
P138 Copy List with Random Pointer
P92 Reverse Linked List II
P25 Reverse Nodes in k-Group✨✨✨
P19 Remove Nth Node From End of List
P82 Remove Duplicates from Sorted List II✨✨✨
P61 Rotate List
P86 Partition List
P146 LRU Cache✨✨✨
P148 Sort List
P160 Intersection of Two Linked Lists
P206 Reverse Linked List✨✨✨
P234 Palindrome Linked List✨✨✨
P142 Linked List Cycle II✨✨✨
P24 Swap Nodes in Pairs

Tree

Binary Tree Traversal

Preorder (Depth-First), Inorder, Postorder, Level Order, Morris Traversal

ProblemLink
P104 Maximum Depth of Binary Tree
P100 Same Tree
P226 Invert Binary Tree
P101 Symmetric Tree
P105 Construct Binary Tree from Preorder and Inorder Traversal✨✨✨
P106 Construct Binary Tree from Inorder and Postorder Traversal✨✨✨
P117 Populating Next Right Pointers in Each Node II
P114 Flatten Binary Tree to Linked List✨✨✨
P112 Path Sum
P129 Sum Root to Leaf Numbers
P124 Binary Tree Maximum Path Sum✨✨✨
P173 Binary Search Tree Iterator
P222 Count Complete Tree Nodes✨✨✨
P236 Lowest Common Ancestor of a Binary Tree✨✨✨
P199 Binary Tree Right Side View
P637 Average of Levels in Binary Tree
P102 Binary Tree Level Order Traversal
P103 Binary Tree Zigzag Level Order Traversal
P530 Minimum Absolute Difference in BST
P98 Validate Binary Search Tree
P230 Kth Smallest Element in a BST
P94 Binary Tree Inorder Traversal
P543 Diameter of Binary Tree✨✨✨
P437 Path Sum III✨✨✨

Trie

A string as a node

1
2
3
4
5
struct TrieNode {
string word;
bool is_end;
unordered_map<char, TrieNode *> children;
};

A char as a node

1
2
3
4
5
6
7
8
9
10
11
12
struct TrieNode{
char data;
bool is_end;
unordered_map<char, TrieNode *> children;
}

// When char is limited to lowercase or uppercase letters, using vector is faster
struct TrieNode{
char data;
bool is_end;
vector<TrieNode *> children;
}
ProblemLink
P14 Longest Common Prefix
P208 Implement Trie (Prefix Tree)
P211 Add and Search Word - Data Structure Design
P212 Word Search II✨✨✨
P139 Word Break✨✨✨

Graph

DFS and BFS, Topological Sort

ProblemLink
P200 Number of Islands✨✨✨
P130 Surrounded Regions
P133 Clone Graph
P399 Evaluate Division
P207 Course Schedule✨✨✨
P210 Course Schedule II✨✨✨
P909 Snakes and Ladders✨✨✨
P443 Minimum Genetic Mutation
P127 Word Ladder✨✨✨
P994 Rotting Oranges✨✨✨

Stack

TitleLink
P20 Valid Parentheses
P71 Simplify Path
P155 Min Stack✨✨✨
P150 Evaluate Reverse Polish Notation✨✨✨
P224 Basic Calculator✨✨✨
P394 Decode String✨✨✨
P2197 Replace Non-Coprime Numbers in Array✨✨✨
P32 Longest Valid Parentheses ✨✨✨

Monotonic Stack

Core Rule of Monotonic StackThat is:When pushing an element onto the stack, if it breaks monotonicity, pop the top until monotonicity is restored

Suppose we want to maintain a ‘monotonically decreasing stack’ (smallest at top)Either push, or pop several times and then push, always maintaining ‘monotonically decreasing’.

TypeFrom bottom to top of stackFunction
Monotonically Increasing StackValues increasingSuitable for finding the ‘next greater element’
Monotonically decreasing stackValues become smaller and smallerSuitable for finding the ‘next smaller element’

Its core idea is:

Use monotonicity to filter out useless elements, ensuring the ‘most recent valid element’ is at the top of the stack

ProblemLink
P3542 Minimum number of operations to make all elements zero ✨✨✨
P739 Daily Temperatures ✨✨✨
P84 Largest Rectangle in Histogram ✨✨✨

Queue

Monotonic Queue

ProblemLink
P239 Sliding Window Maximum

Matrix

ProblemLink
P73 Set Matrix Zeroes ✨✨✨
P54 Spiral Matrix
P48 Rotate Image ✨✨✨
P240 Search a 2D Matrix II✨✨✨
P36 Valid Sudoku
P289 Game of Life✨✨✨

Difference and Prefix Sum

2D Difference and Its Prefix Sum
2D Difference and Its Prefix Sum

ProblemLink
P2536 Increment Submatrix Elements by One✨✨✨

Sliding Window

Classic Template

1
2
3
4
5
6
7
8
9
int left=0, right=0;
while(right < s.size()){
windows.add(s[right]);
right++;
while(windows需要收缩){
windows.remove(s[left]);
left++;
}
}
ProblemLink
P3 Longest Substring Without Repeating Characters
P438 Find All Anagrams in a String
P209 Minimum Size Subarray Sum
P30 Substring with Concatenation of All Words ✨✨✨
P76 Minimum Window Substring ✨✨✨
P3186 Maximum Total Damage from Spell Casting

Backtracking

Solving a backtracking problem is essentially a traversal process of a decision tree.

Full permutation of 1, 2, 3
Full permutation of 1, 2, 3

You only need to think about one question:

  1. Path: that is, the choices already made.
  2. Choice list: that is, the choices you can currently make.
  3. End condition: that is, the condition of reaching the bottom of the decision tree where no further choices can be made.

Classic template framework

1
2
3
4
5
6
7
8
9
10
11
12
result = []
void backtrace(路径,选择列表){
if (满足结束条件){
result.add(路径)
return;
}
for(选择&: 选择列表){
做选择,当前选择加入选择路径
backtrace(路径,选择列表)
撤销选择,当前选择移除出选择路径
}
}
ProblemLink
P46 Permutations
P78 Subsets
P17 Letter Combinations of a Phone Number
P39 Combination Sum
P22 Generate Parentheses✨✨✨
P79 Word Search
P212 Word Search II
P51 N-Queens✨✨✨
P52 N-Queens II
P77 Combinations
P131 Palindrome Partitioning ✨✨✨

Dynamic Programming

TitleLink
P120 Triangle Minimum Path Sum
P1039 Minimum Score Triangulation of Polygon
P122 Best Time to Buy and Sell Stock II
P55 Jump Game
P45 Jump Game II
P3147 Maximum Energy Drawn from a Wizard
P3186 Maximum Total Damage from Casting Spells
P3539 Sum of Array Products of Magic Sequences
P392 Is Subsequence
P474 Ones and Zeroes
P70 Climbing Stairs
P53 Maximum Subarray Sum
Maximum Sum Circular Subarray
House Robber
Coin Change
Longest Increasing Subsequence
Word Break
Minimum Path Sum
Unique Paths II
Longest Palindromic Substring
P221 Maximal Square
P72 Edit Distance
P97 Interleaving String
P123 Best Time to Buy and Sell Stock III
P188 Best Time to Buy and Sell Stock IV
P131 Palindrome Partitioning
P118 Pascal’s Triangle
P279 Perfect Squares
P152 Maximum Product Subarray
P416 Partition Equal Subset Sum
P32 Longest Valid Parentheses
P62 Unique Paths
P1143 Longest Common Subsequence

Greedy

ProblemLink
P55 Jump Game✨✨✨
P45 Jump Game II✨✨✨
P134 Gas Station✨✨✨
P135 Candy✨✨✨
P1578 Minimum Time to Make Rope Colorful
P3228 Maximum Number of Operations to Move Ones to the End
P763 Partition Labels
P121 Best Time to Buy and Sell Stock
P976 Largest Perimeter Triangle

Sorting

Merge Sort, Counting Sort, Quick Sort, Heap Sort

TitleLink
P148 Sort List
P2785 Sort Vowels in a String
P3541 Find the Highest Frequency of Vowels and Consonants
P274 H-Index
P3005 Count Elements with Maximum Frequency

Quick Sort

Lomuto partition

Each partition determines the position of one value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <vector>
#include <algorithm>
using std::vector;

int partition(vector<int> &arr, int start, int end)
{ // Partition interval [l, r]
int pivot = arr[end];
int i = start; // Right boundary of the region less than pivot

for (int j = start; j < end; j++) {
if (arr[j] < pivot) { // Place elements less than pivot on the left
std::swap(arr[i], arr[j]);
i++;
}
}
std::swap(arr[i], arr[end]); // Place pivot in the correct position
return i; // Return the final position of pivot
}

void quick_sort(vector<int> &arr, int start, int end)
{
if (start >= end)
return;
int p = partition(arr, start, end);
quick_sort(arr, start, p - 1);
quick_sort(arr, p + 1, end);
}

#include <stdio.h>
int main()
{
vector<int> arr = { 1, 3, 6, 2, 3, 8, 4, 9, 0 };
quick_sort(arr, 0, arr.size() - 1);
for(int num: arr){
printf("%d ", num);
}
printf("\n");
}

Hoare partition

It does not require pivot to end up in the correct position, but guarantees:

  • [start..left1]<pivot[start .. left-1] < pivot
  • [left..end]>=pivot[left .. end] >= pivot
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <vector>
using std::vector;


// 4、5、8、1、7、2、6、3 // pivot = 4, left = arr[0]=4 right=arr[n-1] =3

// 3, 5, 8, 1, 7, 2, 6, 4 // left->5 right->6

// 3, 2, 8, 1, 7, 5, 6, 4 // left->8 right->7

// 3, 2, 1, 8, 7, 5, 6, 4 // left->1 right->8 right<left, stop

/*
* Hoare partition normal logic of,It does not require pivot end up in the correct position,but guarantees:
* [start .. left-1] < pivot
* [left .. end] >= pivot
*/

int partition(vector<int> &arr, int start, int end)
{
int pivot = arr[start];
int left = start, right = end;

while (left <= right) {
while (arr[left] < pivot) {
left++;
}
while (arr[right] > pivot) {
right--;
}
if (left <= right) {
std::swap(arr[left], arr[right]);
left++;
right--;
}
}
return left;
}

void quick_sort(vector<int> &arr, int start, int end)
{
if (start >= end) {
return;
}
int p;
p = partition(arr, start, end);
quick_sort(arr, start, p - 1);
quick_sort(arr, p, end);
}

#include <stdio.h>
int main()
{
vector<int> arr = { 1, 3, 6, 2, 3, 8, 4, 9, 0 };
quick_sort(arr, 0, arr.size() - 1);
for(int num: arr){
printf("%d ", num);
}
printf("\n");
}

This can also be written (recommended approach)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Quick sort function (Hoare partition scheme implementation)
void quickSort(vector<int>& nums, int l, int r) {
// 1. Recursive termination condition: when the subarray length is 1 or empty, no sorting is needed
if (l >= r) return;

// 2. Initialize partition parameters:
// - i initially at left boundary minus one (l-1)
// - j initially at right boundary plus one (r+1)
// - Pivot value x selects the middle element (to avoid worst-case time complexity in extreme cases like fully sorted arrays)
int i = l - 1, j = r + 1;
int x = nums[(l + r) >> 1]; // Bitwise operation replaces (l + r) / 2, equivalent and more efficient

// 3. Hoare partition loop: two pointers move from both ends toward the middle
while (i < j) {
// 3.1 Move left pointer i: skip all elements less than x until finding an element >= x
do i++; while (nums[i] < x);

// 3.2 Move right pointer j: skip all elements greater than x until finding an element <= x
do j--; while (nums[j] > x);

// 3.3 If pointers have not crossed, swap elements at left and right pointers (ensuring left <= x, right >= x)
if (i < j) swap(nums[i], nums[j]);
}

// 4. Recursively sort left and right subarrays:
// - Partition point is j (because when i >= j, j is the rightmost end of the left half)
// - Left subarray: [l, j] (all elements <= x)
// - Right subarray: [j+1, r] (all elements >= x)
quickSort(nums, l, j);
quickSort(nums, j + 1, r);
}

Reference Problems

ProblemLink
P215 Kth Largest Element in an Array

Heap Sort

ForComplete Binary TreeNumbering the nodes, the following relationship holds

Item1-based Indexing(root = 1)0-based Indexing(root=0)
left childleft(i)=2i\text{left}(i)=2i left(i)=2i+1 \text{left}(i)=2i+1
right childright(i)=2i+1 \text{right}(i)=2i+1 right(i)=2i+2 \text{right}(i)=2i+2
parent nodeparent(i)=i/2i>1 \text{parent}(i)=\lfloor i/2 \rfloor(i>1)parent(i)=(i1)/2i>0 \text{parent}(i)=\lfloor (i-1)/2 \rfloor(i>0)
condition for having at least a left child2in 2i \le n 2i+1<n 2i+1 < n
condition for existence of right child2i+1n 2i+1 \le n 2i+2<n 2i+2 < n
leaf node conditioni>n/2i > \lfloor n/2 \rfloori>(n2)/2 i > \lfloor (n-2)/2 \rfloor or2i+1n2i+1 \ge n
condition for having only left child2i=n2i = n 2i+1<n 2i+1 < n and 2i+2n 2i+2 \ge n
depth (root level=1)log2i+1 \lfloor \log_2 i \rfloor +1 log2(i+1)+1 \lfloor \log_2 (i+1) \rfloor +1
depth (root level=0)log2i \lfloor \log_2 i \rfloor log2(i+1) \lfloor \log_2 (i+1) \rfloor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <vector>
using std::vector;

void heapify(vector<int> &vec, int curr, int n)
{
int left = curr * 2 + 1;
int right = curr * 2 + 2;
int largest = curr;

if (left < n && vec[left] > vec[largest]) {
largest = left;
}

if (right < n && vec[right] > vec[largest]) {
largest = right;
}

if (largest != curr) {
std::swap(vec[largest], vec[curr]);
//Since the parent and child nodes have been swapped, it may affect the subtree of the child node, so adjust the subtree of the child node.
heapify(vec, largest, n);
}
}

void buildMaxHeap(vector<int> &vec)
{
int n = vec.size();
// The position of the last node is n-1, so the position of the parent node is (n-1-1)/2.
for (int i = (n - 2) / 2; i >= 0; i--) {
heapify(vec, i, n);
}
}

#include <stdio.h>

int main()
{
vector<int> nums = { 9, 8, 2, 1, 5, 3, 0, 10, 22, 5 };
int n = nums.size();
buildMaxHeap(nums);

for (int i = 0; i < n; i++) {
// pop
std::swap(nums[0], nums[n - i - 1]);
heapify(nums, 0, n - i - 1); // heap length is n-i-1
}
// Output sorted array
for (int i = 0; i < n; i++)
printf("%d ", nums[i]);
printf("\n");
}
TitleLink
P3408 Design Task Manager
P407 Trapping Rain Water II✨✨✨
P215 Kth Largest Element in an Array
P373 Find K Pairs with Smallest Sums
P295 Find Median from Data Stream✨✨✨
P239 Sliding Window Maximum
P347 Top K Frequent Elements
P502 IPO

Divide and Conquer

The essence of binary search is:Finding the position of the first element in a sequence that satisfies a certain condition

What often confuses people in binary search is: when to use less than or equal to in the while loop, and when not to use the equal sign;

First consider elements sorted in ascending order (descending is equivalent), which should be divided into two cases:

  1. No duplicate elements;

The problem in this case is generally: find whether a target element appears in the sequence, if it does, return its index; if not, return the position where it should be inserted.

That is, determine an interval [x, x], where target is the element at position x; if this element does not exist, the determined interval is [x+1, x], with the left boundary greater than the right boundary, meaning no element exists in the interval.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <vector>
using std::vector;

class Solution {
public:
int searchInsert(vector<int> &nums, int target)
{
int n = nums.size();
int left = 0, right = n - 1, mid = 0;

while (left <= right) {
mid = left + (right - left) / 2;

if (nums[mid] < target)
left = mid + 1;
else if (nums[mid] > target)
right = mid - 1;
else
return mid;
}


return left;
}
};
  1. With duplicate elements. The latter is a generalization of the former, meaning the algorithm for the latter also applies to the former.

The problem in this case is to determine the index of the first element greater than or equal to target, and the index of the first element greater than target, two subproblems.

(1) Find the lower bound (the index of the first element greater than or equal to target, which is x in [x, y))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int LowerBound(const std::vector<int>& a, int target)
{
int left = 0, right = a.size(), mid;
while (left < right) {
mid = left + (right - left) / 2;
if (a[mid] >= target) {
// If the middle element is greater than or equal to target, search in the left subinterval [left, mid]
right = mid;
} else {
// If the middle element is less than target, search in the right subinterval [mid+1, right]
left = mid + 1;
}
}
// When left == right, returning either is fine
return left;
}

(2) Find the upper bound (the index of the first element greater than target, which is y in [x, y))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int UpperBound(const std::vector<int>& a, int target)
{
int left = 0, right = a.size(), mid;
while (left < right) {
mid = left + (right - left) / 2;
if (a[mid] > target) {
// If the middle element is greater than target, search in the left subinterval [left, mid]
right = mid;
} else {
// If the middle element is less than or equal to target, search in the right subinterval [mid+1, right]
left = mid + 1;
}
}
// When left == right, returning either is fine
return left;
}

Take the following problem as an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <vector>

using std::vector;

class Solution {
private:
int search_range_end(vector<int> &nums, int target)
{
int n = nums.size();
int left = 0, right = n - 1, mid;
while (left < right) {
mid = left + (right - left + 1) / 2;
if (nums[mid] <= target)
left = mid;
else
right = mid - 1;
}

return (nums[left] == target) ? left : -1;
}
int search_range_start(vector<int> &nums, int target)
{
int n = nums.size();
int left = 0, right = n - 1, mid;
while (left < right) {
mid = left + (right - left) / 2;
if (nums[mid] >= target)
right = mid;
else
left = mid + 1;
}

return (nums[left] == target) ? left : -1;
}

public:
vector<int> searchRange(vector<int> &nums, int target)
{
if (nums.empty())
return { -1, -1 };

vector<int> res(2);
res[0] = search_range_start(nums, target);
res[1] = search_range_end(nums, target);
return res;
}
};

When finding the end, use the upper mid:mid = left + (right - left + 1) / 2;This ensures mid > left, so the interval definitely shrinks.

ProblemLink
P34 Find First and Last Position of Element in Sorted Array✨✨✨
P108 Convert Sorted Array to Binary Search Tree
P148 Sort List
P427 Construct Quad Tree
P23 Merge k Sorted Lists
P35 Search Insert Position
P74 Search a 2D Matrix
P240 Search a 2D Matrix II✨✨✨
P162 Find Peak Element
P33 Search in Rotated Sorted Array
P153 Find Minimum in Rotated Sorted Array
P4 Median of Two Sorted Arrays

Union-Find

ProblemLink
P3607 Grid Maintenance