【图解算法数据结构】(一)数据结构

本文介绍了数据结构的基础概念,包括数组、链表、栈、队列、树、图、散列表等,并通过剑指 Offer 中的题目详细解释了这些数据结构在实际问题中的应用,如替换空格、链表逆序、用两个栈实现队列等,适合算法初学者阅读。
摘要由CSDN通过智能技术生成

目录

一、数据结构简介

1.1 前言

1.2 数组

1.3 链表

1.4 栈

1.5 队列

1.6 树

1.7 图

1.8 散列表

二、剑指 Offer 05. 替换空格

2.1 题求

2.2 求解

2.3 解说

三、剑指 Offer 06. 从尾到头打印链表

3.1 题求

3.2 求解

3.3 解说

四、剑指 Offer 09. 用两个栈实现队列

4.1 题求

4.2 求解

4.3 解说

五、剑指 Offer 20. 表示数值的字符串 ☆

5.1 题求

5.2 求解

5.3 解答

六、剑指 Offer 24. 反转链表

6.1 题求

6.2 求解

6.3 解说

七、剑指 Offer 30. 包含 min 函数的栈 ☆

7.1 题求

7.2 求解

7.3 解答

八、剑指 Offer 35. 复杂链表的复制 ☆

8.1 题求

8.2 求解

8.3 解答

九、剑指 Offer 58 - II. 左旋转字符串

9.1 题求

9.2 求解

9.3 解答

十、剑指 Offer 59 - I. 滑动窗口的最大值 ☆

10.1 题求

10.2 求解

10.3 解答

十一、剑指 Offer 59 - II. 队列的最大值 ☆

11.1 题求

11.2 求解

11.3 解答

十二、剑指 Offer 67. 把字符串转换成整数

12.1 题求

12.2 求解

12.3 说明


一、数据结构简介

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 +
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值