【代码随想录算法训练营Day03】203.移除链表元素; 707.设计链表; 206.反转链表

Day 3 第二章 链表part01

  • 今日任务
    • 链表理论基础; 203.移除链表元素; 707.设计链表; 206.反转链表

链表理论基础

1. 什么是链表

  • 链表是以节点的方式来存储,是链式存储
  • 每个节点包含data域,next域(指向下一个节点)
  • 链表的各个节点不一定是连续存储
  • 链表分带头节点的链表和不带头节点的列表(根据实际的需求来确定)

链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。

链表的入口节点称为链表的头结点也就是head。

逻辑结构如图所示: 链表1

2. 链表的类型

单链表
  • 单链表中的指针域只能指向节点的下一个节点。
双链表
  • 双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
  • 双链表 既可以向前查询也可以向后查询。

如图所示: 链表2

循环链表
  • 循环链表,顾名思义,就是链表首尾相连。
  • 循环链表可以用来解决约瑟夫环问题。

链表4

3. 链表的存储方式

  • 数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。
  • 链表是通过指针域的指针链接在内存中各个节点。
  • 所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。

链表3

这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起。

4. 链表的定义

这是因为平时在刷leetcode的时候,链表的节点都默认定义好了,直接用就行了,所以同学们都没有注意到链表的节点是如何定义的。

而在面试的时候,一旦要自己手写链表,就写的错漏百出。

import java.util.List;
 
//ListNode代表一个节点
class ListNode{
    public int val;
    public ListNode next;
 
    //构造函数
    public ListNode(int a){
        this.val = a;
    }
}

//创建链表
public class MyLinkedList {
    public ListNode head;//链表的头
	//方法一:枚举法(直接进行val的赋值以及对next的初始化)
	//不用对最后一个节点的next进行赋值,因为next是引用类型,不赋值则默认为null。
    public void creatList() {
        ListNode listNode1 = new ListNode(11);
        ListNode listNode2 = new ListNode(22);
        ListNode listNode3 = new ListNode(33);
        ListNode listNode4 = new ListNode(44);
        ListNode listNode5 = new ListNode(55);
 
        this.head = listNode1;
 
        listNode1.next = listNode2;
        listNode2.next = listNode3;
        listNode3.next = listNode4;
        listNode4.next = listNode5;
    }
 
    //方法二:头插法
    /*头插法是指在链表的头节点的位置插入一个新节点,定义一个node表示该节点
    然后就是对node的next进行赋值,用node.next = this.head即可完成
    (注意:head应指向新节点)*/
    public void addFirst(int data) {
        ListNode node = new ListNode(data);
        node.next = this.head;
        this.head = node;
        /*if(this.head == null){
            this.head = node;
        }else{
            node.next = this.head;
            this.head = node;
        }*/
    }
 
    //方法三:尾插法
    /*尾插法是指在链表的尾节点的位置插入一个新节点,定义一个node表示该节点
    然后就是对原来最后一个节点的next进行赋值
    先将head移动至原来最后一个节点,用head.next = node进行赋值
    (注意:如果链表不为空,需要定义cur来代替head)*/
    public void addLast(int data) {
        ListNode node = new ListNode(data);
        if (this.head == null) {
            this.head = node;
        } else {
            ListNode cur = this.head;
            while (cur.next != null) {
                cur = cur.next;
            }
            cur.next = node;
        }
    }
 
    //打印顺序表
    /*认识了链表的结构,我们可以知道,节点与节点之间通过next产生联系。
    并且我们已将创建了head,即头节点的地址,通过head的移动来实现链表的打印。
    注意:为了使head一直存在且有意义,
    我们在display()函数中定义一个cur:ListNode cur = this.head;来替代head。
    对于head的移动,可用head = head.next来实现*/
    public void display() {
        ListNode cur = this.head;
        while (cur != null) {
            System.out.print(cur.val + " ");
            cur = cur.next;
        }
        System.out.println();
    }
 
    //查找是否包含关键字key是否在单链表当中
    //查找key,可以利用head移动,实现对于key的查找(**注意**:同样要定义一个cur来代替head)
    public boolean contains(int key) {
        ListNode cur = this.head;
        while (cur != null) {
            if (cur.val == key) {
                return true;
            }
            cur = cur.next;
        }
        return false;
    }
 
    //得到单链表的长度
    //定义计数器count = 0,通过head的移动来判断链表长度(**注意**:同样要定义一个cur来代替head)
    public int Size() {
        int count = 0;
        ListNode cur = this.head;
        while (cur != null) {
            count++;
            cur = cur.next;
        }
        return count;
    }
 
    //找到index位置的前一位置的地址
    public ListNode findIndex(int index) {
        ListNode cur = head.next;
        while (index - 1 != 0) {
            cur = cur.next;
            index--;
        }
        return cur;
    }
 
    //任意位置插入,第一个数据节点为0号下标
    public void addIndex(int index, int data) {
        if (index < 0 || index > Size()) {
            return;
        }
        if (index == 0) {          //相当于头插法
            addFirst(data);
            return;
        }
        if (index == Size()) {      //相当于尾插法
            addLast(data);
            return;
        }
        ListNode cur = findIndex(index);//找到index位置前一位置的地址
        ListNode node = new ListNode(data);//初始化node
        node.next = cur.next;
        cur.next = node;
    }
 
    //找到key的前驱(前一节点)
    public ListNode searchPrev(int key) {
        ListNode cur = this.head;
        while (cur.next != null) {
            if (cur.next.val == key) {
                return cur;
            }
            cur = cur.next;
        }
        return null;
    }
 
    //删除第一次出现关键字为key的节点
    public void remove(int key) {
        if (this.head == null) {
            return;
        }
        if (this.head.val == key) {
            this.head = this.head.next;
            return;
        }
        ListNode cur = searchPrev(key);
        if (cur == null) {
            return;             //没有要删除的节点
        }
        ListNode del = cur.next;//定义要删除的节点
        cur.next = del.next;
    }
 
    //清空链表
    public void clear() {
        while (this.head != null) {
            ListNode curNext = this.head.next;
            this.head.next = null;
            this.head = curNext;
        }
    }
}

5. 链表的操作

删除节点

删除D节点,如图所示:

链表-删除节点

只要将C节点的next指针 指向E节点就可以了。
那有同学说了,D节点不是依然存留在内存里么?只不过是没有在这个链表里而已。
是这样的,所以在C++里最好是再手动释放这个D节点,释放这块内存。
其他语言例如Java、Python,就有自己的内存回收机制,就不用自己手动释放了。

添加节点

如图所示:

链表-添加节点

可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。

但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。

6. 链表 & 数组

再把链表的特性和数组的特性进行一个对比,如图所示:

链表-链表与数据性能对比

数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。

链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。

203.移除链表元素

1. 直接使用原来的链表来进行删除操作。

- 需要判断删除的节点是否是头节点
	- 是头节点把头节点的指针向后移一位就可
	- 不是头节点只需要把前一个节点的指针指向下一个节点
public ListNode removeElements1(int val) {  
	while(head != null && head.val == val){     //首先判断链表不为空,要删除的是不是头节点  
		head = head.next;   //如果要删除的是头节点则指向下一个  
	}  
	ListNode temp = head;  
	while(temp != null && temp.next != null){   //操作判断是否要删除的不是temp是temp.next,所以要保证两个都不为空  
		if(temp.next.val == val){   //要持续判断temp后面的需不需要删除,所以不能一删完就将temp后移  
			temp.next = temp.next.next;  
		}else{  
			temp = temp.next;       //直到temp.next不等于val再后移  
		}  
	}  
	return head;  
}

2. 设置一个虚拟头结点再进行删除操作。

- 好处:代码比较统一
public ListNode removeElements2(int val) {  
	ListNode dummyHead = new ListNode();  
	dummyHead.next = head;  
	ListNode temp = dummyHead;  
	while(temp != null && temp.next != null){   //操作判断是否要删除的不是temp是temp.next,所以要保证两个都不为空  
		if(temp.next.val == val){   //要持续判断temp后面的需不需要删除,所以不能一删完就将temp后移  
			temp.next = temp.next.next;  
		}else{  
			temp = temp.next;       //直到temp.next不等于val再后移  
		}  
	}  
	return dummyHead.next;	//注意返回的不是head,head可能已经被删了
}

3. 通过输入创建链表

思路:先创建一个数据再一个一个添加

707.设计链表

先设计链表:选择单链表/双链表;确定链表固定属性(eg:长度,头尾结点)

//单链表  
class SingleLinkedList707 {  
	int size;   //表示链表元素的个数  
    Node head;  //虚拟头节点  
    public SingleLinkedList707() {  
        size = 0;  
        head = new Node();  
    }  
  
    //获取第index个节点的数值,注意index是从0开始的,第0个节点就是头结点  	
    public int get(int index) {  
        //注意 index = size 也越界  
	    if(index < 0 || index >= size){  
			return -1;  
        }  
        Node curr = head;  
        //包含一个虚拟头节点,所以查找index+1个节点  
		for(int i = 0; i <= index; i++){  
            curr = curr.next;  
        }  
        return curr.val;  
    }  
  
    public void addAtHead(int val) {  
        addAtIndex(0,val);  
    }  
  
    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++;  
  
        //找到要插入节点的前驱  
	    Node temp = head;  
        for(int i = 0; i < index; i++){  
            temp = temp.next;  
        }  
  
        //插入节点  
	    Node toAdd = new Node(val);  
	        toAdd.next = temp.next;  
	        temp.next = toAdd;  
	}  
  
    //删除第index个节点  
    public void deleteAtIndex(int index) {  
        if(index < 0 || index > size){  
            return;  
        }  
        if(index == 0){  
            head = head.next;  
        }  
        Node temp = head;  
        for(int i = 0; i < index; i++){  
            temp = temp.next;  
        }  
        temp.next = temp.next.next;  
    }  
}  
  
class Node{  
    int val;  
    Node next, prev;  
    Node() {}  
    Node(int val) { this.val = val; }  
    Node(int val, Node next){  
        this.val = val;  
        this.next = next;  
    }  
    public String toString(){  
        return "ListNode:val="+val;  
    }  
}

206.反转链表

pFnNOjP.png

1. 双指针法

public static ListNode reverseList(ListNode head) {  
    ListNode cur, pre, temp;  
    cur = head;  
    pre = null;  
    while(cur != null){  
        temp = cur.next;  
        cur.next = pre;  
        pre = cur;  
        cur = temp;  
    }  
    return pre;  
}

2. 递归法

public  ListNode  reverseList(ListNode  head) {
	return  reverse(head, null);
}
public  ListNode  reverse(ListNode  cur, ListNode  pre){
	ListNode  temp;
	if(cur == null) return pre;
	temp = cur.next;
	cur.next = pre;
	return  reverse(temp, cur);
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值