单链表的相关操作是面试中经常被提问的内容,也是反映应聘者数据结构基本能力很好的途径,因此系统归纳总结单链表的一些常用操作和实现,是十分必要的。
而且在实现单链表各种操作的过程中,看到了变量和指针的重要性,引入适当的变量和指针或者引用,来记录编程中的中间状态,会大大简化编程逻辑
package DataStructure;
import java.util.Stack;
import javax.swing.text.AbstractDocument.LeafElement;
//Java语言实现单链表以及各种操作
public class LinkList {
// 实际上链表类中维护一个头节点类即可
private Node head;
// 这个节点表示当前节点的索引,可以理解为链表中最后一个节点,在添加节点的时候使用
private Node current;
// 1.向链表中添加节点,肯定是添加到链表结尾
public void add(int data) {
// 如果头节点为空,表示链表还没开始创建。在链表的操作中,记住永远要判断头结点这个特殊节点
// 因为Java中没有指针,名称实际上也是引用,所以为了好理解,可以将对象的名称当成指针来理解
if (head == null) {
head = new Node(data);
current = head;
} else {
current.next = new Node(data);
// 更新当前节点的指向
current = current.next;
}
}
// 2.删除链表的最后一个节点
public void delete() {
if (head == null)
return;
if (head == current) {
head = null;
current = null;
} else {
// 引入前一个节点,然后遍历递进
Node prev = head;
while (prev.next != current) {
prev = prev.next;
}
prev.next = null;
// 千万不要忘记将current更新为最新
current = prev;
}
}
// 3.遍历链表,不一定从head开始遍历,所以传入的node表示遍历开始节点
public void print(Node node) {
// 对传进来的参数首先做非空非null判断,是良好的编程习惯,防止抛出空指针异常
if (node == null)
return;
// 用来做遍历操作的引用,局部变量
Node temp = node;
while (temp != null) {
System.out.print(temp.data + "->");
temp = temp.next;
}
System.out.println("");
}
// 4.求单链表的长度
public int getLength() {
if (head == null)
return 0;
int length = 0;
Node temp = head;
while (temp != null) {
length++;
temp = temp.next;
}
return length;
}
// 5.查找倒数第K个节点
public Node findLastNode(int k) {
// 判断传入参数K的值,如果为0,则返回null
if (k == 0)
return null;
// 定义两个节点,用两个节点的差值来定位倒数第K个
Node first = head;
Node second = head;
for (int i = 0; i < k - 1; i++) {
second = second.next;
}
if (second == null)
return null;
while (second.next != null) {
second = second.next;
first = first.next;
}
return first;
}
// 6.查找链表的中间节点
public Node findMidNode() {
Node first = head;
Node second = head;
while (second != null && second.next != null) {
// 这样对于偶数个节点,是n/2+1
first = first.next;
second = second.next.next;
}
return null;
}
// 7.两个有序单链表合成一条有序单链表
public Node mergeLinkList(Node head1, Node head2) {
if (head1 == null && head2 == null) {
return null;
}
if (head1 == null) {
return head2;
}
if (head2 == null) {
return head1;
}
Node head = null; // 新链表的头结点
Node current = null;// current指向新链表的节点
// head1和head2不断的迭代递进
if (head1.data <= head2.data) {
head = head1;
current = head;
head1 = head1.next;
} else {
head = head2;
current = head;
head2 = head2.next;
}
// 只要由一个走到最后的节点就停止循环
while (head1 != null && head2 != null) {
if (head1.data <= head2.data) {
current.next = head1;
current = current.next;
head1 = head1.next;
} else {
current.next = head2;
current = current.next;
head2 = head2.next;
}
}
if (head1 != null) {
current.next = head1;
}
if (head2 != null) {
current.next = head2;
}
return head;
}
// 8.翻转单链表 出现频率极高
public Node reverseList(Node head) {
if (head == null || head.next == null)
return head;
Node current = head;
// 保持前一个节点prev
Node prev = null;
// 在前面的正序遍历中,需要定义一个prev指针,这里需要后面的节点,则需要一个next指针
Node next = head.next;
while (current != null) {
// 保存下一个节点
next = current.next;
// 将当前节点指向前一个节点
current.next = prev;
// 做完一次操作,两个指针都往前迭代
prev = current;
current = next;
}
return prev;
}
// 9.从尾到头打印链表,对于这种颠倒顺序的,使用栈来完成,要么自己实现栈,要是使用系统的栈,也就是递归
// 自己使用栈,基于循环实现,代码更好
// 如果使用递归,栈深度太大容易溢出
public void reversePrint(Node head) {
if (head == null)
return;
Stack<Node> stack = new Stack<Node>(); // 新建一个栈,java中已经实现了stack类,实际编程中可以使用
Node current = head;
while (current != null) {
stack.push(current);// 压栈
current = current.next;
}
while (stack.size() > 0) {
System.out.println(stack.pop().data);// 出栈
}
}
// 使用递归,代码简洁
public void reversePrintByRecusion(Node head) {
// 既是判断参数条件,也是递归终止条件 递归的最终结束就是遇到return,虽然这个return没有返回任何数据,这时压入栈的数据开始返回
if (head == null)
return;
reversePrintByRecusion(head.next);
System.out.println(head.data);
}
// 10.判断单链表是否有环
public boolean hasCycle(Node head) {
if (head == null)
return false;
Node first = head;
Node second = head;
while (second != null) {
first = first.next;
second = second.next.next;
if (first == second) {
return true; // 一旦两个指针相遇,表示有环
}
}
return false;
}
// 11.得到有环链表中两个指针的相遇节点
public Node hasMeetPoint(Node head) {
if (head == null) {
return null;
}
Node first = head;
Node second = head;
while (second != null) {
first = first.next;
second = second.next.next;
if (first == second) { // 一旦两个指针相遇,说明链表是有环的
return first; // 将相遇的那个结点进行返回
}
}
return null;
}
// 12.得到环的长度
public int getCycleLength(Node node) {
if (node == null)
return 0;
Node current = node;
int length = 0;
while (current != null) {
// 必须先迭代一步,再判断是否相等,否则会一直循环
current = current.next;
length++;
if (current == node) {
return length;
}
}
return length;
}
// 13.获取环的起始点 需要用到判断是否有环、得到相遇点已经得到环的长度这几个方法的配合,再用追击迫近的思想求出起点
// 本质上还是迫近追击,找到倒数第K个值的问题
public Node getCycleStart(Node head, int cycleLength) {
if (head == null)
return null;
Node first = head;
Node second = head;
for (int i = 0; i < cycleLength; i++) {
second = second.next;
}
while (first != null && second != null) {
first = first.next;
second = second.next;
if (first == second)
return first;// 两个指针相遇表示在起始点回合
}
return null;
}
// 方法:获取单链表的长度
public int getLength(Node head) {
if (head == null) {
return 0;
}
int length = 0;
Node current = head;
while (current != null) {
length++;
current = current.next;
}
return length;
}
// 14.得到两个单链表的相交节点 快慢指针 追击迫近
public Node getFirstCommonNode(Node head1, Node head2) {
if (head1 == null || head2 == null) {
return null;
}
int length1 = getLength(head1);
int length2 = getLength(head1);
int lengthDif = 0;
Node longHead = null;
Node shortHead = null;
//找到长短链表的头部和长度差值,分别保存在自定义的变量中
if (length1 > length2) {
longHead = head1;
shortHead = head2;
lengthDif = length1 - length2;
} else {
longHead = head2;
shortHead = head1;
lengthDif = length2 - length1;
}
//将较长的那个链表的指针向前走length个距离
for (int i = 0; i < lengthDif; i++) {
longHead = longHead.next;
}
//将两个链表的指针同时向前移动
while (longHead != null && shortHead != null) {
if (longHead == shortHead) { //第一个相同的结点就是相交的第一个结点
return longHead;
}
longHead = longHead.next;
shortHead = shortHead.next;
}
return null;
}
public static void main(String[] args) {
LinkList list1 = new LinkList();
LinkList list2 = new LinkList();
LinkList list3 = new LinkList();
// 向LinkList中添加数据
for (int i = 0; i < 5; i++) {
list1.add(i);
}
for (int i = 10; i < 5; i++) {
list2.add(i);
}
list3.print(list3.mergeLinkList(list1.head, list2.head));
}
// 链表节点类,内部类实现,包含数据域和指针域
class Node {
int data; // 数据域
Node next;// 指针域
public Node(int data) {
this.data = data;
}
}
}