数据结构与算法启示
从整体到细节,自顶向下,从抽象到具体的框架思维是通用的,不只是学习数据结构和算法,学习其他任何
知识都是高效的
数据结构是工具,算法是通过合适的工具解决特定问题的方法
一、数据结构的存储方式
存储:数组(顺序存储) 链表(链式存储)
我们分析问题,一定要有递归的思想,自顶向下,从抽象到具体。
*比如 队列 ,栈 这两种数据结构既可以用链表也可以用数组
用数组考虑 处理扩容缩容的问题
用链表考虑内存空间存储结点指针
*图 的两种表示方式邻接表就是链表 邻接矩阵就是二维数组
邻接矩阵判断连通性迅速
图比较稀疏用邻接表节省空间
*「散列表」就是通过散列函数把键映射到一个大数组里。
而且对于解决散列冲突的方法,拉链法需要链表特性,操作简单,但需要额外的空间存储指针;
线性探查法就需要数组特性,以便连续寻址,不需要指针的存储空间,但操作稍微复杂些。
*「树」
用数组实现就是「堆」,因为「堆」是一个完全二叉树,用数组存储不需要节点指针,操作也比较简单;
用链表实现就是很常⻅的那种「树」,因为不一定是完全二叉树,所以不适合用数组存储。
为此,在这种链表「树」结构之上,又衍生出各种巧妙的设计,比如二叉搜索树、AVL 树、红黑树、区间树、B 树
Redis 数据库的朋友可能也知道,Redis 提供列表、字符串、集合等等几种常用数据结构,但是对于每
种数据结构,底层的存储方式都至少有两种,以便于根据存储数据的实际情况使用合适的存储方式
*数组由于是紧凑连续存储,可以随机访问,通过索引快速找到对应元素,而且相对节约存储空间。但正因为连
续存储,内存空间必须一次性分配够,所以说数组如果要扩容,需要重新分配一块更大的空间,再把数据全
部复制过去,时间复杂度 O(N);而且你如果想在数组中间进行插入和删除,每次必须搬移后面的所有数据以
保持连续,时间复杂度 O(N)。
*链表因为元素不连续,而是靠指针指向下一个元素的位置,所以不存在数组的扩容问题;如果知道某一元素
的前驱和后驱,操作指针即可删除该元素或者插入新元素,时间复杂度 O(1)。但是正因为存储空间不连续,
你无法根据一个索引算出对应元素的地址,所以不能随机访问;而且由于每个元素必须存储指向前后元素位
置的指针,会消耗相对更多的储存空间
二、数据结构的基本操作
基本操作无非遍历 + 访问,再具体一点就是:增删查改 它们存在的目的都是在不同的应用场景,尽可能高效地增删查改
各种数据结构的遍历 + 访问无非两种形式:线性的和非线性的
线性就是 for/while 迭代为代表,非线性就是递归为代表
1.数组典型的线性迭代结构
只要涉及递归,都可以抽象成二叉树的问题。
写递归算法的关键是要明确函数的「定义」是什么,
然后相信这个定义,利用这个定义推导最终结果,绝不要试图跳入递归。
从整体到细节,自顶向下,从抽象到具体的框架思维是通用的,不只是学习数据结构和算法,学习其他任何
知识都是高效的
在 C++ 中,数组参数通常需要传入数组的大小(size),因为 C++ 不像 Java 那样能自动获取数组的长度。
通过传递数组的大小 size,我们可以迭代访问数组中的每个元素。
数组遍历框架,典型的线性迭代结构:
void traverse(int arr[], int size) {for (int i = 0; i < size; i++) {// 迭代访问 arr[i]}
}
现代 C++,并且 std::vector 会自动管理数组的大小。
#include <vector>void traverse(const std::vector<int>& arr) {for (int i = 0; i < arr.size(); i++) {// 迭代访问 arr[i]}
}
//2.链表遍历框架
/* 基本的单链表节点 */
class ListNode {
public:int val; // 当前节点的值ListNode* next; // 下一个节点ListNode(int value) : val(value), next(nullptr) {}
};
//迭代
void traverse(ListNode* head) { // 头节点for (ListNode* p = head; p != nullptr; p = p->next) {// 迭代访问 p->val}
}//递归void traverse(ListNode* head) {if (head == nullptr) return;// 递归访问 head->valtraverse(head->next);
}
//3.二叉树遍历框架,典型的非线性递归遍历结构
/* 基本的二叉树节点 */
class TreeNode {
public:int val;TreeNode* left;TreeNode* right;TreeNode(int value) : val(value), left(nullptr), right(nullptr) {}
};void traverse(TreeNode* root) { // 根节点if (root == nullptr) return; // 基本情况// 访问 root->valtraverse(root->left); // 左节点traverse(root->right); // 右节点
}
/* 基本的 N 叉树节点 */
N 叉树的遍历又可以扩展为图的遍历,因为图就是好几 N 叉棵树的结合体。你说图是可能出现环的?这个很
好办,用个布尔数组 visited 做标记就行了
class NTreeNode {
public:int val;std::vector<NTreeNode*> children;NTreeNode(int value) : val(value) {}
};void traverse(NTreeNode* root) {if (root == nullptr) return; // 基本情况// 访问 root->valfor (NTreeNode* child : root->children) {traverse(child); // 递归访问每个子节点}
}