目录
一、数据结构简介
1.1 前言
数据结构 是为实现对计算机数据有效使用的各种数据组织形式,服务于各类计算机操作。不同的数据结构具有各自对应的适用场景,旨在降低各种算法计算的时间与空间复杂度,达到最佳的任务执行效率。
如下图所示,常见的数据结构可分为「线性数据结构」与「非线性数据结构」,具体包括:「数组」、「链表」、「栈」、「队列」、「树」、「图」、「散列表」、「堆」。
以下,将初步介绍各数据结构的基本特点,与 Python3 和 C++11 中各数据结构的初始化与构建方法。
1.2 数组
数组 (array) 是将相同类型的元素存储于连续内存空间的数据结构。
一方面,「固定数组」或者说「定长数组」的长度/尺寸在初始化后即固定不可变。
如下图所示,构建固定/定长数组需在初始化时给定长度,并对数组中每个索引/下标对应的元素赋值,例如:
// C++
// 初始化一个长度为 5 的 array
int array[5];
// 元素赋值
array[0] = 2;
array[1] = 3;
array[2] = 1;
array[3] = 0;
array[4] = 2;
也可使用直接赋值的初始化方式,例如:
int array[] = {2, 3, 1, 0, 2};
另一方面,「可变数组」或者说「动态数组」则是更经常使用的数据结构,它是基于数组和扩容机制实现的,比普通数组更加灵活。常用操作有:访问、添加、删除元素。
# Python
# 初始化可变数组 (list)
array = []
# 向尾部添加元素
array.append(2)
array.append(3)
array.append(1)
array.append(0)
array.append(2)
// C++
// 初始化可变数组 (int-vector)
vector<int> array;
// 向尾部添加元素
array.push_back(2);
array.push_back(3);
array.push_back(1);
array.push_back(0);
array.push_back(2);
1.3 链表
链表 (linked list) 以节点 (node) 为单位,每个节点元素都是一个独立对象,且在内存空间非连续存储。其中,「单链表」的节点对象具有两个成员变量:「节点值 val
」,「后继节点 (successor) 引用 next
」。
# Python
class ListNode:
def __init__(self, x):
self.val = x # 节点值
self.next = None # 后继节点引用
// C++
struct ListNode {
int val; // 节点值
ListNode *next; // 后继节点引用
ListNode(int x) : val(x), next(NULL) {}
};
如下图所示,建立此链表需要实例化每个节点,并构建各节点的引用指向。
# Python
# 实例化节点
n1 = ListNode(4) # 节点 head
n2 = ListNode(5)
n3 = ListNode(1)
# 构建引用指向
n1.next = n2
n2.next = n3
// C++
// 实例化节点
ListNode *n1 = new ListNode(4); // 节点 head
ListNode *n2 = new ListNode(5);
ListNode *n3 = new ListNode(1);
// 构建引用指向
n1->next = n2;
n2->next = n3;
此外,除了单链表,还有支持双向遍历的「双链表」。
1.4 栈
栈 (stack) 是一种具有 「先入后出 FILO」 特点的抽象数据结构,可使用数组或链表实现。
# Python
stack = [] # 可将 list 作为 stack 使用, 双端队列 collections.deque 也可以
// C++
stack<int> stk;
如下图所示,通过常用操作「入栈 push()
」,「出栈 pop()
」,展示了栈的先入后出特性。
# Python
stack.append(1) # 元素 1 入栈
stack.append(2) # 元素 2 入栈
stack.pop() # 出栈 -> 元素 2
stack.pop() # 出栈 -> 元素 1
// C++
stk.push(1); // 元素 1 入栈
stk.push(2); // 元素 2 入栈
stk.pop(); // 出栈 -> 元素 2
stk.pop(); // 出栈 -> 元素 1
注意:通常情况下,不推荐使用 Java 的 Vector 以及其子类 Stack ,而一般将 LinkedList 作为栈来使用。详细说明请见:Stack,ArrayDeque,LinkedList 的区别 。
// Java
LinkedList<Integer> stack = new LinkedList<>();
stack.addLast(1); // 元素 1 入栈
stack.addLast(2); // 元素 2 入栈
stack.removeLast(); // 出栈 -> 元素 2
stack.removeLast(); // 出栈 -> 元素 1
1.5 队列
队列 (queue) 是一种具有 「先入先出 FIFO」 特点的抽象数据结构,可使用链表实现。
# Python
from collections import deque
queue = deque() # Python 常使用双端队列 collections.deque
// C++
queue<int> que;
如下图所示,通过常用操作「入队 push()
」,「出队 pop()
」,展示了队列的先入先出特性。
# Python
queue.append(1) # 元素 1 入队
queue.append(2) # 元素 2 入队
queue.popleft() # 出队 -> 元素 1
queue.popleft() # 出队 -> 元素 2
// C++
que.push(1); // 元素 1 入队
que.push(2); // 元素 2 入队
que.pop(); // 出队 -> 元素 1
que.pop(); // 出队 -> 元素 2
1.6 树
树 (tree) 是一种非线性数据结构,根据子节点数可分为 「二叉树」 和 「N叉树 / 多叉树」,最顶层的节点称为「根节点 root」。
以二叉树为例,每个节点包含三个成员变量:「值 val」、「左子节点 left」、「右子节点 right」 ,例如:
# Python
class TreeNode:
def __init__(self, x):
self.val = x # 节点值
self.left = None # 左子节点
self.right = None # 右子节点
// C++
struct TreeNode {
int val; // 节点值
TreeNode *left; // 左子节点
TreeNode *right; // 右子节点
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
建立此二叉树需要实例化每个节点,并构建各节点的引用指向,如下所示:
# Python
# 初始化节点
n1 = TreeNode(3) # 根节点 root
n2 = TreeNode(4)
n3 = TreeNode(5)
n4 = TreeNode(1)
n5 = TreeNode(2)
# 构建引用指向
n1.left = n2
n1.right = n3
n2.left = n4
n2.right = n5
// C++
// 初始化节点
TreeNode *n1 = new TreeNode(3); // 根节点 root
TreeNode *n2 = new TreeNode(4);
TreeNode *n3 = new TreeNode(5);
TreeNode *n4 = new TreeNode(1);
TreeNode *n5 = new TreeNode(2);
// 构建引用指向
n1->left = n2;
n1->right = n3;
n2->left = n4;
n2->right = n5;
1.7 图
图 (graph) 是一种非线性数据结构,由「节点/顶点 vertex」和「边 edge」组成,每条边连接一对顶点。根据边的方向有无,图可分为「有向图」和「无向图」。以下以无向图为例 开展介绍。
如下图所示,此无向图的 顶点 和 边 集合分别为:
- 顶点集合: vertices = {1, 2, 3, 4, 5}
- 边集合: edges = {(1, 2), (1, 3), (1, 4), (1, 5), (2, 4), (3, 5), (4, 5)}
表示图的方法通常有两种:
# Python
vertices = [1, 2, 3, 4, 5]
edges = [[0, 1, 1, 1, 1],
[1, 0, 0, 1, 0],
[1, 0, 0, 0, 1],
[1, 1, 0, 0, 1],
[1, 0, 1, 1, 0]]
// C++
int vertices[5] = {1, 2, 3, 4, 5};
int edges[5][5] = {
{0, 1, 1, 1, 1},
{1, 0, 0, 1, 0},
{1, 0, 0, 0, 1},
{1, 1, 0, 0, 1},
{1, 0, 1, 1, 0}};
# Python
vertices = [1, 2, 3, 4, 5]
edges = [[1, 2, 3, 4],
[0, 3],
[0, 4],
[0, 1, 4],
[0, 2, 3]]
// C++
int vertices[5] = {1, 2, 3, 4, 5};
vector<vector<int>> edges;
vector<int> edge_1 = {1, 2, 3, 4};
vector<int> edge_2 = {0, 3};
vector<int> edge_3 = {0, 4};
vector<int> edge_4 = {0, 1, 4};
vector<int> edge_5 = {0, 2, 3};
edges.push_back(edge_1);
edges.push_back(edge_2);
edges.push_back(edge_3);
edges.push_back(edge_4);
edges.push_back(edge_5);
1.8 散列表
散列表 (hashtable) 是一种非线性数据结构,通过利用 Hash 函数 将指定的「键 key」映射至对应的「值 value」,以实现高效的元素查找。
设小力、小特、小扣的学号分别为 10001、10002、10003,现需要由「姓名」查找「学号」,则可通过建立以姓名为 key、学号为 value 的散列表实现,例如:
# Python
# 初始化散列表
dic = {}
# 添加 key -> value 键值对
dic["小力"] = 10001
dic["小特"] = 10002
dic["小扣"] = 10003
# 从姓名查找学号
dic["小力"] # -> 10001
dic["小特"] # -> 10002
dic["小扣"] # -> 10003
// C++
// 初始化散列表
unordered_map<string, int> dic;
// 添加 key -> value 键值对
dic["小力"] = 10001;
dic["小特"] = 10002;
dic["小扣"] = 10003;
// 从姓名查找学号
dic.find("小力")->second; // -> 10001
dic.find("小特")->second; // -> 10002
dic.find("小扣")->second; // -> 10003
自行设计 Hash 函数:
假设需求:从「学号」查找「姓名」。
将三人的姓名存储至以下数组中,则各姓名在数组中的索引分别为 0, 1, 2 。
# Python
names = [ "小力", "小特", "小扣" ]
// C++
string names[] = { "小力", "小特", "小扣" };
# Python
def hash(id):
index = (id - 1) % 10000
return index
// C++
int hash(int id) {
int index = (id - 1) % 10000;
return index;
}
参考文献:https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/50e446/
二、剑指 Offer 05. 替换空格
2.1 题求
2.2 求解
法一:遍历字符串 + 条件判断。
在 Python 和 Java 等语言中,字符串都被设计成「不可变」的类型,即无法直接修改字符串的某一位字符,因此需要新建一个字符串保存修改后的字符串。
空间复杂度 O(n),时间复杂度 O(n)。
Python - 2021/3/1 - 99.68% (24ms)
class Solution:
def replaceSpace(self, s: str) -> str:
result = []
for char in s:
result.append("%20" if char == " " else char)
return "".join(result)
# 等价
# return "".join([char if char != " " else "%20" for char in s])
C++ - 2021/3/1 - 100% (0ms) - 58.38% (6.2MB)
class Solution {
public:
string replaceSpace(string s) {
string result; // 存储结果
for(auto &c : s) { // 遍历原字符串 s
if(c == ' ') { // 不同于 Python, C++ 的空字符写法唯一
result.push_back('%');
result.push_back('2');
result.push_back('0');
}
else {
result.push_back(c);
}
}
return result;
}
};
法二:Python 字符串方法 str.replace(old, new[, max])。
Python - 2021/3/1 - 99.96% (20ms) - 不推荐
class Solution:
def replaceSpace(self, s: str) -> str:
return s.replace(" ", "%20")
法三:利用 C++ 字符串可修改特性
在 C++ 中, 字符串被设计成「可变」的类型(Python 和 Java 字符串则不可变),因此可在不新建字符串保存结果的情况下实现 原地修改 (in-place)。
空间复杂度 O(1) (由于是原地扩展 s 长度,因此仅使用 O(1) 额外空间),时间复杂度 O(n)。
C++ - 2021/3/1 - 100% (0ms) - 57.16% (6.2MB)
class Solution {
public:
string replaceSpace(string s)
{
// 字符串空格数
int count = 0;
// 统计空格数量
for (auto &c : s)
{
if (c == ' ')
{
++count; // 理论上 ++i 比 i++ 要快 (省了一些步骤)
}
}
// 字符串原长度
int len = s.size();
// 字符串扩容到新长度
s.resize(len + 2*count);
// 倒序遍历字符串修改 (关键, 原因在后面)
// i 从原字符串开始倒序遍历, j 从新扩充字符串开始倒序遍历, 当 i==j==0 时, 修改完成
for(int i = len-1, j = s.size()-1; i < j; --i, --j)
{
if (s[i] != ' ')
{
s[j] = s[i]; // 遇到非空字符,保持原样拷贝
}
else
{
s[j-2] = '%'; // 遇到空字符,依次修改为 %20
s[j-1] = '2';
s[j] = '0';
j -= 2; // 移动
}
}
return s;
}
};
2.3 解说
/*******************************************************************
Copyright(c) 2016, Harry He
All rights reserved.
Distributed under the BSD license.
(See accompanying file LICENSE.txt at
https://github.com/zhedahht/CodingInterviewChinese2/blob/master/LICENSE.txt)
*******************************************************************/
#include <cstdio>
#include <cstring>
/*length 为字符数组str的总容量,大于或等于字符串str的实际长度*/
void ReplaceBlank(char str[], int length)
{
if(str == nullptr && length <= 0)
return;
/*originalLength 为字符串str的实际长度*/
int originalLength = 0;
int numberOfBlank = 0;
int i = 0;
while(str[i] != '\0')
{
++ originalLength;
if(str[i] == ' ')
++ numberOfBlank;
++ i;
}
/*newLength 为把空格替换成'%20'之后的长度*/
int newLength = originalLength +