什么是链表,链表是一种通过指针串联在一起的线性结构,
每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链表的入口节点称为链表的头结点也就是head。
链表可以分为:单链表、双链表、循环链表
链表节点的定义,很多同学在面试的时候都写不好。
这是因为平时在刷leetcode的时候,链表的节点都默认定义好了,直接用就行了,所以同学们都没有注意到链表的节点是如何定义的。
这里我给出C/C++的定义链表节点方式,如下所示:
// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
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;
}
}
性能分析:
再把链表的特性和数组的特性进行一个对比,如图所示:
题意:删除链表中等于给定值 val 的所有节点。
示例 1: 输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5]
示例 2: 输入:head = [], val = 1 输出:[]
示例 3: 输入:head = [7,7,7,7], val = 7 输出:[]
思路:这里就涉及如下链表操作的两种方式:
第一种操作:直接使用原来的链表来进行移除。
移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。
所以头结点如何移除呢,其实只要将头结点向后移动一位就可以,这样就从链表中移除了一个头结点。
依然别忘将原头结点从内存中删掉。(针对C++而言,当然如果使用java ,python的话就不用手动管理内存了。)
第二种操作:设置虚拟头节点dummy node
其实可以设置一个虚拟头结点,这样原链表的所有节点就都可以按照统一的方式进行移除了。
(虚拟头结点主要还是为了统一解决对于头结点没有前一个节点,不易增删处理的问题。)
最后呢在题目中,return 头结点的时候,别忘了 return dummyNode->next;
, 这才是新的头结点
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
/**
* 不添加虚拟节点and pre Node方式
* 时间复杂度 O(n)
* 空间复杂度 O(1)
* @param head
* @param val
* @return
*/
class Solution {
public ListNode removeElements(ListNode head, int val) {
//第一种情况:头结点的数据为val,需要删除
while(head!=null && head.val==val){//用while是防止不止一个
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;
}
}
/**第二种方式
* 不添加虚拟节点方式
* 时间复杂度 O(n)
* 空间复杂度 O(1)
* @param head
* @param val
* @return
*/
class Solution {
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;
}
}
class Solution {
public ListNode removeElements(ListNode head, int val) {
//第三种方式:添加虚拟节点 dummy node,并且使用pre和cur
if(head == null){
return head;
}
//因为删除可能涉及到头结点,设置dummy node可以方便统一操作
ListNode dummy = new ListNode(-1,head);//连接两个链表,且当前链表头值为-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;//cur继续指向下一个节点
}
return dummy.next;
}
}
你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性:val
和 next
。val
是当前节点的值,next
是指向下一个节点的指针/引用。
如果是双向链表,则还需要属性 prev
以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。
实现 MyLinkedList
类:
MyLinkedList()
初始化 MyLinkedList
对象。int get(int index)
获取链表中下标为 index
的节点的值。如果下标无效,则返回 -1
。void addAtHead(int val)
将一个值为 val
的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。void addAtTail(int val)
将一个值为 val
的节点追加到链表中作为链表的最后一个元素。void addAtIndex(int index, int val)
将一个值为 val
的节点插入到链表中下标为 index
的节点之前。如果 index
等于链表的长度,那么该节点会被追加到链表的末尾。如果 index
比长度更大,该节点将 不会插入 到链表中。void deleteAtIndex(int index)
如果下标有效,则删除链表中下标为 index
的节点。这道题目设计链表的五个接口:
可以说这五个接口,已经覆盖了链表的常见操作,是练习链表操作非常好的一道题目
思路:
插入新节点时,一定要先执行①步,再执行②步,防止先执行②后,cur和cur->next的连接断了,没发找到 cur->next了。
下面采用的设置一个虚拟头结点(这样更方便一些,大家看代码就会感受出来)。
第一种:单链表方式 实现
//单链表
class ListNode {
int val; //定义节点值
ListNode next; //定义下一个节点
ListNode(){} //节点的无参构造
ListNode(int val) { //节点的有参构造
this.val=val;}
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
class MyLinkedList {
int size; //size存储链表元素的个数
ListNode dummyHead; //这里定义的头结点 是一个虚拟头结点,而不是真正的链表头结点
//初始化链表
public MyLinkedList() {
size = 0;
dummyHead = new ListNode(0); //定义虚拟头节点,值为0
//上面等效于: ListNode dummyHead = new ListNode(0);
}
//获取第index个节点的数值,注意index是从0开始的,第0个节点就是 真正的head头结点
// 获取下标为第index个节点,需要找第index+1个节点
public int get(int index) {
//如果index非法,则返回-1
if(index < 0 || index >= size){
return -1;
}
ListNode cur = dummyHead;
//由于我们设置的 包含一个虚拟头节点,所以查找第 index+1 个节点(整体来看)
for(int i = 0;i <= index;i++){ //i<=index,所以可以取到第index+1个节点
cur = cur.next;
//如果index=0;那么cur此时正好指向真正的头结点,返回val,也没错,这么写就按照统一的方式进行操作了
}
return cur.val;//返回原链表下标为index的值
}
//在链表最前面插入一个节点,等价于在第0个元素前添加
public void addAtHead(int val) {
addAtIndex(0, val);
}
//在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加
public void addAtTail(int val) {
addAtIndex(size, val); //因为最后一个元素节点下标是size-1
}
public void addAtIndex(int index, int val) {
// 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果 index 大于链表的长度,则返回空
// 如果index小于0,则在头部插入节点
if(index > size){
return;
}
if(index < 0){
index = 0;
}
size ++;
//找到要插入节点的前驱
ListNode cur = dummyHead; //cur指向虚拟头节点,开始遍历
for(int i = 0;i < index;i++){
//inext
//i=index-1时,循环结束,此时cur指向(原链表)第index-1个节点
cur = cur.next;
}
ListNode newNode = new ListNode(val); //index=0时,跳过for直接到这步
newNode.next = cur.next;//图示的第①步
cur.next = newNode;//图示的第②步
}
//删除第index个节点
public void deleteAtIndex(int index) {
if(index < 0 || index >= size){
return ;
}
size--;
if(index == 0){
dummyHead = dummyHead.next;//虚拟头结点直接 变为原头结点(升官了)
return; //此处注意要 返回头节点也是返回:dummyHead.next
}
//到此则index合法且不为0
ListNode cur = dummyHead;
for(int i = 0;i < index;i++){
//条件inext
cur = cur.next;
}
cur.next = cur.next.next;
}
}
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList obj = new MyLinkedList();
* int param_1 = obj.get(index);
* obj.addAtHead(val);
* obj.addAtTail(val);
* obj.addAtIndex(index,val);
* obj.deleteAtIndex(index);
*/
第一种:双链表方式 实现
//双链表
class ListNode{
int val; // 该节点的值
ListNode next,prev;//属性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; //这里用了this关键字,表示当前节点的头节点
//判断是哪一边遍历时间更短
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; //插入节点的前驱节点pre
for(int i=0; i=size){
return;
}
//删除操作
size--;
ListNode pre = this.head;
for(int i=0; i
题意:反转一个单链表。
示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL
思路 :如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。
其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,
如图所示:
之前链表的头节点是元素1, 反转之后头结点就是元素5 ,这里并没有添加或者删除节点,仅仅是改变next指针的方向。
首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。
然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。
为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。
接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。
最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode cur = head;
ListNode temp = null;//把 cur->next 节点用tmp指针保存一下
while(cur != null){
temp = cur.next;//保存下一个节点
cur.next = prev;
prev = cur;
cur = temp;
}
return prev; //新链表的头结点
}
}
递归法相对抽象一些,但是其实和双指针法是一样的逻辑,同样是当cur为空的时候循环结束,不断将cur指向pre的过程。
// 递归法
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;
//reverse(cur, temp); 也就是cur赋给prev,把temp赋给cur,完成自身的递归调用
//这样算进行了一次循环,cur和prev均向右移动一位,切cur的指针调转指向pre
return reverse(cur, temp);
}
}