链表理论基础
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链接的入口节点称为链表的头结点也就是head。
链表的类型:
单链表:
单链表中的指针域只能指向节点的下一个节点。
双链表:
双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
双链表 既可以向前查询也可以向后查询。
循环链表:
链表首尾相连。
链表的储存方式
链表在内存中不是连续分布的。
链表是通过指针域的指针链接在内存中各个节点。
所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起。
链表的构造(java):
public class ListNode {
int val;
ListNode next;
ListNode() {} //无参
ListNode(int val) { this.val = val; }//一个参数
//两个参数
ListNode(int val, ListNode next) { this.val = val; this.next = next;}
}
删除节点:
只要将C节点的next指针 指向E节点就可以了。
那有同学说了,D节点不是依然存留在内存里么?只不过是没有在这个链表里而已。
是这样的,所以在C++里最好是再手动释放这个D节点,释放这块内存。
其他语言例如Java、Python,就有自己的内存回收机制,就不用自己手动释放了。
添加节点:
复杂度为O(1)
数组和链表的对比:
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。
参考资料:代码随想录
203. 移除链表元素
题目:力扣
涉及如下链表操作的两种方式:
- 直接使用原来的链表来进行删除操作。
- 设置一个虚拟头结点在进行删除操作。
这里采用的是第二种方式:
public ListNode removeElements(ListNode head, int val) {
ListNode result = new ListNode(0);//虚拟结点
result.next = head;
ListNode temp = result;//在head前一位,如果head为val需要删除的时候方便操作;
//如果不用虚拟节点,那么就需要对头节点进行判断,是否为空?是否为val然后将头节点移动
//head = head.next
while(head != null){
if(head.val == val){
head = head.next;
temp.next = head;//如果不采用虚拟节点可以这么写:temp.next.next = head
}else{
head = head.next;
temp = temp.next;
}
}
return result.next;
}
参考资料:
707.设计链表
题目:力扣
自己写了好几次测试用例都出问题 ,研究了下发现在设置变量的时候直接采用了head节点,但是在进行删除、添加头节点操作的时候很容易出现问题。于是改成用虚拟节点。
刚开始打算完全模拟链表,但是index是否有效很难判断,每次都需要遍历并且循环容易出问题,因此增加了size变量。
ps:需要注意边界问题
class Node{
int val;
Node next;
Node(){};
Node(int val){this.val = val;}
}
class MyLinkedList {
Node head;//虚拟节点
//考虑到删除节点 - 设置虚拟节点比较方便
int size;
public MyLinkedList() {
head = new Node(0);
size = 0;
}
public int get(int index) {
if(index <0 || index >= size){ //index = 0, size = 1
return -1;
}
Node cur = head;
for(int i=0; i<=index; i++){ //注意从虚拟指针开始遍历
cur = cur.next;
}
return cur.val;
}
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 cur = head;
for(int i=0; i<index; i++){//遍历到所需要找的节点的前一位
cur = cur.next;
}
Node addnode = new Node(val);
addnode.next = cur.next;
cur.next = addnode;
size ++;
}
public void deleteAtIndex(int index) {
if(index < 0 || index >= size){
return;
}
Node cur = head;
for(int i=0; i<index; i++){//遍历到所需要找的节点的前一位
cur = cur.next;
}
cur.next = cur.next.next;
size --;
}
}
参考资料:代码随想录
206. 翻转链表
题目:力扣
很容易就想到通过迭代的方式,在head前面增加一个null节点,先储存head的下一个节点,然后将head的next指针指向null节点,然后将head向后移动,依次迭代改变链表的指针方向,最后直接输出原来最后一个节点,作为头节点。
但是在实现的时候需要注意循环的写法:
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while(head != null){ //这里不能使用head.next != null,这样最后一个节点遍历不到
head = head.next;//储存head下一个节点
cur.next = pre;//换方向
pre = cur;//移位
cur = head;//移位
}
return pre;//head为null了,pre在head前一位,也就是最后一个节点
}
还可以使用递归来实现。
参考资料:代码随想录