基于节点的数据结构—链表
数组存储:连续的内存空间,并且预留一部分空间方便扩展。这样会大大降低内存的使用率。
如上图降低电影院入座率一样。
链表
功能与数组类似——用于存储一系列相同类型的数组。只是存储方式不同。
节点
链表中每个元素我们称之为节点。
节点由两部分组成——节点内容和下一个节点地址
与数组的区别就是多了下一个节点的地址。
为什么需要下一个节点地址?
- 数组中由于连续内存空间存储,可通过start_adress+item_size*i来获取元素位置。
- 链表中是分散储存的,所以要保存下一个节点位置。
好处:数据分散存储在内存中,达到内存最大利用率。
节点类
public class Node {private int content;private Node next;public Node() {}public Node(int content, Node next) {this.content = content;this.next = next;}public int getContent() {return content;}public void setContent(int content) {this.content = content;}public Node getNext() {return next;}public void setNext(Node next) {this.next = next;}}
代码实现链表:
public class Demo {// 创建链表public static Node createLinkedList(int[] array) {if (array.length==1){Node root = null;return new Node(array[0],root);}int[] newarray = new int[array.length-1];for (int i=1;i<array.length ;i++ ){newarray[i-1] = array[i] ;}return new Node(array[0],createLinkedList(newarray));}public static void main(String[] args) {int[] array = {9, 2, 4};Node root = createLinkedList(array);System.out.println(root.getContent());}}
答案:
public class Demo {// 创建链表public static Node createLinkedList(int[] array) {// 创建一个根节点Node root = null;// 从末尾元素开始依次创建Node节点for (int i = array.length - 1; i >= 0; i--) {Node node = new Node(array[i], null);// 创建两个节点的连接关系if (root != null) {node.setNext(root);root = node;} else {root = node;}}return root;}public static void main(String[] args) {int[] array = {9, 2, 4};Node root = createLinkedList(array);System.out.println(root.getContent());}}
链表的读取与查找
读取
时间复杂度
读取第一个O(1),最坏读取最后一个需要N步,所以为O(N)。
查找
模拟实现JAVA中的LinkedList(底层是用链表实现的)
public class YKDLinkedList {private Node root = null;int size = 0;public YKDLinkedList() {}// 数组创建一个链表public static Node createLinkedLNode(int[] array) {// 创建一个根节点Node root = null;// 从末尾元素开始依次创建Node节点for (int i = array.length - 1; i >= 0; i--) {Node node = new Node(array[i], null);// 创建两个节点的连接关系if (root != null) {node.setNext(root);root = node;} else {root = node;}}return root;}public YKDLinkedList(int[] array) {this.root = YKDLinkedList.createLinkedLNode(array);this.size = array.length;}// 获取长度public int size() {return this.size;}// 获取某个索引节点内容public int get(int index) {int i = 0;Node node = this.root;// 依次往前推进while (i < index) {node = node.getNext();i++;}return node.getContent();}// 查找某个值的索引值,默认不存在为-1public int find(int value) {Node node = this.root;// index 保存当前的索引int index = 0;// 依次遍历链表,找到内容等于value的node,返回indexwhile (node != null && node.getNext() != null) {if (node.getContent() == value) {return index;}node = node.getNext();index++;}return -1;}public static void main(String[] args) {int[] array = {9, 2, 4};YKDLinkedList linkedList = new YKDLinkedList(array);System.out.println(linkedList.find(2));System.out.println(linkedList.find(5));System.out.println(linkedList.get(2));}}
链表的插入
头部插入
中间插入
尾部插入
代码实现
public class YKDLinkedList {private Node root = null;int size = 0;public YKDLinkedList() {}// 数组创建一个链表public static Node createLinkedLNode(int[] array) {// 创建一个根节点Node root = null;// 从末尾元素开始依次创建Node节点for (int i = array.length - 1; i >= 0; i--) {Node node = new Node(array[i], null);// 创建两个节点的连接关系if (root != null) {node.setNext(root);root = node;} else {root = node;}}return root;}public YKDLinkedList(int[] array) {this.root = YKDLinkedList.createLinkedLNode(array);this.size = array.length;}// 获取长度public int size() {return this.size;}// 获取某个索引节点内容public int get(int index) {int i = 0;Node node = this.root;// 依次往前推进while (i < index) {node = node.getNext();i++;}return node.getContent();}// 查找某个值的索引值,默认不存在为-1public int find(int value) {Node node = this.root;// index 保存当前的索引int index = 0;// 依次遍历链表,找到内容等于value的node,返回indexwhile (node != null && node.getNext() != null) {if (node.getContent() == value) {return index;}node = node.getNext();index++;}return -1;}// 末尾添加元素public boolean add(int value) {return this.add(this.size - 1, value);}// 头部插入元素public boolean addFirst(int value) {return this.add(-1, value);}// 插入元素public boolean add(int index, int value) {if (index < -1 || index > this.size - 1){return false;}if (index == -1) {if (this.root != null) {this.root = new Node(value, this.root);} else {this.root = new Node(value, null);}} else {Node pre = this.root;while (index > 0) {if (pre.getNext() == null) {return false;}pre = this.root.getNext();index--;}if (pre == null) {return false;}Node newNode = new Node(value, pre.getNext());pre.setNext(newNode);}this.size++;return true;}// 链表中的元素字符串形式public String toString() {if (this.root == null) {return "";}StringBuilder str = new StringBuilder();Node node = this.root;while (node != null) {str.append(node.getContent()).append(" ");node = node.getNext();}return str.toString();}public static void main(String[] args) {YKDLinkedList linkedList = new YKDLinkedList();boolean result = linkedList.add(9);if (!result) {System.out.println("9插入失败");return;}result = linkedList.add(4);if (!result) {System.out.println("4插入失败");return;}result = linkedList.add(0, 2);if (!result) {System.out.println("2插入失败");return;}result = linkedList.addFirst(10);if (!result) {System.out.println("10插入失败");return;}System.out.println(linkedList.toString());}}
链表删除
头部删除
中间删除
尾部删除
代码实现
public class YKDLinkedList {private Node root = null;int size = 0;public YKDLinkedList() {}// 数组创建一个链表public static Node createLinkedLNode(int[] array) {// 创建一个根节点Node root = null;// 从末尾元素开始依次创建Node节点for (int i = array.length - 1; i >= 0; i--) {Node node = new Node(array[i], null);// 创建两个节点的连接关系if (root != null) {node.setNext(root);root = node;} else {root = node;}}return root;}public YKDLinkedList(int[] array) {this.root = YKDLinkedList.createLinkedLNode(array);this.size = array.length;}// 获取长度public int size() {return this.size;}// 获取某个索引节点内容public int get(int index) {int i = 0;Node node = this.root;// 依次往前推进while (i < index) {node = node.getNext();i++;}return node.getContent();}// 查找某个值的索引值,默认不存在为-1public int find(int value) {Node node = this.root;// index 保存当前的索引int index = 0;// 依次遍历链表,找到内容等于value的node,返回indexwhile (node != null && node.getNext() != null) {if (node.getContent() == value) {return index;}node = node.getNext();index++;}return -1;}// 末尾添加元素public boolean add(int value) {return this.add(this.size - 1, value);}// 头部插入元素public boolean addFirst(int value) {return this.add(-1, value);}// 插入元素public boolean add(int index, int value) {if (index < -1 || index > this.size - 1){return false;}if (index == -1) {if (this.root != null) {this.root = new Node(value, this.root);} else {this.root = new Node(value, null);}} else {Node pre = this.root;while (index > 0) {if (pre.getNext() == null) {return false;}pre = this.root.getNext();index--;}if (pre == null) {return false;}Node newNode = new Node(value, pre.getNext());pre.setNext(newNode);}this.size++;return true;}// 删除最后一个元素public boolean removeLast() {return this.remove(this.size - 1);}// 删除第一个元素public boolean removeFirst() {return this.remove(0);}// 插入元素// index = 0, 表示删除第 1 个元素// index = n,表示删除第 n+1 个元素public boolean remove(int index) {if (index < 0 || index > this.size - 1){return false;}// 判断整个列表为空情况,删除失败if (this.root == null) {return false;}if (index == 0) {// 删除第一个元素,情况比较简单,注意必须先保留 this.root,因为紧接着 this.root 会被修改Node node = this.root;this.root = node.getNext();node.setNext(null);} else {// 判断越界问题if(this.size < index + 1){return false;}// 遍历获取index的前置节点Node pre = this.root;while (index > 1) {pre = pre.getNext();index--;}// 修改next指针,同样大家需要先保存 pre.next,因为紧接着下一步会修改 pre.next 指针Node next = pre.getNext();pre.setNext(next.getNext());next.setNext(null);}this.size--;return true;}// 链表中的元素字符串形式public String toString() {if (this.root == null) {return "";}StringBuilder str = new StringBuilder();Node node = this.root;while (node != null) {str.append(node.getContent()).append(" ");node = node.getNext();}return str.toString();}public static void main(String[] args) {int[] array = {10, 9, 2, 4};YKDLinkedList linkedList = new YKDLinkedList(array);boolean result = linkedList.removeFirst();if (!result) {System.out.println("removeFirst失败");return;}result = linkedList.removeLast();if (!result) {System.out.println("removeLast失败");return;}result = linkedList.remove(1);if (!result) {System.out.println("remove(1)失败");return;}System.out.println(linkedList.toString());}}
链表VS数组
复杂场景
我们手里有一堆手机号码,我们希望写一个算法删除无效的手机号码。
具体逻辑:我们每次读取一个手机号码,判断这个手机号码是否是11位,并且满足正则表达式的要求。
遍历
无论是数组还是链表都要进行N次遍历
删除
数组:每次删除都需要移动后面的元素,所以每次删除的时间复杂度为O(N)。
链表:则是O(1)。
总结
链表经典算法题—多指针使用
如何查找单向链表中倒数第K个节点。
假设链表中:9,2,4,5,6,8,0,3,10
当k为3和6时,结果为0和5。
单向链表——导致节点只知道自己后面的节点,那该如何获取倒数第K个节点呢。
暴力法
- 第一次循环,获取链表所有节点个数,设为N
- 第二次循环,只需要推进N-k次即可。
代码如下:
// 找寻倒数第 k 个节点内容public static int findK(Node node, int k) {// 第一步,计算链表的长度int size = 0;Node p = node;while (p != null) {p = p.getNext();size++;}// 第二步,推进 size - k 步Node c = node;int index = size - k;while (index > 0) {c = c.getNext();index--;}return c.getContent();}
双指针法
- 先申明一个先驱指针。为了探索链表的边界。我们将先驱指针移动k-1位。移动后我们开始申明一个新的指针实际指针,用来寻找倒数第k个节点。
- 同时移动两个指针
- 结束条件:先驱指针执行到链表结尾位置,这是,实际指针刚好指向倒数第K个位置。
代码实现
```java
public class Demo {
// 找寻倒数第 k 个节点内容 public static int findK(Node node, int k) { Node preNode = node; for (int i=0;i<k-1 ;i++ ){ preNode = preNode.getNext(); } Node nowNode = node; while(preNode.getNext()!=null){ preNode = preNode.getNext(); nowNode = nowNode.getNext(); } return nowNode.getContent(); }
// 数组创建一个链表 public static Node createLinkedLNode(int[] array) { // 创建一个根节点 Node root = null; // 从末尾元素开始依次创建Node节点 for (int i = array.length - 1; i >= 0; i—) { Node node = new Node(array[i], null); // 创建两个节点的连接关系 if (root != null) { node.setNext(root); root = node; } else { root = node; } } return root; }
public static void main(String[] args) { int[] array = {9, 2, 4, 5, 6, 8, 0, 3, 10}; Node root = createLinkedLNode(array); int result = findK(root, 3); System.out.println(“k = 3 result: “ + result); result = findK(root, 6); System.out.println(“k = 6 result: “ + result); } }
<a name="UMq6V"></a>#### 双指针变形——获取中心节点```javapublic class Demo {// 找寻倒数第 k 个节点内容public static int findCenter(Node node) {// 创建先驱指针Node pioneer = node;// 创建实际指针Node current = node;// 每次遍历需要实际指针移动一步,先驱指针移动二步while(pioneer.getNext() != null){current = current.getNext();pioneer = pioneer.getNext();if(pioneer.getNext() != null){pioneer = pioneer.getNext();}}return current.getContent();}// 数组创建一个链表public static Node createLinkedLNode(int[] array) {// 创建一个根节点Node root = null;// 从末尾元素开始依次创建Node节点for (int i = array.length - 1; i >= 0; i--) {Node node = new Node(array[i], null);// 创建两个节点的连接关系if (root != null) {node.setNext(root);root = node;} else {root = node;}}return root;}public static void main(String[] args) {int[] array1 = {9, 2, 4, 5, 6, 8, 0, 3, 10};Node root1 = createLinkedLNode(array1);int result = findCenter(root1);System.out.println("array1 result: " + result);int[] array2 = {9, 2, 4, 5, 6, 7, 8, 0, 3, 10};Node root2 = createLinkedLNode(array2);result = findCenter(root2);System.out.println("array2 result: " + result);}}
有环链表
检查链表是否有环
public class Demo {// 查询链表是否有环,true是有环,false是无环public static boolean hasCircle(Node node) {// 移动先驱指针if (node == null || node.getNext() == null) {return false;}// first 每次前进一步Node first = node;// second 每次前进两步Node second = node.getNext();// 核心逻辑:如果链表中有环,两个指针用不同速度运行,最终second会追上first,也就是 first == second。while (first != null && second != null && first != second) {first = first.getNext();second = second.getNext();if (second != null) {second = second.getNext();}}// 如果相等,并且不为空,则表示有环if (first != null && first == second) {return true;}return false;}// 数组创建一个链表public static Node createLinkedLNode(int[] array) {// 创建一个根节点Node root = null;// 从末尾元素开始依次创建Node节点for (int i = array.length - 1; i >= 0; i--) {Node node = new Node(array[i], null);// 创建两个节点的连接关系if (root != null) {node.setNext(root);root = node;} else {root = node;}}return root;}public static void main(String[] args) {testCaseA();testCaseB();testCaseC();}// 无环public static void testCaseA() {int[] array1 = {9, 2, 4, 5, 6, 8, 0, 3, 10};Node root1 = createLinkedLNode(array1);boolean result = hasCircle(root1);System.out.println("caseA has circle: " + result);}// 有环: 案例1public static void testCaseB() {Node node0 = new Node();node0.setContent(10);Node node1 = new Node(3, node0);Node node2 = new Node(0, node1);Node node3 = new Node(8, node2);Node node4 = new Node(6, node3);Node node5 = new Node(5, node4);Node node6 = new Node(4, node5);Node node7 = new Node(2, node6);Node node8 = new Node(9, node7);node0.setNext(node8);boolean result = hasCircle(node8);System.out.println("caseB has circle: " + result);}// 有环: 案例2public static void testCaseC() {Node node0 = new Node();node0.setContent(4);Node node1 = new Node(10, node0);Node node2 = new Node(3, node1);Node node3 = new Node(0, node2);Node node4 = new Node(8, node3);Node node5 = new Node(6, node4);Node node6 = new Node(5, node5);node0.setNext(node6);Node node7 = new Node(2, node0);Node node8 = new Node(9, node7);node0.setNext(node8);boolean result = hasCircle(node8);System.out.println("caseC has circle: " + result);}}
双向链表
特点
- 对于每个节点,有三部分组成,prev(指向前一个节点),next(指向下一个节点),content(节点内容)
对于双向链表,为了方便遍历,我们需要同时保留第一个和最后一个节点。
LinkedList
阅读LinkedList.java的源码,发现实际上就是利用双向链表实现的。
public class LinkedList<E>extends AbstractSequentialList<E>implements List<E>, Deque<E>, Cloneable, java.io.Serializable{transient int size = 0;/*** Pointer to first node.* Invariant: (first == null && last == null) ||* (first.prev == null && first.item != null)*/transient Node<E> first;/*** Pointer to last node.* Invariant: (first == null && last == null) ||* (last.next == null && last.item != null)*/transient Node<E> last;/*** Constructs an empty list.*/public LinkedList() {}......}
