目录
链表
- (单链表)是通过指针将节点串联一起的线性结构,每个节点由数据域和指针域构成,指针域存放指向下一节点的指针。
- 除了单链表,还有双链表、循环链表两种链表类型
- 双链表,每个节点有两个指针域,一个指向下一个节点,另一个指向上一个节点。既可以向前查询,也可以向后查询
- 循环链表,链表首尾相连
- 链表中的节点在内存中不是连续分布的,散落在内存的不同地址空间上,通过指针串联
链表的操作
删除节点
- 将要删除节点的前一节点指针指向删除节点的后一节点
- 至于这一删除节点的内存,系统内存回收机制会去释放该内存
- O(1)操作
增加节点
- 将新节点的指针指向添加位置的后一节点,再将要添加位置的前一节点指针指向新节点,
- O(1)操作
查找节点
- 虽然以上两个操作都是O(1),但都需要在操作前进行查找,而查找需要从头结点开始逐个查找,因而时间复杂度O(n)
链表操作的两种方式
- 直接使用原来的链表来进行操作。
- 设置一个虚拟头结点在进行操作。虚拟头节点可以统一对节点的操作,无需区别该节点是否头节点。
性能分析
public class ListNode {
/**
* 节点的数据值
*/
int val;
/**
* 下一节点
*/
ListNode next;
/**
* 节点的无参构造函数
*/
public ListNode() {
}
/**
* 节点的有参构造函数(一个参数)
*
* @param val
*/
public ListNode(int val) {
this.val = val;
}
/**
* 节点的有参构造函数(两个参数)
*
* @param val
* @param next
*/
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
203. 移除链表元素
public class RemoveLinkedListElements {
/**
* 原链表删除元素方法(需区别是否头节点来进行操作)
*
* @param head
* @param val
* @return
*/
public ListNode regularMethod(ListNode head, int val) {
// 需要定义一个用来遍历链表的移动指针
ListNode cur;
// 先判断头节点的值是否等于val,是则头结点后移
// 因为需要一直往后判断这个头节点是否等于val 直到不等于 所以使用while
while (head != null && head.val == val) {
head = head.next;
}
// 因为单链表无法向前查找,所以移动指针需要从最新的头节点开始遍历
cur = head;
// 判断非头节点的值是否等于val,是则非头节点的上一节点指针指向非头节点的下一节点
while (cur != null && cur.next != null) {
if (cur.next.val == val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return head;
}
/**
* 虚拟头节点方法(统一头节点和非头节点的操作)
*
* @param head
* @param val
* @return
*/
public ListNode dummyHeadMethod(ListNode head, int val) {
// 定义一个虚拟头节点,其指针指向真实头节点
ListNode dummy = new ListNode();
dummy.next = head;
// 定义一个用于遍历的指针
ListNode cur = dummy;
while (cur.next != null) {
if (cur.next.val == val) {
// 找到相同的值,则删除该节点
cur.next = cur.next.next;
} else {
// 否则移动遍历的指针
cur = cur.next;
}
}
return dummy.next;
}
}
707. 设计链表
public class MyLinkedList {
/**
* 链表长度
*/
int size;
/**
* 虚拟头节点
*/
ListNode dummy;
/**
* 初始化链表
*/
public MyLinkedList() {
size = 0;
dummy = new ListNode(0);
}
/**
* 获取对应下标节点的值
*
* @param index
* @return
*/
public int get(int index) {
// 判断下标值的合法性
if (index < 0 || index >= size) {
return -1;
}
ListNode cur = dummy;
// 因为还有虚拟头节点,得多走一步才到指定下标的位置
for (int i = 0; i <= index; i++) {
cur = cur.next;
}
// 循环结束,cur已走到对应下标位置
return cur.val;
}
/**
* 插入新头节点
*
* @param val
*/
public void addAtHead(int val) {
// 新节点
ListNode newNode = new ListNode(val);
// 新节点的指针指向真正的头节点(即虚拟头节点的next),没有头节点时也适用
newNode.next = dummy.next;
dummy.next = newNode;
// 长度+1
size++;
}
/**
* 插入新尾节点
*
* @param val
*/
public void addAtTail(int val) {
// 新节点
ListNode newNode = new ListNode(val);
ListNode cur = dummy;
for (int i = 0; i < size; i++) {
cur = cur.next;
}
// 此时cur指针已到尾节点的位置,在其之后加新节点,不要忘记了长度+1
cur.next = newNode;
size++;
}
/**
* 在指定下标前加入新节点
*
* @param index
* @param val
*/
public void addAtIndex(int index, int val) {
ListNode newNode = new ListNode(val);
// 下标合法值判断
if (index < 0 || index > size) {
// 等于链表长度时,相当于在链表尾部加新节点,所以不包含这种情况
// index为0,size为0,相当于加头节点,也不包含情况
return;
}
ListNode cur = dummy;
// 遍历cur指针到对应index的上一节点
for (int i = 0; i < index; i++) {
cur = cur.next;
}
// 先将新节点指针指向下一节点,否则先给cur的next赋值的话,会丢失下一节点
newNode.next = cur.next;
cur.next = newNode;
size++;
}
/**
* 删除指定下标的节点
*
* @param index
*/
public void deleteAtIndex(int index) {
// 校验下标的合法性
if (index < 0 || index >= size) {
return;
}
ListNode cur = dummy;
// 要删除节点,就要将前一节点指向后一节点
for (int i = 0; i < index; i++) {
cur = cur.next;
}
// 此时cur在指定下标的上一节点
cur.next = cur.next.next;
size--;
}
}
206. 反转链表
由于链表由指针串联,当涉及对链表操作时,是需要至少2个临时指针来作为遍历的指向或者信息缓存。
本题建议使用双指针法(实际我认为是多指针,都三个了),后续复习到递归时再尝试使用递归解此题。
public class ReverseList {
/**
* 双指针解法(逻辑比递归清晰易懂)
*
* @param head
* @return
*/
public ListNode twoPointersMethod(ListNode head) {
// 定义移动的cur指针
ListNode cur = head;
// 定义用于存前后节点的临时指针
ListNode pre = null;
ListNode tmp;
// 当cur为null时说明链表已结束
while (cur != null) {
// 暂存下一节点
tmp = cur.next;
// 当前节点指向上一节点
cur.next = pre;
// 更新cur和pre指针
pre = cur;
cur = tmp;
}
// 当cur指针指向null说明已到尾部,返回新头节点
return pre;
}
}