【数据结构入门】链表

目录

[1. 概念](#1. 概念)

[2. 链表的实现](#2. 链表的实现)

3.相关OJ题

3.1删除有序数组中的重复项

分析:

代码:

[3.2 数组形式的整数加法](#3.2 数组形式的整数加法)

分析:

代码:

[3.3 反转单链表(非递归)](#3.3 反转单链表(非递归))

分析1:

代码1:

分析2:

代码2:


1. 概念

链表是一种结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。也就是说链表的每一个节点,物理上不是连续存储的,每一个节点只有下一个结构体的地址信息

相较于顺序表而言,每次新增一个节点只会新开辟一个节点的空间,而不是和顺序表一样,每次扩容需要翻倍增加新的空间。

2. 链表的实现

主要实现头插、头删、尾插、尾删。

注意:

①链表一般会设置一个头结点,这个头结点一般是一个结构体指针,那么要修改链表的时候(插入删除)需要传入这个指针的地址,也就是二级指针,才能修改链表的值

②头插节点的时候,情况要考虑完全,头结点可能为空,头结点可能有值;当头结点为空的时候,直接将新节点作为头结点,当头结点不为空的时候需要将头结点的下一个节点保存起来(NULL或者其他节点),将头结点的next置为新节点,保存的节点作为新节点的next。

③尾删节点的时候,需要分三种情况,如果头结点为空,提示不能删除,如果只有头结点有值,那么头结点置空,如果至少有两个节点,那么需要找到倒数第二个节点(先找到最后一个,用一个变量每次保存当前遍历的节点),只需要释放倒数第一个节点,让倒数第二个节点的next指向空即可。

③查找节点的时候只需要传入头结点指针即可(结构体变量的地址),这里不传二级指针的原因是因为,没有涉及到修改链表的内容,这里如果找到了那个结构体,只需要返回当前结构体指针即可,后续根据这个结构体地址可以对这个结构体进行修改。

④这里删除、插入指定位置的后一个元素,并不是插入、删除当前元素,因为形参只有当前结构体地址,如果要删除当前的元素,需要再传一个头指针。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include"single_list.h"

// 创建新节点
SingleListNode* createNode(dataType data)
{
	SingleListNode* node = (SingleListNode*)malloc(sizeof(SingleListNode));
	if (node == NULL)
	{
		printf("申请内存失败!");
		exit(1);
	}
	node->data = data;
	node->next = NULL;
	return node;
}

// 尾插
void push_back(SingleListNode** head, dataType data)
{
	SingleListNode* newNode = createNode(data);
	if (*head == NULL)
	{

		*head = newNode;
	}
	else
	{
		SingleListNode* curr = *head;
		while (curr->next != NULL) // 找到最后一个节点
		{
			curr = curr->next;
		}
		curr->next = newNode;
	}
}

// 尾删
void pop_back(SingleListNode** head)
{
	if (*head == NULL)  // 1.链表为空
	{
		printf("空链表无法删除!");
		exit(-1);
	}
	else if ((*head)->next ==NULL) // 2.链表只有一个元素
	{
		free(*head);
		(*head) = NULL;
	}
	else 
	{
		// 3.链表至少2个元素
		// 遍历到倒数第二个
		SingleListNode* curr = *head;
		SingleListNode* prev = NULL;
		while (curr->next != NULL)
		{
			prev = curr; // 倒数第二个节点
			curr = curr->next;
		}
		free(curr);
		prev->next = NULL;
	}
	
}

// 头插
void push_front(SingleListNode** head, dataType data)
{
	SingleListNode* node = createNode(data);
	node->next = *head;
	*head = node;
}

// 头删
void pop_front(SingleListNode** head)
{
	if (*head == NULL)
	{
		printf("链表为空,不能删除");
		exit(-1);
	}
	SingleListNode* tmp = *head;
	*head = (*head)->next;
	free(tmp);
}

// 遍历链表打印
void list_print(SingleListNode* head)
{
	// 因为由头节点所以不需要判空
	SingleListNode* curr = head;
	while (curr != NULL)
	{
		printf("%d\n", curr->data);
		curr = curr->next;
	}
}

// 查找
SingleListNode* find(SingleListNode* head, dataType data)
{
	if (head == NULL)
	{
		printf("链表为空,无法查找!");
		exit(-1);
	}
	while (head) 
	{
		if (head->data == data)
		{
			return head;
		}
		head = head->next;
	}
	return NULL; 
}

// 插入指定位置的后面
void insert(SingleListNode** pos, dataType data) 
{
	assert(*pos); 
	SingleListNode* newNode = createNode(data);
	newNode->next = (*pos)->next;
	(*pos)->next = newNode;
}

// 删除指定位置的节点后面
void erase_pos(SingleListNode** pos)
{
	assert(*pos);
	SingleListNode* tmp = (*pos)->next;
	(*pos)->next = tmp->next;
}

3.相关OJ题

3.1删除有序数组中的重复项

给你一个 非严格递增排列 的数组 nums ,请你**原地** 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。

考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:

  • 更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
  • 返回 k

示例 1:

复制代码
输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 `2` ,并且原数组 nums 的前两个元素被修改为 `1`, `2 `。不需要考虑数组中超出新长度后面的元素。

分析:

这里使用双指针法,左指针起始位置下标为0,右指针起始位置下标为1,判断右指针是否小于整个数组的长度。

当右指针所指向的数字等于左指针指向的数字,此时两个指针同时后移;

当右指针所指向的数字不等于左指针指向的数字,此时需要将左指针指向的数字存储到当前数组的第一个位置中(使用index作为下标);此时两个指针同时后移。

这个数组的最后两个数,存在两种情况:

情况一:当这两个数不相等的时候,此时由于右指针最后+1,所以右指针会出右边界导致循环退出,最后一个数也是单独的数字,所以这里需要将最后一个数单独进行存储。

情况二:此时这两个数如果相同,那么在最后一次循环的时候已经保存过了,但是因为要保持程序的一致性,并且多赋一次值也不会对整个程序造成性能影响,所以这里的情况按照情况一来处理。

代码:

cpp 复制代码
int removeDuplicates(int* nums, int numsSize) {
    int left = 0;
    int right = 1;
    int index = 0;
    // 循环条件写在循环体中
    for(right;right < numsSize;)
    {
        // 左右指针值相同,左右指针移动
        if(nums[left] == nums[right])
        {
           
        }else
        {
            // 左右指针值不相同,左指针赋值给index下标处
            nums[index++] = nums[left];
        }
        left++;
        right++;
    }
    // 最后一个元素若不同,需要再次赋值,相同则赋值没影响
    nums[index] = nums[left];
    index++;// 最后新数组的下标就是元素的个数
    return index;
}

3.2 数组形式的整数加法

整数的 数组形式 num 是按照从左到右的顺序表示其数字的数组。

  • 例如,对于 num = 1321 ,数组形式是 [1,3,2,1]

给定 num ,整数的 数组形式 ,和整数 k ,返回 整数 num + k数组形式

示例 1:

复制代码
输入:num = [1,2,0,0], k = 34
输出:[1,2,3,4]
解释:1200 + 34 = 1234

分析:

这道题目的细节非常多,虽然标注的是简单题,但是可能对于初学者来说是中等题。总体思路一定不能直接把数组的数字转换成整型,如果数组的长度>30,那么整型就会溢出,所以这里其实在考大数相加的问题。

我们可以将数组的每一位数字和k的每一位数字进行相加,一共要加多少次呢?3位数+2位数只需要加3次,也就是说循环的次数是位数更多的数字的位数。

那么我们可以将相加的结果存在一个数组中,那么数组的长度怎么算呢?刚刚我们已经计算出来了两数较长的以为数的位数,只需要在此位数+1就是我们开辟数组的长度了,最极端的情况,三位数+三位数在进位的情况下,是四位数。流程梳理清楚,开始写代码。

①计算出k的位数为k_size;

②计算出k_size和num_size较大值,len;

③开辟出len+1的数组空间;

④开始按位相加,总共循环len次;

⑤当数组中的数字位数 < k的位数的时候,数组会越界,所以这里需要判断数组的合法下标,如果数组下标合法那就将该位参与运算,若不合法则使用0参与运算;

⑥k的末位和数组的最后一位数字(合法)相加,这里记录变量nextNum,表示是否进位,如果和>9那么说明需要进位,nextNum标注为1,否则是0,下一次按位相加的时候+nextNum,;

⑦此时和记录为ret,将ret的个位数存在返回数组的第一位、第二位以此类推;

⑧循环结束之后,需要判断两个数的第一位相加之后,是否需要进位,例如215 + 806 = 1021;

由于循环只进行了三次,所以进位的数字没有进行保存,这里判断如果进位位1,那么需要单独处理,将1赋给返回数组的最新位,此时记录返回数组的下标再+1;

⑨将数组逆置,返回返回数组,此时的下标就是该数组的长度。

代码:

cpp 复制代码
/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* addToArrayForm(int* num, int numSize, int k, int* returnSize) {
    /*
     1.判断两个数哪个位数更长,创建新数组的长度就是改位数+1;
     2.从末尾开始按位相加,一共加的次数是长的那个数字的位数;
     3.相加完毕之后,是否进位需要进行保存,计算完毕的数字需要存放在新数组的首元素位置;
     4.循环开始的时候需要增加进位;
     5.如果数组的位数更大,那么需要判断合法性;
     6.将最后的数组逆序就是最后的结果。
    */
    int k_num = k;
    // 1.判断数字的长度
    int k_size = 0;
    while (k) {
        k /= 10;
        k_size++;
    }
    int len = k_size > numSize ? k_size : numSize;
    int* retArr =
        (int*)malloc(sizeof(int) * (len + 1)); // 三位数+三位数 = 四位数
    // 2.按位计算
    int num_i = numSize - 1;
    int nextNum = 0; // 是否需要进位
    int ret_i = 0;
    while (len--) {
        // 当数组长度小于数字长度,那么此时len是数字长度就会越界

        // 判断数组下标是否合法
        int legal_num = 0;
        if(num_i >= 0)
        {
            legal_num = num[num_i];
            num_i--;
        }
        int ret = legal_num + k_num % 10 + nextNum;
        if (ret > 9) {
            nextNum = 1;
        } else {
            nextNum = 0;
        }
        k_num /= 10;
        // 给新数组赋值
        retArr[ret_i++] = ret % 10;
    }
    // 如果nextNum == 1说明,第一位相加的时候需要进位,但是还没有处理
    if(nextNum == 1)
    {
        retArr[ret_i] = 1;
        ret_i++;
    }

    // 将数组逆序
    int left = 0;
    int right = ret_i - 1;
    while(left < right)
    {
        int tmp = retArr[left];
        retArr[left] = retArr[right];
        retArr[right] = tmp;
        left++;
        right--;
    }
    *returnSize = ret_i;
    return retArr;
}

3.3 反转单链表(非递归)

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例 1:

分析1:

①需要准备三个指针,第一个指针指向空,二、三指针分别指向head和head的next;

②这里需要先判断链表元素为1或者0的情况,这里直接返回head本身即可;

③2个或者2个以上的元素:第二个指针指向第一个指针、第一个指针重新赋值为第二个指针,第二个指针重新赋值为第三个指针、第三个指针重新赋值为他的下一个指针。

所以每次逆向的操作都由n1和n2来完成,n3需要每次向后即可,给n2提供保证。

④循环条件是当第三个指针为空就停止,循环停止的时候,第二个指针还没来得及指向第一个指针循环就结束了,这时候需要手动进行指向。

代码1:

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* reverseList(struct ListNode* head) {
    // 三指针
    struct ListNode* n1 = NULL;
    struct ListNode* n2 = head;
    if(head ==NULL || head->next == NULL)
    {
        return head;
    }
    struct ListNode* n3 = head->next;// 保存第三个变量
    while(n3)
    {
      n2->next = n1;
      n1 = n2;
      n2 = n3;
      n3 = n3->next;
    }
   // 此时n3为空,但是n2没有指向n1
   n2->next = n1;
    return n2;
}

分析2:

使用一个新链表,遍历老链表,每次遍历一个元素对新链表进行头插,需要注意的是需要将当前指针的下一个元素进行保存,不然无法迭代。

代码2:

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* reverseList(struct ListNode* head) {
    struct ListNode* newhead = NULL;
    struct ListNode* curr = head; 
    // 遍历原链表,将元素头插到新链表中
    while (curr) {
        struct ListNode* nt = curr->next;

        curr->next = newhead;
        newhead = curr;
        curr = nt;
    }
    return newhead;
}
相关推荐
快去睡觉~10 分钟前
力扣301:删除无效的括号
数据结构·算法·leetcode
玖剹32 分钟前
Linux文件操作:从C接口到系统调用
linux·服务器·c语言·c++·笔记·ubuntu
YY_TJJ1 小时前
8.4 Java Web(Maven P50-P57)
java·开发语言·maven
技术炼丹人1 小时前
从RNN为什么长依赖遗忘到注意力机制的解决方案以及并行
人工智能·python·算法
闪电麦坤952 小时前
数据结构:反转链表(reverse the linked list)
数据结构·链表
NfN-sh2 小时前
计数组合学7.12( RSK算法的一些推论)
笔记·学习·算法
ikkkkkkkl2 小时前
LeetCode:15.三数之和&&18.四数之和
c++·算法·leetcode
农夫山泉(代码版)2 小时前
Linux驱动学习(七)平台总线框架
linux·服务器·学习
驱动起爆大师x_x2 小时前
STM32_Hal库学习SPI
stm32·嵌入式硬件·学习
小眼睛FPGA2 小时前
【盘古100Pro+开发板实验例程】FPGA学习 | 基于 UDP 的以太网传输实验例程
科技·单片机·学习·ai·fpga开发·fpga