数据结构与算法-笔记

秋招已经大体结束了,可以说是惨不忍睹了,那么寒假需要做的事就很多了,我也想趁着这个机会好好地补充一下基础。

时间复杂度和空间复杂度的定义?

时间复杂度 关注的是算法执行所需时间随输入数据规模增长的变化趋势,而空间复杂度则关注算法运行过程中临时占用的存储空间随输入规模增长的变化趋势。

什么是高精度算法?

高精度算法是处理超大数字(如成百上千位的整数或浮点数)的计算方法,当数字太大,超出计算机常规数据类型(如int、double)的直接处理能力时,它通过将大数拆分为数字序列(通常存储在数组或字符串中),然后模拟人工竖式计算的过程,逐位处理并管理进位或借位,从而完成加、减、乘、除、乘方、开方等精确运算。

高精度加法示例:

cpp 复制代码
#include <iostream>
using namespace std;
void stringtoint(string s,int des[]) {
	int n = s.size();
	for (int i = 0; i < n; ++i) {
		des[n - i- 1] = s[i] - '0';
	}
}
int main() {
	string a, b;
	int c[101] = { 0 }, d[101] = { 0 }, e[101] = { 0 };
	int la = 0, lb = 0, lc = 0;
	cin >> a >> b;
	stringtoint(a,c);
	stringtoint(b,d);
	la = a.size(), lb = b.size();
	lc = max(la, lb) + 1;
	for (int i = 0; i < lc; ++i) {
		e[i] += c[i] + d[i];
		e[i + 1] = e[i] / 10;
		e[i] %= 10;
	}
	while (e[lc - 1] == 0)lc--;
	for (int i = lc - 1; i >= 0; --i)cout << e[i];
}

结果如图所示:

在这个基础上我们也可以实现高精度减法:

cpp 复制代码
#include <iostream>
#include <algorithm> // 用于 max
using namespace std;

void stringtoint(string s, int des[]) {
    int n = s.size();
    for (int i = 0; i < n; ++i) {
        des[n - i - 1] = s[i] - '0'; 
    }
}

bool cmpstr(string s1, string s2) {
    if (s1.size() != s2.size())
        return s1.size() > s2.size();
    else
        return s1 >= s2; 
}

int main() {
    string a, b;
    int c[101] = { 0 }, d[101] = { 0 }, e[101] = { 0 }; 
    int la, lb, lc;
    cin >> a >> b;


    if (!cmpstr(a, b)) {
        swap(a, b);
        cout << "-"; 
    }

    stringtoint(a, c);
    stringtoint(b, d);
    la = a.size();
    lb = b.size();
    lc = max(la, lb);


    int borrow = 0;
    for (int i = 0; i < lc; ++i) {
        int digit_c = c[i];
        int digit_d = (i < lb) ? d[i] : 0; 
        int temp = digit_c - digit_d - borrow;

        if (temp < 0) {
            temp += 10;
            borrow = 1;
        }
        else {
            borrow = 0;
        }
        e[i] = temp;
    }


    while (lc > 1 && e[lc - 1] == 0) {
        lc--;
    }


    for (int i = lc - 1; i >= 0; --i) {
        cout << e[i];
    }
    return 0;
}

结果如图:

高精度乘法:

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
using namespace std;

vector<int> multiply(vector<int>& A, vector<int>& B) {
    int la = A.size(), lb = B.size();
    vector<int> C(la + lb, 0); 

    for (int i = 0; i < la; ++i) {
        for (int j = 0; j < lb; ++j) {
            C[i + j] += A[i] * B[j]; 
        }
    }


    int carry = 0;
    for (int i = 0; i < C.size(); ++i) {
        C[i] += carry;
        carry = C[i] / 10;
        C[i] %= 10;
    }

    while (C.size() > 1 && C.back() == 0) {
        C.pop_back();
    }
    return C;
}

int main() {
    string a, b;
    cin >> a >> b;

    vector<int> A, B;
    for (int i = a.size() - 1; i >= 0; --i) A.push_back(a[i] - '0');
    for (int i = b.size() - 1; i >= 0; --i) B.push_back(b[i] - '0');

    auto C = multiply(A, B);

    for (int i = C.size() - 1; i >= 0; --i) {
        cout << C[i];
    }
    return 0;
}

结果如图:

高精度除法:

cpp 复制代码
#include <iostream>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;


int compare(vector<int>& A, vector<int>& B) {
    if (A.size() != B.size()) {
        return A.size() - B.size();
    }
    for (int i = A.size() - 1; i >= 0; --i) {
        if (A[i] != B[i]) {
            return A[i] - B[i];
        }
    }
    return 0; 
}

vector<int> subtract(vector<int> A, vector<int> B) {
    vector<int> C;
    int carry = 0;

    for (int i = 0; i < A.size(); ++i) {
        int temp = A[i] - carry;
        if (i < B.size()) {
            temp -= B[i];
        }
        if (temp < 0) {
            carry = 1;
            temp += 10;
        }
        else {
            carry = 0;
        }
        C.push_back(temp);
    }

    while (C.size() > 1 && C.back() == 0) {
        C.pop_back();
    }
    return C;
}


vector<int> divide(vector<int> A, vector<int> B) {
    if (compare(A, B) < 0) {
        return { 0 }; 
    }

    vector<int> C(A.size() - B.size() + 1, 0); 

    for (int i = C.size() - 1; i >= 0; --i) {
        vector<int> tempB(B.size() + i, 0);
        for (int j = 0; j < B.size(); ++j) {
            tempB[j + i] = B[j];
        }

        while (compare(A, tempB) >= 0) {
            C[i]++;
            A = subtract(A, tempB);
        }
    }


    while (C.size() > 1 && C.back() == 0) {
        C.pop_back();
    }
    return C;
}

int main() {
    string a, b;
    cin >> a >> b;

    vector<int> A, B;
    for (int i = a.size() - 1; i >= 0; --i) A.push_back(a[i] - '0');
    for (int i = b.size() - 1; i >= 0; --i) B.push_back(b[i] - '0');

    auto C = divide(A, B);

    for (int i = C.size() - 1; i >= 0; --i) {
        cout << C[i];
    }
    return 0;
}

结果如图:

为什么需要排序算法?

其核心价值在于将无序数据转化为有序序列,从而大幅提升后续数据操作的效率 ,并支撑起众多关键应用场景。具体来说,在有序数据上进行搜索(如二分查找)会比在无序数据中线性扫描快几个数量级;数据库通过排序来构建索引,加速查询响应等等。

我们有一个所谓的十大排序算法:

客观的说,运用得比较多的就是我们的三个nlogn复杂度的排序算法,因为剩下三个时间复杂度更低的排序算法往往实现起来都很麻烦,或者说需求一些特殊的条件,而且往往需要占用更大的空间复杂度。

接下来我来逐个介绍排序算法:

冒泡排序

原理:重复遍历待排序序列,依次比较相邻的两个元素,如果它们的顺序有误(比如前一个大于后一个),就交换它们的位置。这样,每一轮遍历都会将当前未排序部分中的最大(或最小)元素"冒泡"到其正确的位置上。这个过程会重复进行,直到整个序列有序。

cpp 复制代码
void bubbleSort(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                // 交换 arr[j] 和 arr[j+1]
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

选择排序

原理:算法将待排序序列分为已排序和未排序两部分。一开始,已排序部分为空。它不断地在未排序部分中寻找最小(或最大)的那个元素,然后将其与未排序部分的第一个元素进行交换,这样这个元素就被放到了已排序部分的末尾。如此重复,直到所有元素都排序完毕。

cpp 复制代码
void selectionSort(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        int minIndex = i;
        for (int j = i + 1; j < n; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }
        // 将找到的最小元素与第i个元素交换
        int temp = arr[minIndex];
        arr[minIndex] = arr[i];
        arr[i] = temp;
    }
}

插入排序

原理:它的工作方式类似于我们整理手中的扑克牌。算法将待排序序列的第一个元素视为一个已排序好的子序列,然后依次将后续的未排序元素插入到这个已排序子序列中的正确位置上。在插入过程中,它会将已排序序列中大于当前元素的那些元素都向后移动一位,从而为当前元素腾出插入空间。

cpp 复制代码
void insertionSort(int arr[], int n) {
    for (int i = 1; i < n; i++) {
        int key = arr[i]; // 待插入的元素
        int j = i - 1;
        // 将大于key的元素向后移动
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key; // 插入到正确位置
    }
}

归并排序

原理:归并排序采用了"分而治之"的策略。它首先将待排序序列递归地分成两半,直到每个子序列只剩下一个元素(一个元素本身自然是有序的)。然后,再将这两个有序的子序列"归并"成一个新的有序序列。这个归并的过程会不断重复,自底向上地将小段有序序列合并成更大的有序序列,最终得到完全有序的序列。

cpp 复制代码
// 合并两个有序子数组 arr[l..m] 和 arr[m+1..r]
void merge(int arr[], int l, int m, int r) {
    int n1 = m - l + 1;
    int n2 = r - m;
    int L[n1], R[n2]; // 创建临时数组

    // 拷贝数据到临时数组
    for (int i = 0; i < n1; i++) L[i] = arr[l + i];
    for (int j = 0; j < n2; j++) R[j] = arr[m + 1 + j];

    // 归并临时数组到原数组arr[l..r]
    int i = 0, j = 0, k = l;
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            arr[k] = L[i];
            i++;
        } else {
            arr[k] = R[j];
            j++;
        }
        k++;
    }
    // 拷贝L[]的剩余元素
    while (i < n1) arr[k++] = L[i++];
    // 拷贝R[]的剩余元素
    while (j < n2) arr[k++] = R[j++];
}

void mergeSort(int arr[], int l, int r) {
    if (l >= r) return; // 递归基
    int m = l + (r - l) / 2;
    mergeSort(arr, l, m);     // 排序左半部分
    mergeSort(arr, m + 1, r); // 排序右半部分
    merge(arr, l, m, r);      // 合并
}

快速排序

原理:快速排序也使用分治思想。它首先从序列中挑选一个元素作为"基准"。然后,重新排列序列,将所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准后面。这个操作称为"分区"。操作结束后,基准元素就处于其最终的正确位置上。然后,再递归地对基准之前和之后的两个子序列进行快速排序。

cpp 复制代码
// 分区函数,返回基准值的最终位置
int partition(int arr[], int low, int high) {
    int pivot = arr[high]; // 选择最后一个元素作为基准
    int i = (low - 1);      // 小于基准的区域的边界索引

    for (int j = low; j <= high - 1; j++) {
        // 如果当前元素小于或等于基准
        if (arr[j] <= pivot) {
            i++; // 扩展小于基准的区域
            swap(arr[i], arr[j]); // 将当前元素交换到该区域内
        }
    }
    swap(arr[i + 1], arr[high]); // 将基准放到正确位置(即两个区域之间)
    return (i + 1);
}

void quickSort(int arr[], int low, int high) {
    if (low < high) {
        int pi = partition(arr, low, high); // 获取基准位置
        quickSort(arr, low, pi - 1);  // 递归排序基准左边的子数组
        quickSort(arr, pi + 1, high); // 递归排序基准右边的子数组
    }
}

堆排序

原理:堆排序利用了一种叫做"堆"的特殊二叉树结构(通常是大顶堆,即每个节点的值都大于或等于其子节点的值)。算法首先将待排序序列构建成一个大顶堆。此时,整个序列的最大值就是堆顶的根节点。将其与堆数组的末尾元素进行交换,这样最大值就放到了正确的位置。然后,将剩余的元素重新调整成一个大顶堆。如此反复执行,直到所有元素有序。

cpp 复制代码
// 用于调整堆的函数,确保以节点i为根的子树满足大顶堆性质
void heapify(int arr[], int n, int i) {
    int largest = i;       // 初始化最大值为根节点i
    int l = 2 * i + 1;    // 左子节点 = 2*i + 1
    int r = 2 * i + 2;    // 右子节点 = 2*i + 2

    // 如果左子节点存在且大于根
    if (l < n && arr[l] > arr[largest]) largest = l;
    // 如果右子节点存在且大于当前最大值
    if (r < n && arr[r] > arr[largest]) largest = r;
    // 如果最大值不是根节点
    if (largest != i) {
        swap(arr[i], arr[largest]);
        // 递归地调整受影响的子堆
        heapify(arr, n, largest);
    }
}

void heapSort(int arr[], int n) {
    // 构建大顶堆(从最后一个非叶子节点开始向上调整)
    for (int i = n / 2 - 1; i >= 0; i--)
        heapify(arr, n, i);

    // 逐个从堆顶取出元素
    for (int i = n - 1; i > 0; i--) {
        // 将当前堆顶(最大值)移到数组末尾
        swap(arr[0], arr[i]);
        // 对剩余元素重新调整堆结构
        heapify(arr, i, 0);
    }
}

关于贪心算法

贪心算法是一种在每一步选择中都采取当前状态下最优(即最有利)的选择,希望通过一系列局部最优决策,导致得到全局最优解的算法策略。它的核心在于"活在当下",只考虑眼前的利益最大化。

我们下面用一个简单的例题来说明:

如题所示,既然我们希望奖品的数目越多越好,那当然我们优先考虑商店中便宜的物品,先把便宜的买完再买贵的不就行了吗。

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// 定义奖品结构体
struct Prize {
    int price;      // 价格
    int stock;      // 库存
    int count;      // 购买数量
};

// 贪心算法实现
int buyMaxPrizes(vector<Prize>& prizes, int budget) {
    // 1. 按价格排序(贪心策略:优先购买便宜的)
    sort(prizes.begin(), prizes.end(), [](const Prize& a, const Prize& b) {
        return a.price < b.price;
    });
    
    int totalPrizes = 0;
    int remainingBudget = budget;
    
    // 2. 从最便宜的奖品开始购买
    for (auto& prize : prizes) {
        if (remainingBudget <= 0) break;
        
        // 计算当前奖品最多能买多少个
        int canBuy = min(prize.stock, remainingBudget / prize.price);
        if (canBuy > 0) {
            prize.count = canBuy;  // 记录购买数量
            totalPrizes += canBuy;
            remainingBudget -= canBuy * prize.price;
        }
    }
    
    return totalPrizes;
}

int main() {
    int budget = 100;  // 班费m元
    int n = 5;         // 奖品种类数
    
    // 示例数据:每种奖品的价格和库存
    vector<Prize> prizes = {
        {20, 3, 0},  // 价格20,库存3
        {15, 5, 0},  // 价格15,库存5
        {25, 2, 0},  // 价格25,库存2
        {10, 8, 0},  // 价格10,库存8
        {30, 1, 0}   // 价格30,库存1
    };
    
    int maxPrizes = buyMaxPrizes(prizes, budget);
    
    cout << "最大可购买奖品数: " << maxPrizes << endl;
    cout << "购买方案:" << endl;
    for (int i = 0; i < prizes.size(); i++) {
        if (prizes[i].count > 0) {
            cout << "奖品" << i+1 << "(价格" << prizes[i].price << "): " 
                 << prizes[i].count << "个" << endl;
        }
    }
    
    int totalCost = 0;
    for (const auto& prize : prizes) {
        totalCost += prize.count * prize.price;
    }
    cout << "总花费: " << totalCost << "元" << endl;
    
    return 0;
}

贪心算法本身并不像其他的算法一样,有着固定的模板套路,但是其思想在很多情景下确实具有可行性。

关于递推算法

递推算法是一种通过已知条件,利用特定关系逐步推导出结果的数学方法。它的核心思想是将复杂过程转化为多个简单步骤的重复。

我们依然用一个例题来说明:

这是经典的错排问题(Derangement),即n个元素都不在原来位置的排列数。我们不知道具体的n,所以显然我们要从小的数开始逐级递推,找到合理的递推公司来表达n的结果。

  • D(1) = 0 (1封信不可能装错)

  • D(2) = 1 (AB → BA)

  • 当n ≥ 3时:D(n) = (n-1) × [D(n-1) + D(n-2)]

这就是递推公式,那么我们就可以写出相应的代码了:

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

// 递推算法计算错排数
long long derangementIterative(int n) {
    if (n <= 1) return 0;
    if (n == 2) return 1;
    
    long long d1 = 0;  // D(1)
    long long d2 = 1;  // D(2)
    long long dn = 0;
    
    for (int i = 3; i <= n; i++) {
        dn = (i - 1) * (d1 + d2);
        d1 = d2;  // 更新D(n-2)
        d2 = dn;  // 更新D(n-1)
    }
    
    return d2;  // 循环结束时d2就是D(n)
}

int main() {
    int n;
    cout << "请输入信的数量n: ";
    cin >> n;
    
    cout << n << "封信全部装错信封的情况数为: " 
         << derangementIterative(n) << endl;
    
    // 输出前n个错排数
    cout << "前" << n << "个错排数:" << endl;
    for (int i = 1; i <= n; i++) {
        cout << "D(" << i << ") = " << derangementIterative(i) << endl;
    }
    
    return 0;
}

当然,熟悉算法的大家应该也能品出来了,这其实就是动态规划。毕竟动态规划里比较重要的一步就是去求出递推公式,只要获知递推公式就可以得到后面的代码怎么写。

关于递归

递归的原理就是通过将复杂问题分解为规模更小、但结构与原问题相似的子问题来工作。

递归是一类比较特定的问题,主要针对一类问题:

总结来说,要么问题是递归定义的(如阶乘),要么数据结构是递归的(如树,链表),还有一些问题确实只能用递归来解决(比如汉诺塔问题),这些问题往往都会选择使用递归来完成。这里必须提一嘴的是,递归可能算是一种优雅的写法,但并不是一种有效率的算法,他的时间复杂度往往很大且难以估计,一旦陷入无限递归还会导致栈溢出引起崩溃------总的来说,使用递归时一定要足够谨慎。

我们给一个比较简单的示例:杨辉三角。

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

// 递归生成杨辉三角的第row行
vector<int> generateRow(int row) {
    vector<int> currentRow;
    
    if (row == 0) {
        // 第0行:[1]
        currentRow.push_back(1);
        return currentRow;
    }
    
    // 递归获取上一行
    vector<int> prevRow = generateRow(row - 1);
    
    // 生成当前行
    currentRow.push_back(1); // 第一个元素是1
    
    for (int i = 1; i < row; i++) {
        currentRow.push_back(prevRow[i - 1] + prevRow[i]);
    }
    
    currentRow.push_back(1); // 最后一个元素是1
    
    return currentRow;
}

// 递归生成整个杨辉三角
vector<vector<int>> generateTriangle(int n, int current = 0) {
    vector<vector<int>> triangle;
    
    if (current >= n) {
        return triangle; // 基准情况
    }
    
    if (current == 0) {
        // 第一行
        triangle.push_back(generateRow(current));
        vector<vector<int>> rest = generateTriangle(n, current + 1);
        triangle.insert(triangle.end(), rest.begin(), rest.end());
        return triangle;
    }
    
    triangle.push_back(generateRow(current));
    vector<vector<int>> rest = generateTriangle(n, current + 1);
    triangle.insert(triangle.end(), rest.begin(), rest.end());
    return triangle;
}

int main() {
    int n = 5;
    vector<vector<int>> triangle = generateTriangle(n);
    
    cout << "递归生成的杨辉三角:" << endl;
    for (const auto& row : triangle) {
        for (int i = 0; i < n - row.size(); i++) {
            cout << "  ";
        }
        for (int num : row) {
            cout << num << "   ";
        }
        cout << endl;
    }
    
    return 0;
}

关于二分法

二分搜索(Binary Search)是一种非常高效的在有序集合中查找特定元素的算法。它的核心思想是"分而治之",通过每次比较将搜索范围缩小一半,从而快速定位目标 。

这种分治思想其实在很多地方都有体现:

二分法最大的提点就是大大减少了时间复杂度和空间复杂度,只需要ologn的时间复杂度和o1的空间复杂度。

下面提供一个简单的二分法实现:

cpp 复制代码
#include <iostream>
#include <vector>

int binarySearch(const std::vector<int>& arr, int target) {
    int left = 0;
    int right = arr.size() - 1; // 注意:初始右边界是有效索引

    while (left <= right) { // 重要:使用 <= 确保区间有效
        // 防止整数溢出的中点计算
        int mid = left + (right - left) / 2;

        if (arr[mid] == target) {
            return mid; // 找到目标,返回索引
        } else if (arr[mid] < target) {
            left = mid + 1; // 目标在右半部分
        } else {
            right = mid - 1; // 目标在左半部分
        }
    }
    return -1; // 未找到
}

int main() {
    std::vector<int> sorted_array = {1, 3, 5, 7, 9, 11, 13, 15, 17, 19};
    int target = 7;

    int result = binarySearch(sorted_array, target);

    if (result != -1) {
        std::cout << "元素 " << target << " 在索引 " << result << " 处找到。" << std::endl;
    } else {
        std::cout << "元素 " << target << " 未找到。" << std::endl;
    }

    return 0;
}

什么是倍增法?

倍增法是一种通过"成倍增长"的方式将线性处理优化为对数级处理的算法思想 。它巧妙地将二进制划分思想融入计算过程,广泛应用于快速幂、最近公共祖先(LCA)和区间最值查询(RMQ)等问题。

快速幂是倍增法的经典应用,它用于高效计算 a的b次方整除p。朴素方法需要 O(b) 次乘法,而快速幂利用倍增思想将复杂度降至 O(log b) 。

其原理是将指数 b 用二进制表示 。例如,计算 313时,13的二进制是1101,即 13=8+4+1。因此,313=38×34×31。我们只需按 31,32,34,38...的顺序倍增计算,并根据b的二进制位决定是否将当前结果乘入最终答案 。

cpp 复制代码
#include <iostream>
using namespace std;

typedef long long ll; // 使用long long防止溢出

ll fastPow(ll a, ll b, ll p) {
    ll res = 1 % p; // 初始化结果,处理p=1的情况
    a %= p;         // 先取模,防止a过大
    while (b > 0) {
        if (b & 1) {        // 判断b的最低位是否为1
            res = res * a % p; // 如果为1,则将当前的a乘入结果
        }
        a = a * a % p;      // a倍增(a^1 -> a^2 -> a^4...)
        b >>= 1;            // b右移一位,相当于b /= 2
    }
    return res;
}

int main() {
    // 示例:计算 3^13 % 7
    ll a = 3, b = 13, p = 7;
    ll result = fastPow(a, b, p);
    cout << a << "^" << b << " mod " << p << " = " << result << endl;
    // 输出:3^13 mod 7 = 3
    return 0;
}

什么是线性表?

线性表是一种非常基础且重要的数据结构,它就像我们日常生活中排队一样,数据元素一个接一个地排列成一条线性的序列。

线性表指的是一种逻辑结构,它描述了数据元素之间清晰的先后关系。这种关系可以通过不同的物理存储方式来实现,主要有两种:

  • 顺序表(顺序存储) :使用一段连续的内存空间(比如数组)依次存放数据元素。它的优点是可以通过下标直接快速访问(随机存取)任何一个元素,就像你知道朋友住在第几栋楼第几间,可以直接找到。缺点是插入或删除元素时,通常需要移动大量后续元素,效率较低。

  • 链表(链式存储) :使用一组任意的存储单元存放数据元素,每个元素(节点)不仅存储自身数据,还存储了下一个元素地址的指针。它的优点是插入和删除非常灵活,只需修改指针即可,无需移动其他元素。缺点是不能直接按位置访问,需要从头开始逐个遍历。

无论是选择数组还是链表来实现线性表,他一般都支持增删查改,基本的初始化和删除。

数组本身是一个固定种类的数据结构,一般来说数组的大小是需要提前声明且固定的,但是也有所谓的可变数组,比如C++的vector。可变数组的价值在于它在保持数组"随机访问"(通过索引快速定位元素)这一核心优势的同时,提供了极大的灵活性 。其底层扩容操作虽然有一定成本,但通过成倍扩容的策略,均摊(Amortized)到每次操作上的时间成本依然是常数级别(O(1)),效率很高。

而链表可以分为单链表,循环链表和双向链表。

单链表是最简单的链表结构。每个节点由数据域指针域 组成,指针域只指向下一个节点,最后一个节点指向空(NULL),表示链表结束。

循环链表是单链表的变体,其最后一个节点的指针不再指向NULL,而是指向头节点,从而形成一个环。这样从任意节点出发都能访问到整个链表。

双向链表的每个节点包含两个指针 ,分别指向前驱节点后继节点。这为链表提供了双向遍历的能力。

关于栈和队列:栈和队列都属于线性表 。更准确地说,它们是操作受限的线性表,是线性表的两种重要特例。栈和队列在逻辑结构上依然是线性的,这意味着数据元素之间保持着"一个接一个"的顺序关系,每个元素最多只有一个前驱和一个后继。你可以把它们想象成一支队伍,元素是依次排列的。

什么是串,数组和广义表?

简单来说,串、数组和广义表是三种密切相关的数据结构,它们都是线性结构(元素有序排列),但功能和灵活性逐级增强。

简单的说,串是针对字符的概念,而数组则是某一类元素逻辑连续的物理结构,广义表是线性表的一种延申,也是一种物理结构,他允许不同类型的元素以线性表的逻辑结构存储。

关于所谓的字符串,我们往往绕不开一个问题,就是字符串的模式匹配算法,我们今天不聊复杂的,就聊聊基础的BF算法和KMP算法。

这是BF算法的C++实现:

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int BFSearch(const string& text, const string& pattern) {
    int n = text.length();
    int m = pattern.length();
    
    for (int i = 0; i <= n - m; ++i) { // i是主串中的起始位置
        int j;
        for (j = 0; j < m; ++j) { // j是模式串中的当前位置
            if (text[i + j] != pattern[j]) {
                break; // 字符不匹配,跳出内层循环
            }
        }
        if (j == m) { // 整个模式串都匹配成功
            return i; // 返回匹配的起始位置
        }
    }
    return -1; // 匹配失败
}

然后是KMP算法的C++实现:

cpp 复制代码
#include <vector>

// 1. 构建next数组
vector<int> buildNext(const string& pattern) {
    int m = pattern.length();
    vector<int> next(m, 0);
    next[0] = -1; // 初始化,-1是个特殊标记,表示模式串开头就失配,主串指针后移,模式串指针重置
    int j = 0, k = -1;
    
    while (j < m - 1) {
        if (k == -1 || pattern[j] == pattern[k]) {
            // 如果pattern[j] == pattern[k],则next[j+1] = k+1
            j++;
            k++;
            next[j] = k;
        } else {
            // 否则,令k = next[k],继续寻找更短的前后缀匹配
            k = next[k];
        }
    }
    return next;
}

// 2. KMP搜索主体
int KMPSearch(const string& text, const string& pattern) {
    int n = text.length();
    int m = pattern.length();
    if (m == 0) return 0; // 空模式串
    
    vector<int> next = buildNext(pattern);
    int i = 0; // 主串指针
    int j = 0; // 模式串指针
    
    while (i < n && j < m) {
        if (j == -1 || text[i] == pattern[j]) {
            // 当前字符匹配成功,或者模式串指针j因特殊标记-1需要重置,则双双后移
            i++;
            j++;
        } else {
            // 当前字符匹配失败,根据next数组回溯模式串指针j,主串指针i不变
            j = next[j];
        }
    }
    
    if (j == m) { // 模式串指针j走到了末尾,表示完全匹配
        return i - j;
    } else {
        return -1;
    }
}

当我们讨论数组的实际应用时,有一个非常常见的用途就是所谓的矩阵压缩存储。矩阵压缩存储是一种旨在节省计算机内存空间的技术,它通过利用矩阵中数据的分布规律,来减少存储需求。这与数组,特别是一维数组,有着非常直接和紧密的关系。

矩阵在计算机中通常用二维数组表示。但某些矩阵存在大量重复值(如常数 c)或零元素。矩阵压缩存储的核心思想很直接:

  • 为多个值相同的元素只分配一个存储空间

  • 对零元素不分配存储空间

这样做的目的是提高存储空间利用率

矩阵压缩主要针对两类矩阵,特殊矩阵 和**稀疏矩阵,**它们的压缩策略有所不同。

下面是C++的实现,首先以对称矩阵为例:

cpp 复制代码
#include <iostream>
using namespace std;

template <class T>
class SymmetricMatrix {
private:
    T* _a;       // 指向压缩存储的一维数组
    size_t _n;    // 矩阵的阶数 (n x n)
    size_t _size; // 一维数组的总大小

public:
    // 构造函数:将原始的 n*n 二维数组压缩存储
    SymmetricMatrix(T* a, size_t n) : _n(n) {
        _size = n * (n + 1) / 2; // 计算存储下三角所需空间
        _a = new T[_size];
        size_t index = 0;
        // 只将下三角部分(包括对角线)存入一维数组
        for (size_t i = 0; i < n; ++i) {
            for (size_t j = 0; j <= i; ++j) { // 注意条件是 j<=i
                _a[index++] = a[i * n + j];
            }
        }
    }

    ~SymmetricMatrix() {
        delete[] _a;
    }

    // 获取矩阵中(i, j)位置的元素
    T& Access(size_t i, size_t j) {
        // 如果访问的是上三角元素,则转换为下三角的坐标
        if (i < j) {
            swap(i, j);
        }
        // 关键:通过公式计算元素在一维数组中的位置 k = i(i+1)/2 + j
        return _a[i * (i + 1) / 2 + j];
    }

    // 打印完整的 n*n 矩阵
    void Display() {
        for (size_t i = 0; i < _n; ++i) {
            for (size_t j = 0; j < _n; ++j) {
                cout << Access(i, j) << " ";
            }
            cout << endl;
        }
    }
};

int main() {
    // 定义一个5x5的对称矩阵作为原始数据
    int a[5][5] = {
        {0, 1, 2, 3, 4},
        {1, 0, 1, 2, 3},
        {2, 1, 0, 1, 2},
        {3, 2, 1, 0, 1},
        {4, 3, 2, 1, 0}
    };
    
    SymmetricMatrix<int> sm((int*)a, 5); // 创建压缩存储对象
    sm.Display(); // 打印还原后的矩阵
    return 0;
}

然后是稀疏矩阵的压缩存储:

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

// 定义三元组结构体
template <class T>
struct Triple {
    size_t _row;
    size_t _col;
    T _value;
    Triple(size_t r, size_t c, const T& v) : _row(r), _col(c), _value(v) {}
    Triple() : _row(0), _col(0), _value(0) {} // 默认构造函数
};

template <class T>
class SparseMatrix {
private:
    size_t _m, _n; // 矩阵的行数、列数
    T _invalid;     // 无效值(通常代表0)
    vector<Triple<T>> _matrix; // 存储三元组的动态数组

public:
    SparseMatrix(T* a, size_t m, size_t n, const T& invalid = T())
        : _m(m), _n(n), _invalid(invalid) {
        // 遍历原始矩阵,收集所有非零元素
        for (size_t i = 0; i < m; ++i) {
            for (size_t j = 0; j < n; ++j) {
                T value = a[i * n + j];
                if (value != invalid) { // 如果值不是"无效值"(即非零)
                    _matrix.push_back(Triple<T>(i, j, value));
                }
            }
        }
    }

    void Display() {
        size_t index = 0; // 用于遍历三元组表的指针
        for (size_t i = 0; i < _m; ++i) {
            for (size_t j = 0; j < _n; ++j) {
                // 如果当前三元组存在,且其坐标等于当前(i, j)
                if (index < _matrix.size() && _matrix[index]._row == i && _matrix[index]._col == j) {
                    cout << _matrix[index]._value << " ";
                    index++; // 移动指针到下一个三元组
                } else {
                    cout << _invalid << " "; // 输出默认值(0)
                }
            }
            cout << endl;
        }
    }

    // 获取非零元素的个数
    size_t getNumOfNonZero() const {
        return _matrix.size();
    }
};

int main() {
    // 定义一个6x5的稀疏矩阵,大部分元素为0
    int a[6][5] = {
        {1, 0, 0, 0, 5},
        {0, 3, 0, 4, 0},
        {0, 0, 0, 0, 0}, // 全零行
        {2, 0, 0, 0, 6},
        {0, 0, 1, 0, 0},
        {0, 0, 0, 0, 0}  // 全零行
    };

    SparseMatrix<int> sm((int*)a, 6, 5, 0); // 0代表无效值
    sm.Display();
    cout << "非零元素个数: " << sm.getNumOfNonZero() << endl;
    return 0;
}

什么是散列表?

散列表(Hash Table,也叫哈希表)是一种非常重要且高效的数据结构,它能让数据的存储和查找变得非常迅速。

为什么散列表里会有冲突呢?具体的解放方法都是什么?

哈希冲突是指两个或更多不同的关键字(key),经过哈希函数计算后,得到了相同的哈希值(即数组下标)。这就像一栋公寓里,两户不同的家庭被分配到了同一个门牌号,显然会造成问题。

针对哈希冲突,有两种主要的解决方法:

什么是二叉树?

二叉树是计算机科学中一种重要的树形数据结构,它的核心特征在于每个节点最多有两个子节点 ,分别为左子节点右子节点。这种清晰的定义使得二叉树成为构建更复杂数据结构和算法的基础 。

二叉树有一些有用的数学性质,在分析问题时很有帮助:

这里解释一下所谓的深度和高度的定义:深度是从根节点到叶子节点逐渐增加,而高度是从叶子节点到根节点逐渐增加

二叉树是一个大类的统称,细分起来还有非常多种具体的二叉树:

我们来从二叉搜索树开始说起,关于二叉搜索树,最大的特征当然就是左节点的值小于根节点小于右节点,那么这也就意味着,如果我们对二叉搜索树进行中序遍历,那么可以得到一个有序序列。

我们接下来用一个简单的示例来展示:

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

// 定义二叉树节点
struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

// 中序遍历函数(递归实现)
void inorderTraversal(TreeNode* root, vector<int>& result) {
    if (root == nullptr) return;
    inorderTraversal(root->left, result);  // 1. 递归遍历左子树
    result.push_back(root->val);           // 2. 访问根节点
    inorderTraversal(root->right, result); // 3. 递归遍历右子树
}

int main() {
    cout << "=== 二叉搜索树中序遍历演示 ===" << endl;

    // 手动构建一个固定的二叉搜索树
    // 树的结构如下:
    //       8
    //      / \
    //     3   10
    //    / \    \
    //   1   6    14
    //      / \   /
    //     4   7 13
    TreeNode* root = new TreeNode(8);
    root->left = new TreeNode(3);
    root->right = new TreeNode(10);
    root->left->left = new TreeNode(1);
    root->left->right = new TreeNode(6);
    root->left->right->left = new TreeNode(4);
    root->left->right->right = new TreeNode(7);
    root->right->right = new TreeNode(14);
    root->right->right->left = new TreeNode(13);

    // 执行中序遍历
    vector<int> sortedResult;
    inorderTraversal(root, sortedResult);

    // 打印遍历结果
    cout << "中序遍历结果: ";
    for (int num : sortedResult) {
        cout << num << " ";
    }
    cout << endl;

    // 验证是否有序
    bool isSorted = true;
    for (size_t i = 1; i < sortedResult.size(); ++i) {
        if (sortedResult[i] < sortedResult[i-1]) {
            isSorted = false;
            break;
        }
    }
    cout << "→ 序列是否为升序: " << (isSorted ? "是" : "否") << endl;
    cout << "→ 结论:二叉搜索树的中序遍历结果是有序的。" << endl;

    // 释放内存(在实际演示中可能省略)
    // ... 此处省略递归删除节点的代码 ...

    return 0;
}

在二叉搜索树的基础之上,我们又可以得到平衡二叉树:平衡二叉树是一种特殊的二叉搜索树,其核心特点在于它能够通过特定的调整机制,保持树的结构始终处于相对平衡的状态,从而确保其上的查找、插入和删除等操作在最坏情况 下的时间复杂度也能稳定在 **O(log n)**​ 级别 ,而这也是为什么平衡二叉树如此重要。

以下是常见的平衡二叉树的对比:

那还有一种二叉树,叫做哈夫曼树:哈夫曼树(Huffman Tree),也称为最优二叉树,是一种带权路径长度最短的二叉树。它的主要目的是用最短的编码来表示信息,从而实现对数据的有效压缩 。

通过这个过程,最终形成的哈夫曼树就天然地满足了一个重要特性:出现频率高(权值大)的字符对应的编码短,频率低的字符编码长 ​ 。哈夫曼树最核心、最经典的应用就是哈夫曼编码,这是一种用于数据压缩的无损编码方式 。

什么是图?

图就是顶点和线的结合,按照线是否有方向,是否有权值,我们可以分为有(无)向有(无)权图等等,其实图的分类很多,图也可以说是最常见也是复杂的数据结构之一。

一个图该如何存储呢?一般来说有两种做法:邻接表和邻接矩阵。

下面是具体的C++实现:

cpp 复制代码
#include <iostream>
#include <vector>
#include <list>
using namespace std;

int main() {
    // 定义图的顶点和边
    const int V = 5; // 5个顶点
    const int E = 6; // 6条边
    
    // 定义图的边集合 (无向图)
    int edges[E][2] = {
        {0, 1},  // 边1: 顶点0-1
        {0, 2},  // 边2: 顶点0-2  
        {1, 2},  // 边3: 顶点1-2
        {1, 3},  // 边4: 顶点1-3
        {2, 4},  // 边5: 顶点2-4
        {3, 4}   // 边6: 顶点3-4
    };
    
    cout << "构建的图结构(5个顶点,6条边):" << endl;
    cout << "边集合: ";
    for(int i = 0; i < E; i++) {
        cout << edges[i][0] << "-" << edges[i][1] << " ";
    }
    cout << endl << endl;
    
    // ==================== 邻接矩阵存储 ====================
    cout << "=== 邻接矩阵存储 ===" << endl;
    
    // 创建邻接矩阵(二维数组)
    vector<vector<int>> adjMatrix(V, vector<int>(V, 0));
    
    // 添加边到邻接矩阵
    for(int i = 0; i < E; i++) {
        int u = edges[i][0];
        int v = edges[i][1];
        adjMatrix[u][v] = 1;
        adjMatrix[v][u] = 1; // 无向图,对称设置
    }
    
    // 打印邻接矩阵
    cout << "邻接矩阵:" << endl;
    cout << "   ";
    for(int i = 0; i < V; i++) {
        cout << i << " ";
    }
    cout << endl;
    
    for(int i = 0; i < V; i++) {
        cout << i << ": ";
        for(int j = 0; j < V; j++) {
            cout << adjMatrix[i][j] << " ";
        }
        cout << endl;
    }
    
    // ==================== 邻接表存储 ====================
    cout << endl << "=== 邻接表存储 ===" << endl;
    
    // 创建邻接表(vector<list>)
    vector<list<int>> adjList(V);
    
    // 添加边到邻接表
    for(int i = 0; i < E; i++) {
        int u = edges[i][0];
        int v = edges[i][1];
        adjList[u].push_back(v);
        adjList[v].push_back(u); // 无向图,双向添加
    }
    
    // 打印邻接表
    cout << "邻接表:" << endl;
    for(int i = 0; i < V; i++) {
        cout << "顶点 " << i << " 的邻居: ";
        for(auto neighbor : adjList[i]) {
            cout << neighbor << " ";
        }
        cout << endl;
    }
    
    return 0;
}

C++中的邻接矩阵其实就是一个二维数组vector<vector<int>>,邻接表则是一个vector<list<int>>,我们的矩阵里第(i,j)个元素记录着从节点i到节点j是否有边,或者边的权值,而表中vector的第i个元素则是一个记录所有以i为起点的边,用一个list记录。

关于dfs和bfs:

深度优先搜索(DFS)和广度优先搜索(BFS)是图和树结构中最基本、最重要的两种遍历算法。它们核心的差别在于遍历的"哲学"不同:DFS追求的是"深度",类似于探险家,会选择一条路径尽可能深入地探索,直到尽头再回溯;其底层通常使用 (递归或迭代)来管理待访问的节点。而BFS则强调"广度",像水面的波纹一样,从起点开始一层一层地向外扩散,确保先访问所有相邻节点再进入下一层;其底层依赖队列来实现这个"先进先出"的访问顺序。

我们用一个耳熟能详的编程题------岛屿问题,来展示dfs和bfs具体怎么用C++实现:

cpp 复制代码
#include <iostream>
#include <vector>
#include <queue>
#include <utility> // for pair
using namespace std;

// ==================== DFS解法 ====================
class SolutionDFS {
public:
    int numIslands(vector<vector<char>>& grid) {
        if (grid.empty()) return 0;
        
        int count = 0;
        int rows = grid.size();
        int cols = grid[0].size();
        
        // 遍历整个网格
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                // 当遇到未访问的陆地时
                if (grid[i][j] == '1') {
                    count++; // 发现新岛屿
                    dfs(grid, i, j); // 用DFS标记整个岛屿
                }
            }
        }
        return count;
    }
    
private:
    void dfs(vector<vector<char>>& grid, int i, int j) {
        int rows = grid.size();
        int cols = grid[0].size();
        
        // 边界条件检查
        if (i < 0 || i >= rows || j < 0 || j >= cols) return;
        // 如果不是陆地或已访问,返回
        if (grid[i][j] != '1') return;
        
        // 标记当前陆地为已访问(沉没岛屿)
        grid[i][j] = '0';
        
        // 递归访问四个方向
        dfs(grid, i - 1, j); // 上
        dfs(grid, i + 1, j); // 下
        dfs(grid, i, j - 1); // 左
        dfs(grid, i, j + 1); // 右
    }
};

// ==================== BFS解法 ====================
class SolutionBFS {
public:
    int numIslands(vector<vector<char>>& grid) {
        if (grid.empty()) return 0;
        
        int count = 0;
        int rows = grid.size();
        int cols = grid[0].size();
        
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (grid[i][j] == '1') {
                    count++;
                    bfs(grid, i, j);
                }
            }
        }
        return count;
    }
    
private:
    void bfs(vector<vector<char>>& grid, int i, int j) {
        int rows = grid.size();
        int cols = grid[0].size();
        
        queue<pair<int, int>> q;
        q.push({i, j});
        grid[i][j] = '0'; // 标记为已访问
        
        // 四个方向:上、右、下、左
        vector<pair<int, int>> directions = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
        
        while (!q.empty()) {
            auto [x, y] = q.front();
            q.pop();
            
            // 检查四个相邻单元格
            for (auto [dx, dy] : directions) {
                int newX = x + dx;
                int newY = y + dy;
                
                // 检查是否在边界内且是未访问的陆地
                if (newX >= 0 && newX < rows && newY >= 0 && newY < cols && 
                    grid[newX][newY] == '1') {
                    q.push({newX, newY});
                    grid[newX][newY] = '0'; // 立即标记,避免重复入队
                }
            }
        }
    }
};

// ==================== 工具函数:打印网格 ====================
void printGrid(const vector<vector<char>>& grid, const string& title) {
    cout << title << ":" << endl;
    for (const auto& row : grid) {
        for (char cell : row) {
            cout << cell << " ";
        }
        cout << endl;
    }
    cout << endl;
}

// ==================== 主函数:测试两种算法 ====================
int main() {
    // 测试用例1:示例网格
    vector<vector<char>> grid1 = {
        {'1','1','0','0','0'},
        {'1','1','0','0','0'},
        {'0','0','1','0','0'},
        {'0','0','0','1','1'}
    };
    
    // 测试用例2:另一个网格
    vector<vector<char>> grid2 = {
        {'1','1','1','1','0'},
        {'1','1','0','1','0'},
        {'1','1','0','0','0'},
        {'0','0','0','0','0'}
    };
    
    cout << "=== 岛屿数量问题 - DFS vs BFS 比较 ===" << endl << endl;
    
    // 测试网格1
    vector<vector<char>> grid1_dfs = grid1; // 复制用于DFS
    vector<vector<char>> grid1_bfs = grid1; // 复制用于BFS
    
    printGrid(grid1, "原始网格1");
    
    SolutionDFS dfsSolver;
    SolutionBFS bfsSolver;
    
    int result_dfs1 = dfsSolver.numIslands(grid1_dfs);
    int result_bfs1 = bfsSolver.numIslands(grid1_bfs);
    
    cout << "网格1结果:" << endl;
    cout << "DFS计算岛屿数量: " << result_dfs1 << endl;
    cout << "BFS计算岛屿数量: " << result_bfs1 << endl << endl;
    
    // 测试网格2
    vector<vector<char>> grid2_dfs = grid2;
    vector<vector<char>> grid2_bfs = grid2;
    
    printGrid(grid2, "原始网格2");
    
    int result_dfs2 = dfsSolver.numIslands(grid2_dfs);
    int result_bfs2 = bfsSolver.numIslands(grid2_bfs);
    
    cout << "网格2结果:" << endl;
    cout << "DFS计算岛屿数量: " << result_dfs2 << endl;
    cout << "BFS计算岛屿数量: " << result_bfs2 << endl << endl;
    
    // 算法比较
    cout << "=== 算法特性比较 ===" << endl;
    cout << "DFS(深度优先搜索):" << endl;
    cout << "  - 使用递归或栈实现" << endl;
    cout << "  - 空间复杂度: O(m×n) 最坏情况" << endl;
    cout << "  - 适合深度探索,代码简洁" << endl << endl;
    
    cout << "BFS(广度优先搜索):" << endl;
    cout << "  - 使用队列实现" << endl;
    cout << "  - 空间复杂度: O(min(m,n))" << endl;
    cout << "  - 按层扩展,适合最短路径问题" << endl;
    
    return 0;
}

什么是迷宫问题?

迷宫问题是一个经典的计算模型,它模拟在一个充满障碍的空间中,从起点寻找一条到达终点的有效路径的过程。这个问题不仅在算法学习中地位重要,在机器人导航、游戏开发等领域也有直接应用。

针对不同的迷宫,解决方法有很多,比如:

关于A*算法的话,之前的笔记已经做过了,这里随便放一个示例吧。

cpp 复制代码
#include <iostream>
#include <vector>
#include <queue>
#include <cmath>
#include <algorithm>
using namespace std;

// 定义节点结构体
struct Node {
    int x, y;        // 节点坐标
    double g, h, f;  // 代价: g-实际, h-启发, f-总代价
    Node* parent;    // 父节点指针,用于回溯路径

    Node(int x, int y, Node* parent = nullptr) 
        : x(x), y(y), g(0), h(0), f(0), parent(parent) {}

    // 重载运算符,用于优先队列比较
    bool operator < (const Node& other) const {
        return f > other.f; // 注意:优先队列默认最大堆,这里反向定义实现最小堆
    }
};

// 比较函数对象,用于优先队列
struct NodeCompare {
    bool operator() (const Node* a, const Node* b) const {
        return a->f > b->f;
    }
};

// 启发式函数:使用欧几里得距离计算估计代价
double heuristic(int x1, int y1, int x2, int y2) {
    return sqrt(pow(x1 - x2, 2) + pow(y1 - y2, 2));
}

// A*算法核心函数
vector<pair<int, int>> aStarSearch(vector<vector<int>>& grid, 
                                   pair<int, int> start, 
                                   pair<int, int> goal) {
    int rows = grid.size();
    int cols = grid[0].size();
    
    // 开放列表(优先队列),按f值从小到大排序
    priority_queue<Node*, vector<Node*>, NodeCompare> openList;
    // 封闭列表(二维数组),标记已处理的节点
    vector<vector<bool>> closedList(rows, vector<bool>(cols, false));
    
    // 创建起点节点并加入开放列表
    Node* startNode = new Node(start.first, start.second);
    startNode->h = heuristic(start.first, start.second, goal.first, goal.second);
    startNode->f = startNode->g + startNode->h;
    openList.push(startNode);
    
    // 定义8个移动方向(上、下、左、右、四个对角线)
    const int dx[8] = {1, 0, -1, 0, 1, 1, -1, -1};
    const int dy[8] = {0, 1, 0, -1, 1, -1, 1, -1};
    const double straightCost = 1.0;   // 直线移动代价
    const double diagonalCost = 1.414; // 对角线移动代价(√2)
    
    while (!openList.empty()) {
        // 1. 从开放列表取出f值最小的节点
        Node* current = openList.top();
        openList.pop();
        int x = current->x;
        int y = current->y;
        
        // 2. 如果到达目标点,回溯构建路径
        if (x == goal.first && y == goal.second) {
            vector<pair<int, int>> path;
            while (current != nullptr) {
                path.push_back({current->x, current->y});
                current = current->parent;
            }
            reverse(path.begin(), path.end()); // 反转路径,从起点到终点
            return path;
        }
        
        // 3. 将当前节点标记为已处理
        closedList[x][y] = true;
        
        // 4. 遍历当前节点的所有邻居
        for (int i = 0; i < 8; i++) {
            int nx = x + dx[i];
            int ny = y + dy[i];
            
            // 检查邻居是否有效(不越界、非障碍物、未处理)
            if (nx < 0 || nx >= rows || ny < 0 || ny >= cols) 
                continue;
            if (grid[nx][ny] == 1 || closedList[nx][ny]) 
                continue;
            
            // 计算移动代价(判断是直线还是对角线移动)
            double moveCost = (abs(dx[i]) + abs(dy[i]) == 2) ? diagonalCost : straightCost;
            double newG = current->g + moveCost;
            
            // 创建邻居节点
            Node* neighbor = new Node(nx, ny, current);
            neighbor->g = newG;
            neighbor->h = heuristic(nx, ny, goal.first, goal.second);
            neighbor->f = neighbor->g + neighbor->h;
            
            // 将邻居加入开放列表
            openList.push(neighbor);
        }
    }
    
    return {}; // 未找到路径,返回空向量
}

// 打印网格和路径的可视化结果
void printSolution(const vector<vector<int>>& grid, 
                   const vector<pair<int, int>>& path) {
    vector<vector<char>> display(grid.size(), 
                                vector<char>(grid[0].size(), ' '));
    
    // 标记障碍物
    for (int i = 0; i < grid.size(); i++) {
        for (int j = 0; j < grid[0].size(); j++) {
            if (grid[i][j] == 1) 
                display[i][j] = '#';
        }
    }
    
    // 标记路径
    for (const auto& p : path) {
        if (p == path.front()) 
            display[p.first][p.second] = 'S'; // 起点
        else if (p == path.back()) 
            display[p.first][p.second] = 'G'; // 终点
        else 
            display[p.first][p.second] = '*'; // 路径
    }
    
    // 打印结果
    for (const auto& row : display) {
        for (char c : row) {
            cout << '[' << c << ']';
        }
        cout << endl;
    }
}

int main() {
    // 定义地图:0=空地, 1=障碍物
    vector<vector<int>> grid = {
        {0, 0, 0, 0, 0, 0},
        {0, 1, 1, 0, 1, 0},
        {0, 0, 0, 0, 1, 0},
        {0, 1, 1, 1, 1, 0},
        {0, 0, 0, 0, 0, 0}
    };
    
    pair<int, int> start = {0, 0};
    pair<int, int> goal = {4, 5};
    
    // 执行A*搜索
    vector<pair<int, int>> path = aStarSearch(grid, start, goal);
    
    // 输出结果
    if (path.empty()) {
        cout << "未找到路径!" << endl;
    } else {
        cout << "找到路径 (共" << path.size() << "步):" << endl;
        for (const auto& p : path) {
            cout << "-> (" << p.first << "," << p.second << ") ";
        }
        cout << "\n\n路径可视化:" << endl;
        printSolution(grid, path);
    }
    
    return 0;
}

图的应用也很多,主要包括:

什么是深搜搜索树?

深度优先搜索树(DFS Tree)是理解深度优先搜索(DFS)核心机制的一个关键模型,深度优先搜索树并不是预先存在的,它是在DFS算法的运行过程中动态生成的一种逻辑结构,记录了DFS探索问题空间的完整路径。

它的构建过程紧密贴合DFS的步骤:

  • 从根节点开始:算法从初始状态(根节点)启动。

  • 深度优先探索:在每个节点上,DFS会选择一个尚未尝试的方向(即一条边)深入探索,从而在树中添加一个新的子节点和一条边。

  • 回溯与分支 :当遇到一个"死胡同"(即当前状态的所有可能转移都指向已访问过的状态)时,算法会回溯到上一个节点,并尝试该节点尚未探索的其他分支。

为什么我们要提到这个深搜搜索树呢?因为有了这个深搜搜索树,我们就可以进行剪枝了。

什么是回溯?

回溯算法是一种通过"试错"来寻找问题所有可行解的算法 。它的核心思想很像我们平时走迷宫:选择一条路走到底,如果发现走不通(此路不符合条件),就退回到上一个岔路口(回溯),选择另一条路继续尝试,直到找到出口或尝试完所有可能的路径。

回溯算法通常以**深度优先搜索(DFS)**​ 的方式遍历解空间,但两者侧重点不同:

我们通过一个经典的示例来展示回溯是怎么工作的:

给定一个不含重复数字的数组,返回所有可能的排列,也就是全排列问题。

cpp 复制代码
#include <vector>
using namespace std;

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    
    void backtrack(vector<int>& nums, vector<bool>& used) {
        // 终止条件:路径长度等于原数组长度
        if (path.size() == nums.size()) {
            result.push_back(path);
            return;
        }
        
        for (int i = 0; i < nums.size(); i++) {
            if (used[i]) continue;  // 跳过已使用的元素
            
            // 做出选择
            used[i] = true;
            path.push_back(nums[i]);
            
            // 递归探索
            backtrack(nums, used);
            
            // 撤销选择(回溯)
            path.pop_back();
            used[i] = false;
        }
    }

public:
    vector<vector<int>> permute(vector<int>& nums) {
        vector<bool> used(nums.size(), false);
        backtrack(nums, used);
        return result;
    }
};
相关推荐
日更嵌入式的打工仔21 小时前
单片机基础知识:内狗外狗/软狗硬狗
笔记·单片机
KhalilRuan1 天前
秋招笔记汇总
笔记
laplace01231 天前
Part3 RAG文档切分
笔记·python·中间件·langchain·rag
被遗忘的旋律.1 天前
Linux驱动开发笔记(二十三)—— regmap
linux·驱动开发·笔记
技术宅学长1 天前
关于CLS与mean_pooling的一些笔记
人工智能·pytorch·笔记·pycharm
数据轨迹0011 天前
CVPR DarkIR:低光图像增强与去模糊一体化
经验分享·笔记·facebook·oneapi·twitter
自小吃多1 天前
爬电距离与电气间隙
笔记·嵌入式硬件·硬件工程
半夏知半秋1 天前
rust学习-Option与Result
开发语言·笔记·后端·学习·rust
雍凉明月夜1 天前
深度学习网络笔记Ⅴ(Transformer源码详解)
笔记·深度学习·transformer