前言
我发现在学习算法的过程中,我还不太了解基本的数据结构用法和实现原理,在做算法题时,感觉有点吃力,所以我这一章好好总结了一下数据结构的特点。只有掌握好底层结构,才能准确利用每个数据结构的特点,并理解写的代码的时间复杂度。
一、数组(顺序存储)基本原理
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
后面再做一个补充吧