这两天又被链表困难到了,虽然之前了解过,但是自己书写起来才发现各种细节没有注意到。依旧是参照leetcode(https://leetcode-cn.com/problems/design-linked-list/)和《代码随想录》(https://programmercarl.com/0707.%E8%AE%BE%E8%AE%A1%E9%93%BE%E8%A1%A8.html#%E5%85%B6%E4%BB%96%E8%AF%AD%E8%A8%80%E7%89%88%E6%9C%AC),这次主要针对于代码书写中定链表位置时可能出现的pre.next.next这种链接了很多next的情况进行了汇总和修改,按照题目要求,书写了单链表和双链表的代码。其中,用pre, cur, nex分别指代所定位节点(即index指定的那个节点)的前一个、当前节点、后一个节点;用prev, next 代表一个节点的前后指向,其余具体思路见注释。
题目:
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。
在链表类中实现这些功能:
get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
代码:
(单链表)
//定义节点 class Node { public int val; public Node next; public Node(int val, Node next) { this.next = next; this.val = val; } } //构造链表 class MyLinkedList { private int size; //虚拟头结点 private Node head; //初始化链表信息 public MyLinkedList() { size = 0; head = new Node(0,null); } //获取第index个节点的值 public int get(int index) { if(index < 0 || index >= size) { return -1; } //由于插入值是在pre-cur之间并且设有虚拟节点head,从代码清晰度和逻辑性上讲,找到index对应的pre更方便,以下代码统一用这样的思路和格式。 Node pre = head; for(int i = 0; i < index; i++) { pre = pre.next; } Node cur = pre.next; return cur.val; } //头和尾直接按照index的不同来插入 public void addAtHead(int val) { addAtIndex(0, val); } public void addAtTail(int val) { addAtIndex(size, val); } public void addAtIndex(int index, int val) { if(index > size) { return ; } if(index < 0) { index = 0; } Node pre = head; for(int i = 0; i < index; i++) { pre = pre.next; } //找到正确的pre和cur节点就很清晰 pre.next = newNode, newNode.next = cur Node cur = pre.next; Node newNode = new Node(val, cur); pre.next = newNode; size++; } public void deleteAtIndex(int index) { if(index < 0 || index >= size) { return ; } Node pre = head; for(int i = 0; i < index; i++) { pre = pre.next; } //Java语言有内存回收机制,删除节点就直接pre.next = cur.next让pre.next跳过cur指向cur.next Node cur = pre.next; pre.next = cur.next; size--; } }
结果:
(双链表)
class Node { public int val; //双链表的节点主要变成了前和后的双向指向 public Node next, prev; public Node(int val, Node prev, Node next) { this.val = val; this.prev = prev; this.next = next; } } class MyLinkedList { public int size; //增设了尾部节点 public Node head; public Node tail; public MyLinkedList() { size = 0; head = new Node(0, null, null); tail = new Node(0, null, null); head.next = tail; tail.prev = head; } public int get(int index) { if(index < 0 || index >= size) { return -1; } //选择从头遍历和从尾遍历 //i<多少是需要盘逻辑的,前面单链表中对于从头遍历已经比较明了,从尾遍历带入特殊情况来确定也很明了 if(index <= (size + 1) / 2) { Node pre = head; for(int i = 0; i < index; i++) { pre = pre.next; } Node cur = pre.next; return cur.val; } else { //这里丛尾开始遍历就是用nex了 Node nex = tail; for(int i = 0; i < (size - index - 1); i++) { nex = nex.prev; } Node cur = nex.prev; return cur.val; } } public void addAtHead(int val) { Node pre = head; Node cur = pre.next; Node newNode = new Node(val, pre, cur); pre.next = newNode; cur.prev = newNode; size++; } public void addAtTail(int val) { Node nex = tail; Node cur = nex.prev; Node newNode = new Node(val, cur, nex); cur.next = newNode; nex.prev = newNode; size++; } public void addAtIndex(int index, int val) { if(index > size) { return ; } if(index < 0) { index = 0; } Node pre = head; for(int i = 0; i < index; i++) { pre = pre.next; } //双链表在此处的代码比单链表更加清晰 Node cur = pre.next; //newNode的定义语句直接清晰地显示出位置关系 Node newNode = new Node(val, pre, cur); pre.next = newNode; cur.prev = newNode; size++; } public void deleteAtIndex(int index) { if(index < 0 || index >= size) { return ; } //删除语句的查询方式和get函数相差无多 if(index <= (size + 1) / 2) { Node pre = head; for(int i = 0; i < index; i++) { pre = pre.next; } //就是删节点时候有点麻烦,但很清晰的是,得到pre-cur-nex三个节点,将pre.next和nex.prev重新赋值就行了。 Node cur = pre.next; Node nex = cur.next; nex.prev = pre; pre.next = nex; } else { Node nex = tail; for(int i = 0; i < (size - index - 1); i++) { nex = nex.prev; } Node cur = nex.prev; Node pre = cur.prev; pre.next = nex; nex.prev = pre; } size--; } }
结果:
分析:
可以看出双链表的执行时间更短,主要是因为双向的特性,此处的双链表指定位置赋值还没有充分利用双向的特性,速度未能达到顶尖。