跳转至

Csp s ds

3.1 线性结构

3.1.1 双端栈

​ 双端栈(Double Ended Stack)是一种数据结构,它允许从两端(顶部和底部)执行入栈(push)和出栈(pop)操作。双端栈可以看作是两个栈共享同一个存储空间,一个栈从数组的头部开始增长,另一个栈从数组的尾部开始增长。这种结构使得双端栈在某些场景下具有更高的空间利用率。

下面是一个基本的双端栈C++实现:

C++
 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
#include <iostream>
#include <stdexcept>

class DoubleEndedStack {
  private:
    int maxSize;
    int* stackArray;
    int top1;
    int top2;

  public:
    DoubleEndedStack(int size) : maxSize(size), top1(-1), top2(size) {
      stackArray = new int[maxSize];
    }

    ~DoubleEndedStack() {
      delete[] stackArray;
    }

    // 向第一个栈压入元素
    void push1(int value) {
      if (top1 < top2 - 1) {
        stackArray[++top1] = value;
      } else {
        throw std::runtime_error("Stack overflow");
      }
    }

    // 向第二个栈压入元素
    void push2(int value) {
      if (top1 < top2 - 1) {
        stackArray[--top2] = value;
      } else {
        throw std::runtime_error("Stack overflow");
      }
    }

    // 从第一个栈弹出元素
    int pop1() {
      if (top1 >= 0) {
        return stackArray[top1--];
      } else {
        throw std::runtime_error("Stack underflow");
      }
    }

    // 从第二个栈弹出元素
    int pop2() {
      if (top2 < maxSize) {
        return stackArray[top2++];
      } else {
        throw std::runtime_error("Stack underflow");
      }
    }

    // 检查第一个栈是否为空
    bool isEmpty1() {
      return top1 == -1;
    }

    // 检查第二个栈是否为空
    bool isEmpty2() {
      return top2 == maxSize;
    }
};

int main() {
  DoubleEndedStack des(10);

  des.push1(1);
  des.push2(20);
  des.push1(2);
  des.push2(19);

  std::cout << "Stack 1 Pop: " << des.pop1() << std::endl;
  std::cout << "Stack 2 Pop: " << des.pop2() << std::endl;

  return 0;
}

​ 在这个实现中,我们定义了一个DoubleEndedStack类,它有一个动态分配的整数数组stackArray,以及两个栈顶指针top1top2。我们实现了push1push2pop1pop2isEmpty1isEmpty2方法,分别用于执行双端栈的基本操作。

​ 双端栈的一个重要应用是在处理具有限制性的资源分配问题,如内存管理。它可以有效地利用有限的空间来实现两个独立的栈,从而提高内存利用率。以下是两个双端栈的应用案例:

1、表达式求值:

​ 在表达式求值中,我们需要处理两种不同类型的数据:操作数(数字)和操作符(如加、减、乘、除等)。我们可以使用双端栈来分别存储操作数和操作符,以便在计算过程中轻松地从两个栈之间移动数据。例如,当我们遇到一个操作符时,可以从操作数栈中弹出所需的操作数,执行相应的操作,然后将结果推入操作数栈。双端栈可以使我们以更高效的方式处理这些操作。

2、回文字符串检测:

​ 回文字符串是指正读和反读都相同的字符串。我们可以使用双端栈来检测一个字符串是否是回文。首先,将字符串的前半部分压入栈1,然后将字符串的后半部分压入栈2。接下来,逐个比较从栈1和栈2弹出的字符。如果所有字符都相等,则给定字符串是回文;否则,它不是回文。这种方法可以在O(n)时间内检测回文字符串,其中n是字符串的长度。

​ 双端栈在这些应用场景中的优势在于,它可以在一个连续的内存块中实现两个独立的栈,从而提高内存利用率。然而,双端栈的一个潜在缺点是它可能会导致栈溢出。为了避免这种情况,可以在实现中加入适当的错误检查和处理。

3.1.2 双端队列

​ 双端队列(Double Ended Queue,简称Deque)是一种允许在两端(头部和尾部)进行插入和删除操作的数据结构。双端队列是栈和队列的泛化,可以在头部执行队列操作(enqueue)和在尾部执行栈操作(push)。

以下是一个简单的C++双端队列实现:

C++
  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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#include <iostream>
#include <stdexcept>

class Deque {
  private:
    int maxSize;
    int* dequeArray;
    int front;
    int rear;

  public:
    Deque(int size) : maxSize(size), front(-1), rear(0) {
      dequeArray = new int[maxSize];
    }

    ~Deque() {
      delete[] dequeArray;
    }

    // 向双端队列头部插入元素
    void insertFront(int value) {
      if (isFull()) {
        throw std::runtime_error("Deque overflow");
      }

      if (front == -1) {
        front = 0;
        rear = 0;
      } else if (front == 0) {
        front = maxSize - 1;
      } else {
        front = front - 1;
      }

      dequeArray[front] = value;
    }

    // 向双端队列尾部插入元素
    void insertRear(int value) {
      if (isFull()) {
        throw std::runtime_error("Deque overflow");
      }

      if (front == -1) {
        front = 0;
        rear = 0;
      } else if (rear == maxSize - 1) {
        rear = 0;
      } else {
        rear = rear + 1;
      }

      dequeArray[rear] = value;
    }

    // 从双端队列头部删除元素
    int deleteFront() {
      if (isEmpty()) {
        throw std::runtime_error("Deque underflow");
      }

      int temp = dequeArray[front];

      if (front == rear) {
        front = -1;
        rear = -1;
      } else if (front == maxSize - 1) {
        front = 0;
      } else {
        front = front + 1;
      }

      return temp;
    }

    // 从双端队列尾部删除元素
    int deleteRear() {
      if (isEmpty()) {
        throw std::runtime_error("Deque underflow");
      }

      int temp = dequeArray[rear];

      if (front == rear) {
        front = -1;
        rear = -1;
      } else if (rear == 0) {
        rear = maxSize - 1;
      } else {
        rear = rear - 1;
      }

      return temp;
    }

    // 判断双端队列是否为空
    bool isEmpty() {
      return front == -1;
    }

    // 判断双端队列是否已满
    bool isFull() {
      return (front == 0 && rear == maxSize - 1) || (front == rear + 1);
    }
};

int main() {
  Deque deque(5);

  deque.insertFront(1);
  deque.insertRear(2);
  deque.insertFront(3);
  deque.insertRear(4);

  std::cout << "Deleted from front: " << deque.deleteFront() << std::endl;
  std::cout << "Deleted from rear: " << deque.deleteRear() << std::endl;

  return 0;
}

​ 在这个实现中,我们定义了一个Deque类,它具有一个动态分配的整数数组dequeArray,以及指向队列头部和尾部的frontrear指针。我们实现了insertFrontinsertReardeleteFrontdeleteRearisEmptyisFull方法,分别用于执行双端队列的基本操作。

​ 在main函数中,我们创建了一个大小为5的双端队列,并向其头部和尾部插入了一些元素。接着,我们从双端队列的头部和尾部删除并输出元素。

​ 这个简单的实现展示了双端队列的基本功能。实际上,C++标准库已经提供了一个双端队列的实现std::deque,可以在<deque>头文件中找到。std::deque是一个模板类,可以用于任何数据类型,并提供了更多功能和优化。因此,在实际项目中,建议使用std::deque而不是自定义实现。

​ 双端队列(Deque)在许多应用中非常有用,因为它允许在两端执行插入和删除操作,提供了更大的灵活性。以下是双端队列的一些应用案例:

1、滑动窗口问题:

​ 在处理滑动窗口问题时,双端队列可以用于存储窗口内的元素,并在窗口滑动时快速添加新元素和删除旧元素。例如,在查找数组中大小为k的窗口的最大值时,可以使用双端队列存储窗口内的最大值的索引,这样在每次窗口滑动时都能快速找到最大值。

2、实现双栈数据结构:

​ 双端队列可以用于实现一个双栈数据结构,其中一个栈从队列头部开始增长,另一个栈从队列尾部开始增长。这样,可以在一个连续的内存块中实现两个独立的栈,提高内存利用率。

3、用作具有可变大小的栈或队列:

​ 双端队列可以充当具有可变大小的栈或队列,以便在需要时动态地添加或删除元素。当只在一端进行操作时,它的行为类似于栈或队列,但具有更大的灵活性。

4、用于回文检查:

​ 双端队列可以用于检查字符串是否是回文。首先,将字符串的每个字符插入双端队列。然后,每次从队列的两端删除并比较字符,直到队列为空或只剩一个字符。如果所有字符对都相等,那么字符串是回文。

5、缓存策略:

​ 在计算机科学中,双端队列可以用于实现缓存策略,如Least Recently Used(LRU)算法。在这种情况下,双端队列用于存储最近访问的缓存项。当需要从缓存中删除最近最少使用的项时,可以从双端队列的一端删除,而将新的缓存项添加到双端队列的另一端。

​ 总之,双端队列是一种灵活且功能强大的数据结构,适用于许多不同的场景。在实际项目中,可以使用C++标准库中的std::deque类,它提供了双端队列的高效实现。

3.1.3 单调队列

​ 单调队列(Monotonic Queue)是一种特殊类型的数据结构,通常用于解决滑动窗口问题。它的主要特点是在队列中维护一个单调递增或单调递减的元素顺序。单调队列可以帮助我们在O(1)的时间复杂度内获得滑动窗口内的最大值或最小值。此外,单调队列在插入和删除操作中保持其单调性。

​ 单调队列可以使用双端队列来实现。以下是一个基本的单调递减队列实现(用于维护滑动窗口内的最大值):

C++
 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 <iostream>
#include <deque>
#include <vector>

class MonotonicQueue {
  private:
    std::deque<int> monoQueue;

  public:
    // 向单调队列中插入元素
    void push(int value) {
      // 删除队列中所有小于新元素的值,以保持单调递减性
      while (!monoQueue.empty() && monoQueue.back() < value) {
        monoQueue.pop_back();
      }
      monoQueue.push_back(value);
    }

    // 从单调队列中删除元素
    void pop(int value) {
      // 如果队列头部的元素等于要删除的值,则删除它
      if (!monoQueue.empty() && monoQueue.front() == value) {
        monoQueue.pop_front();
      }
    }

    // 获取单调队列中的最大值
    int getMax() {
      return monoQueue.empty() ? -1 : monoQueue.front();
    }
};

std::vector<int> slidingWindowMax(const std::vector<int>& nums, int k) {
  MonotonicQueue monoQueue;
  std::vector<int> result;

  for (int i = 0; i < nums.size(); ++i) {
    // 向单调队列插入元素
    monoQueue.push(nums[i]);

    // 当窗口向右滑动时,从单调队列中删除元素
    if (i >= k - 1) {
      result.push_back(monoQueue.getMax());
      monoQueue.pop(nums[i - k + 1]);
    }
  }

  return result;
}

int main() {
  std::vector<int> nums = {1, 3, -1, -3, 5, 3, 6, 7};
  int k = 3;
  std::vector<int> result = slidingWindowMax(nums, k);

  for (int val : result) {
    std::cout << val << " ";
  }

  return 0;
}

​ 在这个实现中,我们定义了一个MonotonicQueue类,用双端队列monoQueue维护单调递减顺序。我们实现了pushpopgetMax方法,分别用于插入元素、删除元素和获取队列中的最大值。

slidingWindowMax函数接受一个整数数组nums和一个窗口大小k,使用MonotonicQueue计算并返回每个滑动窗口内的最大值。

单调队列除了在滑动窗口问题中求最大值或最小值之外,还可以用于解决其他问题。以下是一些单调队列的其他应用:

1、求解下一个更大(或更小)元素:

​ 给定一个数组,要求找出数组中每个元素的右侧第一个比它大(或小)的元素。可以使用单调队列解决这个问题。遍历数组,使用单调递减(或递增)队列维护元素的索引。当遇到一个更大(或更小)的元素时,从队列中弹出元素并记录它们的右侧更大(或更小)元素。

2、最长递增子序列的长度:

​ 对于一个未排序的数组,可以使用单调队列找到最长递增子序列的长度。遍历数组中的每个元素,将其插入到单调递增队列中的适当位置。最后,单调队列的长度即为最长递增子序列的长度。

3、股票交易问题:

​ 在股票交易问题中,我们需要找到某个时间段内的最佳买卖时机。可以使用单调队列在O(n)时间内找到最佳买卖时机。维护一个单调递增队列,其中存储每个时间段的股票价格。在遍历过程中,计算当前价格与队列中最低价格的差值,并更新最大利润。

4、在图像处理中找到局部最大值或最小值:

​ 在处理数字图像时,我们可能需要找到局部最大值或最小值以进行特征提取或滤波。可以在每个像素周围创建一个滑动窗口,并使用单调队列快速找到窗口内的最大值或最小值。

​ 这些仅是单调队列的一部分应用。由于它能够在O(1)时间内获取队列中的最大值或最小值,并在插入和删除操作中保持单调性,因此单调队列在解决许多实际问题中具有很大的价值。

3.1.4 优先队列

​ 优先队列(Priority Queue)是一种特殊类型的队列数据结构,其中每个元素都具有一个与之相关的优先级。在优先队列中,具有最高优先级的元素先出队。如果两个或多个元素具有相同的优先级,它们的出队顺序通常是按照它们插入队列的顺序。优先队列可以用于解决需要在一组元素中快速找到最大值或最小值的问题。

​ 优先队列可以使用不同的数据结构实现,例如数组、链表、有序数组等。但最常用且效率最高的实现是使用二叉堆(Binary Heap),特别是最小堆(Min Heap)和最大堆(Max Heap)。二叉堆是一种完全二叉树(除最后一层外,所有层都填充满了元素),并满足堆的性质:对于最大堆,每个节点的值大于或等于其子节点的值;对于最小堆,每个节点的值小于或等于其子节点的值。

以下是C++中使用std::priority_queue实现的一个优先队列示例:

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <queue>

int main() {
    // 创建一个最大堆优先队列
    std::priority_queue<int> maxHeap;

    // 向优先队列插入元素
    maxHeap.push(3);
    maxHeap.push(5);
    maxHeap.push(1);
    maxHeap.push(7);

    // 获取优先队列中的最大值
    std::cout << "Max value: " << maxHeap.top() << std::endl;

    // 删除优先队列中的最大值
    maxHeap.pop();

    // 获取优先队列中的新最大值
    std::cout << "New max value: " << maxHeap.top() << std::endl;

    return 0;
}

​ 在这个示例中,我们创建了一个std::priority_queue对象,用于存储整数值。默认情况下,std::priority_queue实现为最大堆。我们向队列中插入了一些元素,并使用top()方法获取队列中的最大值。然后,我们使用pop()方法删除最大值,并再次获取队列中的新最大值。

​ 优先队列在许多实际应用中非常有用,以下是一些优先队列的典型应用案例:

1、任务调度:

​ 在操作系统或其他任务调度系统中,需要根据任务的优先级对任务进行调度。使用优先队列可以有效地实现这种调度机制,使具有更高优先级的任务先执行。

2、Dijkstra算法:

​ 在图论中,Dijkstra算法是一种求解单源最短路径问题的算法。使用优先队列可以有效地实现该算法,因为在每个步骤中,需要找到具有最小距离的未访问顶点。优先队列可以在O(log n)时间内找到最小值并更新距离。

3、A*搜索算法:

​ A*搜索算法是一种广泛用于路径规划和图搜索的启发式算法。该算法使用优先队列来存储待处理的节点,并根据启发式评估函数为每个节点分配优先级。优先队列确保了具有最低评估成本的节点最先被处理。

4、Huffman编码:

​ 在数据压缩中,Huffman编码是一种使用变长编码表进行无损数据压缩的方法。Huffman编码算法需要构建一个表示字符频率的最优二叉树。优先队列可以用于有效地构建这个树,因为它可以快速找到具有最低频率的节点。

5、模拟事件处理:

​ 在离散事件模拟中,系统中的事件按照时间顺序发生。优先队列可以用于存储和管理这些事件,以便按照预定的时间顺序处理它们。

6、数据流处理:

​ 在处理大量数据流时,可能需要找到数据流中的最大或最小值,或者维护数据流中的某种排序。优先队列可以帮助解决这类问题,因为它能够在O(log n)时间内插入元素并获取最大或最小值。

7、拓扑排序:

​ 在解决依赖关系排序问题时,例如在项目管理或课程安排中,可以使用优先队列实现拓扑排序。优先队列可以帮助确定按照依赖关系执行的任务顺序。

​ 这些仅是优先队列的一部分应用。由于其能够快速找到最大值或最小值并执行插入和删除操作,优先队列在许多实际场景中都具有很大的价值。在C++中,可以使用std::priority_queue类来实现优先队列。

3.1.5 ST 表(Sparse Table)

​ Sparse Table(稀疏表)是一种用于解决区间查询问题的数据结构。它主要用于处理一些不可修改的数组,并在O(1)时间内对数组的某个区间进行查询。Sparse Table适用于处理与区间长度无关的查询问题,例如最小值、最大值、最大公约数等。它的主要优点是预处理时间复杂度为O(n log n),空间复杂度为O(n log n),查询时间复杂度为O(1)。

​ Sparse Table的基本思想是使用动态规划进行预处理,将查询区间分解为若干个子区间,然后利用子区间的信息进行查询。预处理阶段,Sparse Table使用一个二维数组ST来存储所有可能的子区间结果。对于数组长度为n的情况,我们需要log(n)层来存储所有可能的子区间结果。其中,ST[i][j]表示从第i个元素开始,长度为2^j的区间的查询结果。

以下是一个基本的Sparse Table实现,用于查询给定数组中某个区间的最小值:

C++
 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
#include <iostream>
#include <vector>
#include <cmath>

class SparseTable {
private:
    std::vector<std::vector<int>> ST;
    std::vector<int> logValues;

public:
    SparseTable(const std::vector<int>& nums) {
        int n = nums.size();
        int maxLog = int(log2(n)) + 1;

        ST.resize(n, std::vector<int>(maxLog));
        logValues.resize(n + 1);

        // 初始化ST表的第一层
        for (int i = 0; i < n; ++i) {
            ST[i][0] = nums[i];
        }

        // 使用动态规划进行预处理
        for (int j = 1; j < maxLog; ++j) {
            for (int i = 0; i + (1 << j) <= n; ++i) {
                ST[i][j] = std::min(ST[i][j - 1], ST[i + (1 << (j - 1))][j - 1]);
            }
        }

        // 预处理logValues数组
        logValues[1] = 0;
        for (int i = 2; i <= n; ++i) {
            logValues[i] = logValues[i / 2] + 1;
        }
    }

    // 查询区间[l, r]的最小值
    int query(int l, int r) {
        int j = logValues[r - l + 1];
        return std::min(ST[l][j], ST[r - (1 << j) + 1][j]);
    }
};

int main() {
    std::vector<int> nums = {1, 3, 4, 8, 6, 1, 4, 2};
    SparseTable sparseTable(nums);

    // 查询区间[1, 6]的最小值
    std::cout << "Minimum value in range [1, 6]: " << sparseTable.query(1, 6) << std::endl;

    return 0;
}

​ 在这个实现中,我们定义了一个SparseTable类,用于存储给定数组的预处理结果。构造函数首先初始化了二维数组ST,然后使用动态规划填充了表格。我们还计算了一个辅助的logValues数组,用于在查询阶段加速计算。

query函数用于查询给定区间[l, r]的最小值。首先,我们找到最大的2的幂次j,使得2^j <= r - l + 1。然后,我们可以在O(1)时间内使用ST表中的两个子区间结果计算整个区间的最小值:ST[l][j]和ST[r - (1 << j) + 1][j]。

​ 在main函数中,我们使用示例数组创建了一个SparseTable实例,并查询了区间[1, 6]的最小值。

​ Sparse Table适用于解决一些特定类型的区间查询问题,尤其是在数据不变且查询过程中不需要更新的情况下。然而,对于需要支持修改操作的问题,Sparse Table可能不是最佳选择,因为它的预处理成本较高。在这种情况下,可以考虑使用其他数据结构,如线段树或树状数组。

3.2 集合与森林

3.2.1 并查集

​ 并查集(Disjoint Set,也称为Union-Find)是一种用于处理不相交集合(Disjoint Sets)问题的数据结构。它主要用于解决一类涉及动态连接与判断连通性的问题,例如网络连接、朋友圈等。并查集主要支持两种操作:连接(Union)和查找(Find)。

  1. Union操作:将两个不相交的集合合并为一个集合。即将两个元素所在的集合进行合并。
  2. Find操作:查询一个元素所属的集合。即查找元素的代表(通常是集合中的一个特定元素,例如根结点)。

​ 并查集的基本实现使用一个一维数组(或向量)表示集合的父节点。每个元素的父节点可以表示为它所在集合的代表元素。初始状态下,每个元素都是它自己的代表,即它们各自属于一个只包含自己的集合。

​ 并查集的核心操作是路径压缩和按秩合并。路径压缩是在Find操作过程中实现的,它将查询路径上的每个元素直接连接到代表元素,从而减少了查询和连接操作的时间复杂度。按秩合并是在Union操作中实现的,它将较小的集合连接到较大的集合上,从而保证合并后的树的高度尽可能小。

​ 以下是一个基本的并查集实现:

C++
 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
#include <iostream>
#include <vector>

class UnionFind {
private:
    std::vector<int> parent;
    std::vector<int> rank;

public:
    UnionFind(int n) {
        parent.resize(n);
        rank.resize(n);
        for (int i = 0; i < n; ++i) {
            parent[i] = i;
            rank[i] = 1;
        }
    }

    int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]);
        }
        return parent[x];
    }

    void unite(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);

        if (rootX == rootY) {
            return;
        }

        if (rank[rootX] > rank[rootY]) {
            parent[rootY] = rootX;
        } else {
            parent[rootX] = rootY;
            if (rank[rootX] == rank[rootY]) {
                rank[rootY]++;
            }
        }
    }

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

int main() {
    UnionFind uf(5);

    uf.unite(0, 1);
    uf.unite(1, 2);
    uf.unite(3, 4);

    std::cout << "Are 0 and 2 connected? " << (uf.connected(0, 2) ? "Yes" : "No") << std::endl;
    std::cout << "Are 1 and 4 connected? " << (uf.connected(1, 4) ? "Yes" : "No") << std::endl;
    return 0;
}

​ 在这个实现中,我们定义了一个UnionFind类,用于表示并查集。构造函数初始化了parent和rank数组。find函数用于查找元素的代表元素,同时实现了路径压缩。unite函数用于将两个元素所在的集合合并,并实现了按秩合并。connected函数用于判断两个元素是否属于同一个集合。 在main函数中,我们创建了一个包含5个元素的并查集实例,并将一些元素合并到同一个集合中。接下来,我们查询了两对元素之间的连通性。 并查集在解决动态连通性问题时非常高效,它的时间复杂度在实际应用中接近O(1)。然而,并查集并不适用于所有类型的集合操作,例如它不能直接支持删除元素或获取集合大小等操作。在这种情况下,可能需要使用其他数据结构,如线段树或树状数组。

3.2.2 树的孩子兄弟表示法

​ 树的孩子兄弟表示法(Child-Sibling Representation)是一种将普通树(多叉树)转换为二叉树的表示方法。这种表示法的优点是能够通过简化树的结构来简化树的操作,同时减少存储空间的需求。

​ 在孩子兄弟表示法中,每个结点有两个指针:一个指向它的第一个孩子(Child),另一个指向它的右侧兄弟(Sibling)。通过这两个指针,我们可以将多叉树转换为二叉树。每个节点的孩子指针指向其第一个子节点,兄弟指针指向其在同一层的下一个兄弟节点。

以下是一个简单的C++实现:

C++
 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 <iostream>

struct TreeNode {
    int data;
    TreeNode* firstChild;
    TreeNode* nextSibling;

    TreeNode(int data) : data(data), firstChild(nullptr), nextSibling(nullptr) {}
};

void addChild(TreeNode* parent, int data) {
    TreeNode* newNode = new TreeNode(data);

    if (parent->firstChild == nullptr) {
        parent->firstChild = newNode;
    } else {
        TreeNode* sibling = parent->firstChild;
        while (sibling->nextSibling != nullptr) {
            sibling = sibling->nextSibling;
        }
        sibling->nextSibling = newNode;
    }
}

int main() {
    TreeNode* root = new TreeNode(1);

    addChild(root, 2);
    addChild(root, 3);
    addChild(root, 4);

    addChild(root->firstChild, 5);
    addChild(root->firstChild, 6);

    addChild(root->firstChild->nextSibling, 7);
    addChild(root->firstChild->nextSibling, 8);

    return 0;
}

​ 在这个实现中,我们定义了一个TreeNode结构体,其中包含一个数据成员和两个指针成员:firstChildnextSibling。我们还提供了一个addChild函数,用于向给定的父节点添加一个新的子节点。

main函数中,我们创建了一个树的实例,并添加了一些子节点。注意,这种表示方法适用于任意数量的子节点,因为每个节点只需维护一个指向其第一个子节点的指针,以及一个指向其下一个兄弟节点的指针。

​ 树的孩子兄弟表示法在解决树相关问题时具有一定的优势,因为它将多叉树转换为二叉树,从而简化了许多操作。然而,它可能不适用于所有场景,特别是在需要高效地执行某些操作(如查找某个节点的父节点)时。在这种情况下,可能需要考虑使用其他表示方法,如邻接表或邻接矩阵。