【数据结构和算法】之链表
一、为什么学习链表数据结构
- 经典的链表应用场景,那就是 LRU 缓存淘汰算法
二、链表的数据结构
1、单向链表
2、单向循环链表
3、双向链表
4、双向循环链表
三、链表的数据特点及和数组对比
1、链表的数据结构的特点
- 不支持像数组一样随机访问,查找数据的时间复杂度为O(n)
- 插入和删除操作,不像数组一样需要搬移数据,时间复杂度为O(1)
- 不需要连续的内存空间
2、数组和链表的对比
数组:需要申请连续的内存空间;当数组空间不足,需要进行扩容;
链表:无需申请,连续的内存空间;不需要考虑扩容问题
四、如何写好链表代码技巧
技巧一:理解指针或引用的含义
- 将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。
技巧二:警惕指针丢失和内存泄漏
- 插入结点时,一定要注意操作的顺序;
- 删除链表结点时,也一定要记得手动释放内存空间
链表:p->b // 向链表中插入X节点(错误写法) p->next = x; // 将p的next指针指向x结点; x->next = p->next; // 将x的结点的next指针指向b结点; // 向链表中插入X节点(正确写法) x->next = p->next; // 将x的结点的next指针指向b结点; p->next = x; // 将p的next指针指向x结点;
技巧三:利用哨兵简化实现难度
- 针对链表的插入、删除操作,需要对插入第一个结点和删除最后一个结点的情况进行特殊处理。
带头链表(哨兵结点是不存储数据的。因为哨兵结点一直存在,所以插入第一个结点和插入其他结点,删除最后一个结点和删除其他结点,都可以统一为相同的代码实现逻辑了。)
技巧四:重点留意边界条件处理
- 如果链表为空时,代码是否能正常工作?
- 如果链表只包含一个结点时,代码是否能正常工作?
- 如果链表只包含两个结点时,代码是否能正常工作?
- 代码逻辑在处理头结点和尾结点的时候,是否能正常工作?
技巧五:举例画图,辅助思考
技巧六:多写多练,没有捷径
五、链表的常见操作算法
1、单链表反转
public class LinkedTest { public static void main(String[] args) { //step1:组装链表 Node node1=new Node(); node1.setData("A"); Node node2=new Node(); node2.setData("B"); Node node3=new Node(); node3.setData("C"); node1.setNext(node2); node2.setNext(node3); //step2:执行链表反转 Node newLinked=resetLinked(node1); //step3:测试链表反转结果 for(Node node=newLinked;node!=null;node=node.getNext()){ System.out.println(node.getData()); } } /** * 单链表反转 * * @param node * @return */ public static Node resetLinked(Node node) { if (node == null || node.getNext() == null) { //如果链表为空,则返回null return node; } Node nextNode = null; Node currentNode = node; while (currentNode != null) { Node tmp = currentNode.getNext(); currentNode.setNext(nextNode); nextNode = currentNode; currentNode = tmp; } return nextNode; } } class Node { private String data; private Node next; public String getData() { return data; } public void setData(String data) { this.data = data; } public Node getNext() { return next; } public void setNext(Node next) { this.next = next; } }
2、链表中环的检测
public class LinkedTest { public static void main(String[] args) { //step1:组装链表 Node node1 = new Node(); node1.setData("A"); Node node2 = new Node(); node2.setData("B"); Node node3 = new Node(); node3.setData("C"); node1.setNext(node2); node2.setNext(node3); node3.setNext(node1); System.out.println(isLoopLinked(node1)); System.out.println(isLoopLinked2(node1)); } /** * 方法1:通过链表追击,判断链表是否有环 * * A->B->C->D->E->F * * 第一次循环: * nextNode=B * next=C * * 第二次循环: * first=C * next=E * * 第三次循环: * first=D * next=A * * 第四次循环 * first=E * next=C * * 第五次循环 * first=F * next=E * * 第六次循环 * first=A * next=A * * @param node * @return */ public static boolean isLoopLinked(Node node) { if (node == null || node.getNext() == null) { return false; } Node nextNode = getNextNode(node); Node nextNextNode = getNextNextNode(node); while (nextNode != null && nextNextNode != null) { if (nextNode == nextNextNode) { return true; } nextNode = getNextNode(nextNode); nextNextNode = getNextNextNode(nextNextNode); } return false; } /** * 通过HashMap破解链表是否有环 * * 前提是链表节点没有重写hashcode和 equals * * @param node * @return */ public static boolean isLoopLinked2(Node node) { if (node == null || node.getNext() == null) { return false; } Map<Node, Node> registryMap = new HashMap<>(); for (Node node1 = node; node1.getNext() != null; node1 = node1.getNext()) { Node tmp = registryMap.get(node1); if (tmp != null) { return true; } registryMap.put(node1,node1); } return false; } /** * 获取下一个节点 * * @param node * @return */ public static Node getNextNode(Node node) { return node != null ? node.getNext() : null; } /** * 获取下下一个节点 * * @param node * @return */ public static Node getNextNextNode(Node node) { Node nextNode = getNextNode(node); return nextNode != null ? getNextNode(nextNode) : null; } } class Node { private String data; private Node next; public String getData() { return data; } public void setData(String data) { this.data = data; } public Node getNext() { return next; } public void setNext(Node next) { this.next = next; } }
3、两个有序的链表合并
public class LinkedTest { public static void main(String[] args) { //step1:组装链表 Node node1 = new Node(); node1.setData("A"); Node node2 = new Node(); node2.setData("C"); Node node3 = new Node(); node3.setData("E"); //左侧链表组装 node1.setNext(node2); node2.setNext(node3); Node node4 = new Node(); node4.setData("B"); Node node5 = new Node(); node5.setData("D"); Node node6 = new Node(); node6.setData("F"); //右侧链表组装 node4.setNext(node5); node5.setNext(node6); Node node = mergeLinked(node1, node4); while (node != null) { System.out.println(node.getData()); node = node.getNext(); } } /** * 合并两个有序链表 * * @param leftNode * @param rightNode * @return */ public static Node mergeLinked(Node leftNode, Node rightNode) { if (leftNode == null) { return rightNode; } if (rightNode == null) { return leftNode; } //step1:选择出未来的头节点,作为返回节点 Node result = null; Node headNode = null; Node nextLeftNode = null; Node nextRightNode = null; if (compareTo(leftNode, rightNode)) { result = headNode = leftNode; nextLeftNode = headNode.getNext(); nextRightNode = rightNode; } else { result = headNode = rightNode; nextLeftNode = leftNode; nextRightNode = rightNode.getNext(); } while (nextLeftNode != null || nextRightNode != null) { //左侧剩余链表和右侧剩余链表比对 while (nextLeftNode != null && nextRightNode != null && compareTo(nextLeftNode, nextRightNode)) { headNode.setNext(nextLeftNode); headNode = nextLeftNode; nextLeftNode = nextLeftNode.getNext(); } //右侧剩余链表头节点和左侧剩余链表头节点比对 while (nextLeftNode != null && nextRightNode != null && compareTo(nextRightNode, nextLeftNode)) { headNode.setNext(nextRightNode); headNode = nextRightNode; nextRightNode = nextRightNode.getNext(); } //如果两个链表都不为空则进行下一轮比对 if (nextLeftNode != null && nextRightNode != null) { continue; } //如果链表中已经有一个链表完全合并到主链表中了 while (nextLeftNode != null) { headNode.setNext(nextLeftNode); headNode = nextLeftNode; nextLeftNode = nextLeftNode.getNext(); } while (nextRightNode != null) { headNode.setNext(nextRightNode); headNode = nextRightNode; nextRightNode = nextRightNode.getNext(); } } return result; } /** * 左节点比右节点小 * * @param leftNode * @param rightNode * @return */ public static boolean compareTo(Node leftNode, Node rightNode) { if (leftNode.getData().compareTo(rightNode.getData()) <= 0) { return true; } return false; } } class Node { private String data; private Node next; public String getData() { return data; } public void setData(String data) { this.data = data; } public Node getNext() { return next; } public void setNext(Node next) { this.next = next; } }