链表理论基础
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针)。链表的入口节点称为链表的头结点也就是head。最后一个节点的指针域指向null(空指针的意思)。
链表的类型
单链表
单链表中的指针域只能指向节点的下一个节点。
c
typedef struct ListNode {
int val; // 节点的值
struct ListNode* next; // 指向下一个节点的指针
} ListNode;
java
public class ListNode {
// 结点的值
int val;
// 下一个结点
ListNode next;
// 节点的构造函数(无参)
public ListNode() {
}
// 节点的构造函数(有一个参数)
public ListNode(int val) {
this.val = val;
}
// 节点的构造函数(有两个参数)
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
双链表
双链表:每一个节点有两个指针域,一个指向上一个节点,一个指向下一个节点。
双链表 既可以向前查询也可以向后查询。
循环链表
循环链表:链表首尾相连。
(循环链表可以用来解决约瑟夫环问题。)
链表的存储方式
数组是在内存中是连续分布的,但是链表在内存中不是连续分布的。
链表是通过指针域的指针链接在内存中各个节点。链表中的节点散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
链表的操作
删除节点
删除D节点,只要将C节点的next指针指向E节点就可以了。
添加节点
性能分析
链表的增添和删除都是O(1),操作不会影响到其他节点。
但是要删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
链表的长度可以不固定,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。
203.移除链表元素
删除链表中等于给定值 val 的所有节点。
链表操作的两种方式:
- 直接使用原来的链表来进行删除操作。
- 设置一个虚拟头结点在进行删除操作。
如果使用C,C++,要从内存中删除这两个移除的节点;如果使用java ,python不用手动管理内存
直接使用原来的链表来进行移除
删除头结点和移除其他节点的操作不一样,因为删除链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。只要将头结点向后移动一位就可以。
将原头结点从内存中删掉。
struct ListNode* removeElements(struct ListNode* head, int val){
struct ListNode* temp;//定义一个指针temp
while(head && head->val == val) {//检查头节点是否存在,并且头节点的值是否等于 val值
temp = head;
head = head->next;// 将新的头结点设置为head->next
free(temp);//删除原来的头结点,释放该节点的内存空间
}
struct ListNode *cur = head;//定义一个指针 cur 指向头节点,用来迭代遍历链表
while(cur && (temp = cur->next)) {// 当cur存在并且temp 指向当前节点cur的下一个节点存在时
//(此解法需要判断cur存在因为cur指向head。若head本身为NULL或者原链表中元素都为val的话,cur也会为NULL)
// 如果 temp 的值等于 val,说明需要删除该节点
if(temp->val == val) {//temp->val 是指针 temp 指向的节点的值
cur->next = temp->next;// 当前节点cur的下一个节点指向 temp 的下一个节点
free(temp);//释放 temp 的内存
}
//如果 temp 的值不等于 val,则 cur 指向下一个节点。
else
cur = cur->next;
}
// 返回头结点
return head;
}
设置一个虚拟头结点在进行删除
给链表添加一个虚拟头结点为新的头结点,原链表的所有节点就可以按照统一的方式进行移除了。
struct ListNode* removeElements(struct ListNode* head, int val){
// 定义别名 ListNode 代替结构体指针 struct ListNode
typedef struct ListNode ListNode;
// 创建一个假头结点,方便删除头结点
ListNode *shead;
shead = (ListNode *)malloc(sizeof(ListNode));
shead->next = head;
// 定义 cur 指针指向假头结点,用于迭代遍历链表
ListNode *cur = shead;
// 当 cur 后面还有节点时,进行如下操作
while(cur->next != NULL){
// 当前节点的值等于 val 时,删除该节点
if (cur->next->val == val){
ListNode *tmp = cur->next;
cur->next = cur->next->next;
free(tmp);
}
// 当前节点的值不等于 val 时,将 cur 后移一位
else{
cur = cur->next;
}
}
// 将 head 指向真正的头结点
head = shead->next;
// 释放假头结点的内存
free(shead);
// 返回处理后的链表的头结点
return head;
}
java写法
/**
* 添加虚节点方式
* 时间复杂度 O(n)
* 空间复杂度 O(1)
* @param head
* @param val
* @return
*/
public ListNode removeElements(ListNode head, int val) {
if (head == null) {
return head;
}
// 因为删除可能涉及到头节点,所以设置dummy节点,统一操作
ListNode dummy = new ListNode(-1, head);
ListNode pre = dummy;
ListNode cur = head;
while (cur != null) {
if (cur.val == val) {
pre.next = cur.next;
} else {
pre = cur;
}
cur = cur.next;
}
return dummy.next;
}
/**
* 不添加虚拟节点方式
* 时间复杂度 O(n)
* 空间复杂度 O(1)
* @param head
* @param val
* @return
*/
public ListNode removeElements(ListNode head, int val) {
while (head != null && head.val == val) {
head = head.next;
}
// 已经为null,提前退出
if (head == null) {
return head;
}
// 已确定当前head.val != val
ListNode pre = head;
ListNode cur = head.next;
while (cur != null) {
if (cur.val == val) {
pre.next = cur.next;
} else {
pre = cur;
}
cur = cur.next;
}
return head;
}
/**
* 不添加虚拟节点and pre Node方式
* 时间复杂度 O(n)
* 空间复杂度 O(1)
* @param head
* @param val
* @return
*/
public ListNode removeElements(ListNode head, int val) {
while(head!=null && head.val==val){
head = head.next;
}
ListNode curr = head;
while(curr!=null){
while(curr.next!=null && curr.next.val == val){
curr.next = curr.next.next;
}
curr = curr.next;
}
return head;
}
707.设计链表
在链表类中实现这些功能:
- get(index):获取链表第index个节点的数值。
- addAtHead(val):在链表的最前面插入一个节点。插入后,新节点将成为链表的第一个节点。
- addAtTail(val):在链表的最后面插入一个节点。
- addAtIndex(index,val):在链表第index个节点前面插入一个节点。(如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。)
- deleteAtIndex(index):删除链表的第index个节点。
typedef struct MyLinkedList {
int val;//存储节点的值
struct MyLinkedList* next;//指向下一个节点的指针
}MyLinkedList;
MyLinkedList* myLinkedListCreate() {
//这个题必须用虚拟头指针,参数都是一级指针,头节点确定后没法改指向了!!!
MyLinkedList* head = (MyLinkedList *)malloc(sizeof (MyLinkedList));//为头结点分配内存空间
head->next = NULL;//当前链表为空
return head;
}
int myLinkedListGet(MyLinkedList* obj, int index) {//MyLinkedList* obj:表示要操作的链表
MyLinkedList *cur = obj->next; // 定义指针 cur,指向链表的第一个真正存储数据的节点(实际上是头结点的下一个节点,头结点 obj 并不存储任何数据,它的作用只是作为链表的起点)
for (int i = 0; cur != NULL; i++){ // 循环遍历链表
if (i == index){ // 如果当前遍历到的节点正好是需要获取的节点
return cur->val; // 返回该节点的值,cur->val 表示当前节点 cur 的值
}
else{ // 否则继续遍历下一个节点
cur = cur->next;
}
}
return -1; // 遍历完整个链表仍然没有找到需要获取的节点,则说明索引不合法,返回 -1
}
void myLinkedListAddAtHead(MyLinkedList* obj, int val) {
MyLinkedList *nhead = (MyLinkedList *)malloc(sizeof (MyLinkedList)); // 创建一个新节点 nhead,并分配内存空间
nhead->val = val; // 设置新节点 nhead 的值为传入的参数 val。(nhead->val 是值)
nhead->next = obj->next; // 将新节点 nhead 的 next 指针指向链表原来的第一个节点。(nhead->next是指针)
obj->next = nhead; // 将头结点 obj 的 next 指针指向新节点 nhead,将新节点插入到链表的头部
}
void myLinkedListAddAtTail(MyLinkedList* obj, int val) {
MyLinkedList *cur = obj; // 定义一个指针变量 cur,初始化为头结点 obj
while(cur->next != NULL){ // 遍历链表,找到最后一个节点
cur = cur->next;
}
MyLinkedList *ntail = (MyLinkedList *)malloc(sizeof (MyLinkedList)); //创建一个新的节点 ntail,并为其分配内存空间
ntail->val = val; // 设置新节点 ntail 的值为传入的参数 val
ntail->next = NULL; // 将新节点 ntail 的 next 指针设置为 NULL,表示它是链表的最后一个节点
cur->next = ntail; // 将当前节点 cur 的 next 指针指向新节点 ntail,将新节点插入到链表的尾部
}
void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val) {
if (index == 0){ // 如果 index 等于 0,将新节点添加到链表头部
myLinkedListAddAtHead(obj, val); // 调用 myLinkedListAddAtHead() 函数,将新节点添加到链表头部
return; // 添加完成后直接返回
}
MyLinkedList *cur = obj->next; // 定义一个指针变量 cur,初始化为头结点的下一个节点
for (int i = 1 ;cur != NULL; i++){ // 遍历链表,查找要插入的位置
if (i == index){ // 如果找到了要插入的位置
MyLinkedList* newnode = (MyLinkedList *)malloc(sizeof (MyLinkedList)); // 创建一个新的节点 newnode,并为其分配内存空间
newnode->val = val; // 将新节点的值设置为传入的参数 val
newnode->next = cur->next; // 将新节点的 next 指针指向当前节点的下一个节点
cur->next = newnode; // 将当前节点的 next 指针指向新节点,将新节点插入到链表中间
return; // 插入完成后直接返回
}
else{ // 如果还没有找到要插入的位置
cur = cur->next; // 将指针 cur 指向下一个节点,继续遍历链表
}
}
}
void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index) {
if (index == 0){ // 如果 index 等于 0,删除链表中的第一个节点
MyLinkedList *tmp = obj->next; // 定义一个指针变量 tmp,指向链表中的第一个节点
if (tmp != NULL){ // 如果链表不为空
obj->next = tmp->next; // 将头结点的 next 指针指向第二个节点
free(tmp); // 释放第一个节点的内存空间
}
return; // 删除完成后直接返回
}
MyLinkedList *cur = obj->next; // 定义一个指针变量 cur,初始化为头结点的下一个节点
for (int i = 1 ;cur != NULL && cur->next != NULL; i++){ // 遍历链表,查找要删除的节点
if (i == index){ // 如果找到了要删除的节点
MyLinkedList *tmp = cur->next; // 定义一个指针变量 tmp,指向要删除的节点
if (tmp != NULL) { // 如果要删除的节点存在
cur->next = tmp->next; // 将当前节点的 next 指针指向要删除节点的下一个节点
free(tmp); // 释放要删除节点的内存空间
}
return; // 删除完成后直接返回
}
else{ // 如果还没有找到要删除的节点
cur = cur->next; // 将指针 cur 指向下一个节点,继续遍历链表
}
}
}
void myLinkedListFree(MyLinkedList* obj) {
while(obj != NULL){ // 循环遍历链表中的每个节点
MyLinkedList *tmp = obj; // 定义一个指针变量 tmp,指向当前节点
obj = obj->next; // 将头结点指针指向下一个节点,准备删除当前节点
free(tmp); // 释放当前节点的内存空间
}
} // 释放所有节点的内存空间,销毁链表
java写法
//单链表
class ListNode {
int val;
ListNode next;
ListNode(){}
ListNode(int val) {
this.val=val;
}
}
class MyLinkedList {
//size存储链表元素的个数
int size;
//虚拟头结点
ListNode head;
//初始化链表
public MyLinkedList() {
size = 0;
head = new ListNode(0);
}
//获取第index个节点的数值,注意index是从0开始的,第0个节点就是头结点
public int get(int index) {
//如果index非法,返回-1
if (index < 0 || index >= size) {
return -1;
}
ListNode currentNode = head;
//包含一个虚拟头节点,所以查找第 index+1 个节点
for (int i = 0; i <= index; i++) {
currentNode = currentNode.next;
}
return currentNode.val;
}
//在链表最前面插入一个节点,等价于在第0个元素前添加
public void addAtHead(int val) {
addAtIndex(0, val);
}
//在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加
public void addAtTail(int val) {
addAtIndex(size, val);
}
// 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果 index 大于链表的长度,则返回空
public void addAtIndex(int index, int val) {
if (index > size) {
return;
}
if (index < 0) {
index = 0;
}
size++;
//找到要插入节点的前驱
ListNode pred = head;
for (int i = 0; i < index; i++) {
pred = pred.next;
}
ListNode toAdd = new ListNode(val);
toAdd.next = pred.next;
pred.next = toAdd;
}
//删除第index个节点
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) {
return;
}
size--;
if (index == 0) {
head = head.next;
return;
}
ListNode pred = head;
for (int i = 0; i < index ; i++) {
pred = pred.next;
}
pred.next = pred.next.next;
}
}
//双链表
class ListNode{
int val;
ListNode next,prev;
ListNode() {};
ListNode(int val){
this.val = val;
}
}
class MyLinkedList {
//记录链表中元素的数量
int size;
//记录链表的虚拟头结点和尾结点
ListNode head,tail;
public MyLinkedList() {
//初始化操作
this.size = 0;
this.head = new ListNode(0);
this.tail = new ListNode(0);
//这一步非常关键,否则在加入头结点的操作中会出现null.next的错误!!!
head.next=tail;
tail.prev=head;
}
public int get(int index) {
//判断index是否有效
if(index<0 || index>=size){
return -1;
}
ListNode cur = this.head;
//判断是哪一边遍历时间更短
if(index >= size / 2){
//tail开始
cur = tail;
for(int i=0; i< size-index; i++){
cur = cur.prev;
}
}else{
for(int i=0; i<= index; i++){
cur = cur.next;
}
}
return cur.val;
}
public void addAtHead(int val) {
//等价于在第0个元素前添加
addAtIndex(0,val);
}
public void addAtTail(int val) {
//等价于在最后一个元素(null)前添加
addAtIndex(size,val);
}
public void addAtIndex(int index, int val) {
//index大于链表长度
if(index>size){
return;
}
//index小于0
if(index<0){
index = 0;
}
size++;
//找到前驱
ListNode pre = this.head;
for(int i=0; i<index; i++){
pre = pre.next;
}
//新建结点
ListNode newNode = new ListNode(val);
newNode.next = pre.next;
pre.next.prev = newNode;
newNode.prev = pre;
pre.next = newNode;
}
public void deleteAtIndex(int index) {
//判断索引是否有效
if(index<0 || index>=size){
return;
}
//删除操作
size--;
ListNode pre = this.head;
for(int i=0; i<index; i++){
pre = pre.next;
}
pre.next.next.prev = pre;
pre.next = pre.next.next;
}
}
206.反转链表
输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL
如果再定义一个新的链表,实现链表元素的反转,会对内存空间造成浪费。
其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表。
动画所示:(纠正:动画应该是先移动pre,在移动cur)
双指针法
首先定义一个cur指针,指向头结点,再定义一个pre指针初始化为null。
然后开始反转,把 cur->next 节点用temp指针保存一下(保存该节点呢,因为要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点)。
接下来,循环遍历,继续移动pre和cur指针。
最后,cur 指针已经指向了null,循环结束,链表反转完毕。 return pre指针,pre指针就指向了新的头结点。
- 时间复杂度: O(n)
- 空间复杂度: O(1)
struct ListNode* reverseList(struct ListNode* head){
//保存cur的下一个结点
struct ListNode* temp;
//pre指针指向前一个当前结点的前一个结点
struct ListNode* pre = NULL;
//用head代替cur,也可以再定义一个cur结点指向head。
while(head) {
//保存下一个结点的位置
temp = head->next;
//翻转操作
head->next = pre;
//更新结点
pre = head;
head = temp;
}
return pre;
}
java写法
// 双指针
class Solution {
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode cur = head;
ListNode temp = null;
while (cur != null) {
temp = cur.next;// 保存下一个节点
cur.next = prev;
prev = cur;
cur = temp;
}
return prev;
}
}
递归法
将链表的反转拆分成两部分(反转前一个节点和当前节点的指针,以及反转当前节点和下一个节点的指针),通过递归遍历链表,在遍历到当前节点时完成反转操作,并不断返回反转后的链表头结点(在递归过程中,链表的头结点可能会发生变化),最终得到完整的反转链表。
- 时间复杂度: O(n), 要递归处理链表的每个节点
- 空间复杂度: O(n), 递归调用了 n 层栈空间
struct ListNode* reverse(struct ListNode* pre, struct ListNode* cur) {
if(!cur)
return pre;
struct ListNode* temp = cur->next; // 创建一个临时指针temp,保存当前节点cur的下一个节点
cur->next = pre; // 将当前节点cur的next指针指向pre,实现反转操作
// 将cur作为pre传入下一层递归函数调用
// 将temp作为cur传入下一层递归函数调用,并改变temp的next指针指向当前cur,将temp连接到反转后的链表上
return reverse(cur, temp); // 递归调用reverse函数
}
struct ListNode* reverseList(struct ListNode* head){
return reverse(NULL, head); // 调用辅助函数reverse来完成整个链表的反转,初始时将pre设置为NULL,即空链表,将head作为第二个节点传入reverse函数
}
java写法
// 递归
class Solution {
public ListNode reverseList(ListNode head) {
return reverse(null, head);
}
private ListNode reverse(ListNode prev, ListNode cur) {
if (cur == null) {
return prev;
}
ListNode temp = null;
temp = cur.next;// 先保存下一个节点
cur.next = prev;// 反转
// 更新prev、cur位置
// prev = cur;
// cur = temp;
return reverse(cur, temp);
}
}
// 从后向前递归
class Solution {
ListNode reverseList(ListNode head) {
// 边缘条件判断
if(head == null) return null;
if (head.next == null) return head;
// 递归调用,翻转第二个节点开始往后的链表
ListNode last = reverseList(head.next);
// 翻转头节点与第二个节点的指向
head.next.next = head;
// 此时的 head 节点为尾节点,next 需要指向 NULL
head.next = null;
return last;
}
}