DAY 02 数据结构基础


前言

我发现在学习算法的过程中,我还不太了解基本的数据结构用法和实现原理,在做算法题时,感觉有点吃力,所以我这一章好好总结了一下数据结构的特点。只有掌握好底层结构,才能准确利用每个数据结构的特点,并理解写的代码的时间复杂度。


一、数组(顺序存储)基本原理

1.静态数组

「静态数组」就是一块连续的内存空间,我们可以通过索引来访问这块内存空间中的元素,这才是数组的原始形态

静态数组在创建的时候就要确定数组的元素类型和元素数量。

定义一个静态数组的方法:

// 定义一个大小为 10 的静态数组
int arr[10];

// 用 memset 函数把数组的值初始化为 0
memset(arr, 0, sizeof(arr));

// 使用索引赋值
arr[0] = 1;
arr[1] = 2;

// 使用索引取值
int a = arr[0];

几点说明:

int arr[10] 这段代码到底做了什么事情呢?

1、在内存中开辟了一段连续的内存空间,大小是 10 * sizeof(int) 字节。一个 int 在计算机内存中占 4 字节,也就是总共 40 字节。

2、定义了一个名为 arr 的数组指针,指向这段内存空间的首地址。

那么 arr[1] = 2 这段代码又做了什么事情呢?

1、计算 arr 的首地址加上 1 * sizeof(int) 字节(4 字节)的偏移量,找到了内存空间中的第二个元素的地址。

2、从这个地址开始的 4 个字节的内存空间中写入了整数 2

为什么数组的索引从 0 开始?

方便取地址。arr[0] 就是 arr 的首地址,从这个地址往后的 4 个字节存储着第一个元素的值。数组的名字 arr 就指向整块内存的首地址,所以数组名 arr 就是一个指针。即*arr 的值是 arr[0],即第一个元素的值。

为什么用memset函数

如果不用 memset 这种函数初始化数组的值,那么数组内的值是不确定的。因为 int arr[10] 这个语句只是请操作系统在内存中开辟了一块连续的内存空间,这段空间不知道有没有被人用过,所以一般要用 memset 函数把这块内存空间的值初始化一下再使用。初始化是针对 C/C++。

增删改查

数据结构的职责就是增删查改

在上面讲过查和改。

给静态数组增加元素,分为三种情况:

1.数组末尾追加(append)元素

// 大小为 10 的数组已经装了 4 个元素
int arr[10];
for (int i = 0; i < 4; i++) {
    arr[i] = i;
}

// 现在想在数组末尾追加一个元素 4
arr[4] = 4;

// 再在数组末尾追加一个元素 5
arr[5] = 5;

// 依此类推
// ...

由于只是对索引赋值,所以在数组末尾追加元素的时间复杂度是 O(1)

2.数组中间插入(insert)元素

例如我有一个大小为 10 的数组 arr,前 4 个索引装了元素,现在想在第 3 个位置(arr[2])插入一个新元素,怎么办?

这就要涉及「数据搬移」,给新元素腾出空位,然后再才能插入新元素。大概的代码逻辑是这样的:

// 大小为 10 的数组已经装了 4 个元素
int arr[10];
for (int i = 0; i < 4; i++) {
    arr[i] = i;
}

// 在第 3 个位置插入元素 888
// 需要把第 3 个位置及之后的元素都往后移动一位
// 注意要倒着遍历数组中已有元素,避免覆盖。
for (int i = 4; i > 2; i--) {
    arr[i] = arr[i - 1];
}

// 现在第 3 个位置空出来了,可以插入新元素
arr[2] = 888;

3.数组空间已满

1.删除末尾元素

比如现在有一个大小为 10 的数组,里面装了 5 个元素,现在想删除末尾的元素,怎么办?

直接把末尾元素标记为一个特殊值代表已删除就行了,我们这里简单举例,就用 -1 作为特殊值代表已删除好了。和后面学习的动态数组不一样,会有更完善的方法删除数组元素,这里只是为了说明删除数组尾部元素的本质就是进行一次随机访问,时间复杂度是 O(1)

// 大小为 10 的数组已经装了 5 个元素
int arr[10];
for (int i = 0; i < 5; i++) {
    arr[i] = i;
}

// 删除末尾元素,暂时用 -1 代表元素已删除
arr[4] = -1;

2.删除中间元素

一个大小为 10 的数组,里面装了 5 个元素,现在想删除第 2 个元素(arr[1]),怎么办?

这也要涉及「数据搬移」,把被删元素后面的元素都往前移动一位,保持数组元素的连续性。

// 大小为 10 的数组已经装了 5 个元素
int arr[10];
for (int i = 0; i < 5; i++) {
    arr[i] = i;
}

// 删除 arr[1]
// 需要把 arr[1] 之后的元素都往前移动一位

for (int i = 1; i < 4; i++) {
    arr[i] = arr[i + 1];
}

// 最后一个元素置为 -1 代表已删除
arr[4] = -1;


二、链表(链式存储)基本原理

单链表定义:

// 定义单链表节点结构体
struct ListNode 
{
    int val;
    struct ListNode* next;
};

// 输入一个数组,转换为一条单链表
ListNode* createLinkedList(vector<int>& arr) 
{
    if (arr.empty()) 
    {
        return NULL;
    }
    ListNode* head = new ListNode(arr[0]);
    ListNode* cur = head;
    for (int i = 1; i < arr.size(); i++) 
    {
        cur->next = new ListNode(arr[i]);
        cur = cur->next;
    }
    return head;
}

数组简单的说就是一块连续的内存空间,有了这块内存空间的首地址,就能直接通过索引计算出任意位置的元素地址。链表不一样,一条链表并不需要一整块连续的内存空间存储元素。链表的元素可以分散在内存空间的天涯海角,通过每个节点上的 next, prev 指针,将零散的内存块串联起来形成一个链式结构。

数组最大的优势是支持通过索引快速访问元素,而链表就不支持。因为元素并不是紧挨着的,所以如果你想要访问第 3 个链表元素,你就只能从头结点开始往顺着 next 指针往后找,直到找到第 3 个节点才行。

1.单链表的基本操作

单链表的遍历/查找/修改

比方说,我想访问单链表的每一个节点,并打印其值,可以这样写:

  // 创建一条单链表

struct ListNode* head = createLinkedList((int[]){1, 2, 3, 4, 5}, 5);

// 遍历单链表
struct ListNode* p = head;

while (p != NULL) 
{
    printf("%d\n", p->val);

    p = p->next;
}

第一种在单链表头部插入新元素0。

//C语言写法:
// 创建一条单链表
struct ListNode* head = createLinkedList((int[]){1, 2, 3, 4, 5}, 5);

// 在单链表头部插入一个新节点 0
struct ListNode* newHead = (struct ListNode*)malloc(sizeof(struct ListNode));
newHead->val = 0;
newHead->next = head;
head = newHead;
// 现在链表变成了 0 -> 1 -> 2 -> 3 -> 4 -> 5
-------------------------------------------------------------------------------------------

//C++写法:
// 创建一条单链表
ListNode* head = createLinkedList({1, 2, 3, 4, 5});

// 在单链表头部插入一个新节点 0
ListNode* newHead = new ListNode(0);
newHead->next = head;
head = newHead;
// 现在链表变成了 0 -> 1 -> 2 -> 3 -> 4 -> 5

第二种,在单链表尾部插入新元素6

这个操作稍微复杂一点,因为我们要先从头结点开始遍历到链表的最后一个节点,然后才能在最后一个节点后面再插入新节点:

//C语言写法

// 创建一条单链表
struct ListNode* head = createLinkedList((int[]){1, 2, 3, 4, 5}, 5);

// 在单链表尾部插入一个新节点 6
struct ListNode* p = head;
// 先走到链表的最后一个节点
while (p->next != NULL)
{
    p = p->next;
}
// 现在 p 就是链表的最后一个节点

// 分配内存并赋值新节点
struct ListNode* new_node = (struct ListNode*)malloc(sizeof(struct ListNode));//注意

new_node->val = 6;
new_node->next = NULL;

// 将新节点插入到链表的尾部
p->next = new_node;

----------------------------------------------------------------------------------------


//C++写法
// 创建一条单链表
ListNode* head = createLinkedList({1, 2, 3, 4, 5});

// 在单链表尾部插入一个新节点 6
ListNode* p = head;
// 先走到链表的最后一个节点
while (p->next != nullptr)
{
    p = p->next;
}
// 现在 p 就是链表的最后一个节点
// 在 p 后面插入新节点
p->next = new ListNode(6);//注意

第三种,在单链表中间插入新元素

这个操作稍微有点复杂,我们要先找到要插入位置的前驱节点,然后操作前驱节点把新节点插入进去:

// 创建一条单链表
ListNode* head = createLinkedList({1, 2, 3, 4, 5});

// 在第 3 个节点后面插入一个新节点 66
// 先要找到前驱节点,即第 3 个节点
ListNode* p = head;
for (int i = 0; i < 2; i++) 
{
    p = p->next;
}
// 此时 p 指向第 3 个节点

// 创建新节点并插入
ListNode* newNode = new ListNode(66);
newNode->next = p->next;
p->next = newNode;

// 现在链表变成了 1 -> 2 -> 3 -> 66 -> 4 -> 5

在单链表中删除一个节点

删除一个节点,首先要找到要被删除节点的前驱节点,然后把这个前驱节点的 next 指针指向被删除节点的下一个节点。这样就能把被删除节点从链表中摘除了。

// 创建一条单链表
ListNode* head = createLinkedList({1, 2, 3, 4, 5});

// 删除第 4 个节点,要操作前驱节点
ListNode p = head;
for (int i = 0; i < 2; i++) {
    p = p.next;
}

// 此时 p 指向第 3 个节点,即要删除节点的前驱节点
// 把第 4 个节点从链表中摘除
p.next = p.next.next;

// 现在链表变成了 1 -> 2 -> 3 -> 5

在单链表尾部删除元素

ListNode* head = createLinkedList({1, 2, 3, 4, 5});

    ListNode* p = head;
    // 找到倒数第二个节点
    while (p->next->next != nullptr) 
    {
        p = p->next;
    }

    // 删除尾节点
    p->next = nullptr;

// 现在链表变成了 1 -> 2 -> 3 -> 4

在单链表头部删除元素

// 创建一条单链表
ListNode* head = createLinkedList({1, 2, 3, 4, 5});

// 删除头结点
head = head->next;

// 现在链表变成了 2 -> 3 -> 4 -> 5

2.双链表的基本操作

//c语言写法
#include <stdio.h>
#include <stdlib.h>

// 定义双向链表结点结构
struct DoublyListNode {
    int val;                    // 结点值
    struct DoublyListNode* next; // 指向下一个结点的指针
    struct DoublyListNode* prev; // 指向前一个结点的指针
};

// 创建双向链表的函数
struct DoublyListNode* createDoublyLinkedList(int arr[], int size) 
{
    // 如果输入数组为空或大小为0,则返回空指针

    if (arr == NULL || size == 0)  return NULL;

    // 创建链表头结点
    struct DoublyListNode* head = (struct DoublyListNode*)malloc(sizeof(struct DoublyListNode));

    head->val = arr[0];
    head->next = NULL;
    head->prev = NULL;

    // 保存当前结点指针
    struct DoublyListNode* cur = head;

    // 循环迭代创建链表
    for (int i = 1; i < size; i++) 
  {
        // 创建新结点
        struct DoublyListNode* newNode = (struct DoublyListNode*)malloc(sizeof(struct DoublyListNode));
        newNode->val = arr[i];
        // 当前结点指向新结点,新结点指向当前结点,然后当前结点移动到新结点
        cur->next = newNode;
        newNode->prev = cur;
        cur = cur->next;
    }

    return head; // 返回链表头结点指针
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int size = sizeof(arr) / sizeof(arr[0]);
    // 调用函数创建双向链表并得到头结点指针
    struct DoublyListNode* head = createDoublyLinkedList(arr, size);

    // 可以在这里输出或对双链表进行其他操作

    return 0;
}
//C++写法
#include <iostream>
#include <vector>

struct DoublyListNode
 {
    int val;
    DoublyListNode* next;
    DoublyListNode* prev;
    DoublyListNode(int x) : val(x), next(nullptr), prev(nullptr) {}
};

DoublyListNode* createDoublyLinkedList(std::vector<int> arr) 
{
    if (arr.empty()) 
    {
        return nullptr;
    }
    DoublyListNode* head = new DoublyListNode(arr[0]);
    DoublyListNode* cur = head;

    // for 循环迭代创建双链表
    for (size_t i = 1; i < arr.size(); i++)
   {
        DoublyListNode* newNode = new DoublyListNode(arr[i]);
        cur->next = newNode;
        newNode->prev = cur;
        cur = cur->next;
    }
    return head;
}

int main() {
    DoublyListNode* head = createDoublyLinkedList({1, 2, 3, 4, 5});

    // 可以在这里输出或对双链表进行其他操作

    return 0;
}

双链表的遍历/查找/修改

//创建一条双链表
DoublyListNode* head = createDoublyLinkedList({1,2,3,4,5});

//从头遍历双链表
for (struct DoublyListNode* p = head;p!=NULL;p=p->next)
{
  print("%d\n",p->val);
}

从尾遍历双链表
for (struct DoublyListNode* p = tail;p!=NULL;p=p->prev)
{
  print("%d\n",p->val);
}

在双链表头部插入新元素

在双链表尾部插入新元素

// 创建一条双链表
DoublyListNode* head = createDoublyLinkedList({1, 2, 3, 4, 5});

DoublyListNode* tail = head;
// 先走到链表的最后一个节点
while (tail->next != Null) 
{
    tail = tail->next;
}

// 在双链表尾部插入新节点 6
DoublyListNode *newNode = new DoublyListNode(6);
tail->next = newNode;
newNode->prev = tail;
// 更新尾节点引用
tail = newNode;

// 现在链表变成了 1 -> 2 -> 3 -> 4 -> 5 -> 6

在双链表中间插入新元素

// 创建一条双链表
DoublyListNode* head = createDoublyLinkedList({1, 2, 3, 4, 5});

// 在第 3 个节点后面插入新节点 66
// 找到第 3 个节点
DoublyListNode* p = head;
for (int i = 0; i < 2; i++) 
{
    p = p->next;
}

// 组装新节点
DoublyListNode* newNode = new DoublyListNode(66);
newNode->next = p->next;
newNode->prev = p;

// 插入新节点
p->next->prev = newNode;
p->next = newNode;

// 现在链表变成了 1 -> 2 -> 3 -> 66 -> 4 -> 5

在双链表中删除一个节点

// 创建一条双链表
DoublyListNode* head = createDoublyLinkedList({1, 2, 3, 4, 5});

// 删除第 4 个节点
// 先找到第 3 个节点
DoublyListNode* p = head;
for (int i = 0; i < 2; i++) 
{
    p = p->next;
}

// 现在 p 指向第 3 个节点,我们把它后面那个节点摘除出去
DoublyListNode* toDelete = p->next;

// 把 toDelete 从链表中摘除
p->next = toDelete->next;
toDelete->next->prev = p;

// 把 toDelete 的前后指针都置为 null 是个好习惯(可选)
toDelete->next = null;
toDelete->prev = null;

// 现在链表变成了 1 -> 2 -> 3 -> 5

在双链表头部删除元素

// 创建一条双链表
DoublyListNode* head = createDoublyLinkedList({1, 2, 3, 4, 5});

// 删除头结点
DoublyListNode* toDelete = head;
head = head->next;
head->prev = NULL;

// 清理已删除节点的指针
toDelete->next = NULL;

// 现在链表变成了 2 -> 3 -> 4 -> 5

在双链表尾部删除元素

// 创建一条双链表
DoublyListNode* head = createDoublyLinkedList({1, 2, 3, 4, 5});

// 删除尾节点
DoublyListNode* p = head;
// 找到尾结点
while (p->next != null)
 {
    p = p->next;
 }

// 现在 p 指向尾节点
// 把尾节点从链表中摘除
p->prev->next = null;

// 把被删结点的指针都断开是个好习惯(可选)
p->prev = null;

// 现在链表变成了 1 -> 2 -> 3 -> 4

后面再做一个补充吧

三、队列-栈基本原理

1.用链表实现队列、栈

2.用数组实现队列、栈

3.双端队列(Deque)原理及实现

四、哈希表基本原理

1.拉链法实现哈希表

2.开放寻址法实现哈希表

3.开放寻址法的两种代码实现

  • 55
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值