数据结构与算法--线性表(数组&链表)

专栏用于写各个数据结构的基本算法和复杂算法思路(个人理解),便于理清思路

文章中算法题源主要来自于《数据结构与算法分析–C语言描述》和 天勤(考研机构) 的《2021 数据结构高分笔记》,分类按顺序排列,因其大部分在 LeetCode 中有对应,故附上题号以便参考

加了序号的注释是为了方便不同方法类似思路的引用

[TOC]

数组(顺序表)

基本操作

/* 初始化数组为 0 */
void initZero(int *nums, int numsSize){
int i = 0;
while(i < numsSize)
nums[i++] = 0;
}

/* 交换数组下标 i,j 对应的值 */
void swap(int *nums, int i, int j){
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}

/* 另一个等价的方法,不过调用时格式为 swap(&nums[i], &nums[j]) */
void swap(int *ni, int *nj){
int t = *ni;
*ni = *nj;
*nj = t;
}


/* 返回关键字首次出现的位置,未找到返回 -1 */
int find(int *nums, int val){
int i, length;
length = sizeof(nums) / sizeof(int);

for (i = 0; i < length && nums[i] != val; i++)
;
if (i == length)
return -1;
else
return i;
}

/* 倒转数组 */
void reverse(int *nums, int begin, int end){
int i = begin;

while(i < end){
swap(&nums[i++], &nums[end--]);
}
}

/* 返回数组中的最大值 */
int maxNumber(int *nums, int numsSize){
int i, max;
for (i = 0, max = INT_MIN; i < numsSize; i++){
if (max < nums[i])
max = nums[i];
}
return max;
}

/* 返回数组中最大值的下标 */
int maxIndex(int *nums, int numsSize){
int i, max, maxi;
for (i = 0, max = INT_MIN; i < numsSize; i++){
if (max < nums[i]){
max = nums[i];
maxi = i;
}
}
return maxi;
}

/* 对数组中所有数加 val */
void addVal(int *nums, int numsSize, int val){
int i = 0;
while(i < numsSize)
nums[i++] += val;
}

从位置 i 插入一个数(假设数组长度足够)

void insert(int *nums, int i, int numsSize, int data){
/* 下标从后到i,各向右移动一位,若从i开始向右移动,则需要保存下一个数的值,否则会因覆盖丢失 */
int cur = numsSize; /* numsSize为当前存储的数据长度 */
while(cur-- > i)
nums[cur + 1] = nums[cur];
nums[cur + 1] = data; /* 此时cur = i-1,故需要+1在位置 i 处插入数据(当然,直接 nums[i] 也可) */
}

循环右移 k 位

解法1: 通过三次反转达到移动的效果

对应于 LeetCode 189题

算法思路:有两个思路原理一致的方式

(方式一)先旋转前numsSize-k%numsSize,再旋转后k%numsSize个,最后整体旋转,可以达到循环右移k位的效果,即循环左移numsSize-k位

(方式二)先整体旋转,再旋转前k%numsSize位和后numsSize-k%numsSize位,可循环左移k位

注:取余可以跳过多余的移动

/* 方式一 */
void rotate(int* nums, int numsSize, int k){
int r = k % numsSize;

reverse(nums, 0, numsSize-r-1); /* reverse() 见基础操作 */
reverse(nums, numsSize-r, numsSize-1);
reverse(nums, 0, numsSize-1);

}

/* 方式二 */
void rotate(int* nums, int numsSize, int k){
int r = k % numsSize;

reverse(nums, 0, numsSize-1); /* reverse() 见基础操作 */
reverse(nums, 0, r-1);
reverse(nums, r, numsSize-1);
}

删除有序表中的重复元素

算法思路:若数组内元素小于 2,直接返回 numsSize,不需对 nums 进行修改。

因为有序,直接用 tmp 记录上个重复的数,初始化 tmp 为数组中的第一个元素。

从第二个元素开始对比,用 n 记录重复次数,重复时 +1,不重复时将数组内元素向前移动 n 位。

/* 删除有序数组内的重复元素,并返回数组剩余的元素个数 */
int removeDuplicates(int* nums, int numsSize){
int i, tmp, n; /* tmp 记录上个重复元素,n 记录重复次数 */

if(numsSize < 2){
return numsSize;
}
tmp = nums[0];
for (i = 1, n = 0; i < numsSize; i++){
if (tmp == nums[i])
n++;
else{
tmp = nums[i];
nums[i - n] = nums[i];
}
}
return numsSize - n;
}

返回数组中前 k 个高频

解法1: 暴力解法

对应于 LeetCode 347题

算法思路:创建两个数组:数组 numsTimes 记录对应 nums 的频数,数组 frequent 记录前 k 个高频对应的值。(虽然代码不复杂,但其实算是暴力解法)

(最初想法:建立一个数组记录频数,一个链表记录出现过的数,最后根据链表查找,后来发现创建多个链表结构会报错,可能是平台原因,故改成下方代码 )

int maxNumber(int *nums, int numsSize); /* 返回数组中的最大值 */
int maxIndex(int *nums, int numsSize); /* 返回数组中最大值的下标 */
void initZero(int *nums, int numsSize);

/* 仅能处理数组元素全大于 0 的情况 */
int* topKFrequent_pos(int* nums, int numsSize, int k){
int i, cur, maxi;

int max = maxNumber(nums, numsSize);
int numsTimes[max+1];
initZero(numsTimes, max+1);

for (i = 0; i < numsSize; i++){
cur = nums[i];
++numsTimes[cur]; /* 记录频数 */
}

int *frequent = (int *)malloc(sizeof(int) * k); /* 因为需返回数组,故需要malloc分配空间 */
i = 0;
while(i < k){
maxi = maxIndex(numsTimes, max+1);
frequent[i++] = maxi;
numsTimes[maxi] = 0; /* 将当前最高频数置为0 */
}

return frequent;
}

/* 对于所有情况 */
void addVal(int *nums, int numsSize, int val);
int* topKFrequent(int* nums, int numsSize, int k){
/* 思路同上,只是先对数组做个预处理,让其中的值必然大于0 */
int i, cur, maxi;

int max = abs(maxNumber(nums, numsSize)); /* 取其绝对值是为了处理存在负数的情况 */
addVal(nums, numsSize, max);

int numsTimes[2 * max + 1]; /* 2*max+1 是因为此时 nums 数组中最大的是 max+|max|,+1 是为了直接以其值作为索引,省去后续为了不越界的 -1 操作 */
initZero(numsTimes, 2 * max + 1);

for (i = 0; i < numsSize; i++){
cur = nums[i];
++numsTimes[cur];
}

int *frequent = (int *)malloc(sizeof(int) * k);
i = 0;
while(i < k){
maxi = maxIndex(numsTimes, 2*max+1);
frequent[i++] = maxi - max; /* 需 -max, 对应于原 nums 中的值 */
numsTimes[maxi] = 0;
}

return frequent;
}

一个更简洁的表达方式(但会多 o(k) 的时间)

int maxNumber(int *nums, int numsSize);
int maxIndex(int *nums, int numsSize);
void initZero(int *nums, int numsSize);
int* topKFrequent_pos(int* nums, int numsSize, int k);

/* 是上面方法的汇总 */
int* topKFrequent(int* nums, int numsSize, int k){
int max = abs(maxNumber(nums, numsSize));
addVal(nums, numsSize, max);
int *frequent = topKFrequent_pos(nums, numsSize, k);
addVal(frequent, k, -max); /* 该语句为多余时间来源 */
return frequent;
}

链表

节点定义

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

基本操作·

/* 判空 */
int isempty(struct Listnode *node){
return node == NULL;
}

/* 判断是否为尾指针,对于空指针返回 0 */
int islast(struct Listnode *node){
if (!node && node->next == NULL)
return 1;
else
return 0;
}

/* 返回关键字首次出现的节点指针,若未找到返回 NULL */
struct Listnode *find(struct Listnode *head, int val){
struct Listnode *ret = head;

while (ret != NULL){
if (ret->val == val)
return ret;
ret = ret->next;
}
return ret;
}

/* 插入节点(头插法),可实现链表逆置 */
void insert(struct Listnode *head, struct Listnode *node){
node->next = head->next;
head->next = node;
}

/* 删除下一节点 */
void deleteNext(struct Listnode *node){
struct Listnode *t = node->next;
node->next = t->next;
free(t);
}

/* 删除当前节点(无法处理最后一个节点),删除成功返回1,否则(为最后一个节点)返回0 */
int deleteCur(struct Listnode *cur){
/* 思路为用下一个节点覆盖当前节点,然后删除下一节点,达到删除当前节点的效果 */
struct Listnode *next = cur->next;
if (!isempty(next)){
cur->val = next->val;
cur->next = next->next;
free(next);
return 1;
}
return 0;
}

反转链表(无额外头结点,即判空的语句为 head == NULL)

解法1: 前后双指针

对应于 LeetCode 206 题

算法思路:利用前后双指针p,q实现节点之间的反转,p 初始为 head ,q 始终为 p->next,即指向需翻转的下一个节点,故还需要一个节点 t 保存 q->next,在通过 q->next = p 翻转链表后,令 q = t,p = q 以移动指针处理下一节点,当 q 为尾指针即 t 为 NULL 时代表完成反转(以 q == NULL 为结束条件更好,否则 t 需要提前初始化,而且因为 q 在反转后指向 t 所指,故依然正确)。

首个节点需要对 *head 单独处理,否则会导致局部循环,处理也很简单,在初始化用 q 保存了 head-next 后,令head->next = NULL。

在初始化前需要判断链表是否为空,若为空直接返回 head。

struct ListNode *reverseList(struct ListNode* head){
struct ListNode *p, *q, *t;

if(head != NULL){
p = head;
q = p->next;
head->next = NULL;

while(q != NULL){
t = q->next;
q->next = p; /* 1.这一步为反转步骤,其余均是为了两个指针能够向后移动 */
p = q;
q = t;
}
head = p; /* 2.仅为了只写一个 return,也可以在这里直接 return p; */
}
return head;
}

解法2:递归

算法思路:假设链表有 n 个结点,链表可以分解为 1 个头结点和 n-1 个剩余结点两个部分,剩余结点组成的链表亦可继续分解,直到 1 个头结点和 0 个剩余结点,也即 head->next == NULL,这时便有了递归终止的条件,满足该条件时返回 head。

现在根据递归的特性,倒退回 1 个头结点和 1 个剩余结点的情况,反转只有两个节点的链表仅需要头结点的指针,此时令 head->next->next = head(该步和解法1的注释1所在行的目的一样), head->next = NULL 即可,这是基准情况。递归过程本身可以看成保存数据的栈,所以不需要像解法1那样额外记录下一个反转的结点,之后每次倒退其实都可以看成只有两个“结点”的链表,一个是头结点,一个是已经反转好的链表。

现在最后的需求是返回新的头结点,即原来链表的尾结点,有三种方式(其实想法一致):

  1. 定义一个全局变量 *rhead ,rhead 在触发递归终止条件时记录当前所处结点,随后一直返回 rhead(因为 head 会随递归出栈而变化,故记录)。
  2. 和 1 思路一样,不过是在函数内定义变量,然后将递归的步骤在一个小函数中重写,该函数接受的参数为(head, &rhead),这里 rhead 的声明和上面不同,是指向结点类型的指针的指针。
  3. 递归在终止时返回的就是反转后的 head,故只需要记录该指针并令其始终为返回值即可(当前递归程序并不需要额外的返回值来帮助反转链表)
/* 方式1 */
struct ListNode *rhead;

struct ListNode *reverseList(struct ListNode* head){
if(head == NULL || head->next == NULL){
rhead = head;
return rhead;
}

reverseList(head->next);
head->next->next = head;
head->next = NULL;
return rhead;
}

/* 方式3 */
struct ListNode *reverseList(struct ListNode* head){
if (head == NULL || head->next == NULL)
return head;

struct Listnode *rhead = reverseList(head->next);
head->next->next = head; /* 反转 */
head->next = NULL;
return rhead;
}

返回倒数第 k 个结点的值

解法1: 快慢指针

对应于 LeetCode 面试题 02.02.

算法思路:初始化两个指针 p 和 q,p 先逐个遍历结点,用 i 记录 p 遍历的结点个数,当 p 遍历了 k-1 个结点后,q 开始和 p 一起逐步遍历,当 q == NULL时, q 恰好指向链表的倒数第 k 个结点。

int kthToLast(struct ListNode *head, int k){
struct ListNode *p = head;
struct ListNode *q = head;
int i = 0;

while (p != NULL){
p = p->next;
if (i > k-1) /* k-1 是因为 k>=1,而当 k==1时,q与p指向同一个结点(可以初始化 i=1,if(i >k)) */
q = q->next;
else
++i; /* ++i 移到 if 语句前也可以,放在这里是因为当 i>k 后就没有必要再对 i 进行 +1 操作了 */
}
return q->val;
}

解法2: 递归

算法思路:初始化全局变量 i = 0,当递归至 NULL 时(若递归至尾结点,那么当 k == 1时还需额外判断,所以这里就改成 NULL),i++(或 i = 1),然后开始递归出栈,出栈时每次判断 i 是否等于 k,并令 i++,若等于,返回当前结点的值

int i = 0;

int kthToLast(struct ListNode *head, int k){
if (head == NULL){
i++; /* 在 LeetCode 中这里应改为i = 1(原因见 LeetCode 对全局变量的处理) */
return -1;
}

int ret = kthToLast(head->next, k);
if (i++ == k)
return head->val;
else
return ret;
}