1.和数组一样,链表也是一种线性表。
2.从内存结构来看,链表的内存结构是不连续的内存空间,是将一组零散的内存块串联起来,从而进行数据存储的数据结构。
3.链表中的每一个内存块被称为节点Node。节点除了存储数据外,还需记录链上下一个节点的地址,即后继指针next。
注意:掌握好技巧,会提高正确率,真正理解后,举一反三。将常见算法深刻理解之后,大多数链表算法将不再是你的障碍。
例如: a和b节点中插入x节点
x.next = a.next;
a.next = x;
//其中x.next引用存储了a的下个节点next 的内存地址
// a.next 引用储存的是x节点的内存地址
//上面是先将节点b 的内存地址同时给多个引用,然后在改变a.next的引用
//如果我们先改变a.next的应用
a.next = x;
x.next = a.next
//这样会产生的现象是,a.next 指向了x的地址,但是b的地址就无法找到了。
//然后x.next = a.next; 这时a.next 已经是x的地址了。相等于x.next = x,造成内存泄漏。
image.png
//1、链表为空
/**
* 尾插入元素是,如果头节点为null,上面的代码就不适用了,需要对节点做一个判断
**/
if(a == null){
a = x;
}else{
Node temp = a;
while(temp.next != null){
temp = temp.next;
}
temp.next = x;
}
return a;
//删除一个节点 a.next = a.next.next;但是如果删除的是B节点,显然也需要做判断
//2、链表只有一个元素、两个元素
//3、头节点和尾节点的处理
// 上面在写链表的处理的时候,插入新节点,有两种代码逻辑,一种是头节点null、一种是不为null,逻辑是不一样的
// 那么我们可以考虑另一种方式,创建一个哨兵节点,这个节点什么元素都不存。只是简化边界判断
Node temp;
Node head = new Node();
head.next = a;
temp = head;
while (temp.next != null) {
temp = temp.next;
}
temp.next = x;
return head.next;
//这样的代码就可以处理,那两种情况,同理删除元素
// 输入:1->2->3->4 输出:4->3->2->1
// 思路: 1、迭代所有节点,取当前节点 2、取当前节点的前一个节点 3、当前节点的next 指向前一个节点
// 实现:
Node reserver(Node curr) {
Node pre = null; //作为全局变量,保证下次遍历时能访问到,初始值null,因为第一次遍历头节点的下个节点是null
while(curr != null){
//头节点(1)的前一个节点是null:curr.next = null;
//但是下一个节点(2)的前一个节点是头节点(1) curr.next =?,所以我们需要将前一个节点保存起来,下次迭代还能访问
curr.next = pre; // 当前节点curr的下个节点指向上一个 pre
pre = curr; //将当前节点给pre预存起来,保证当前处理不会改变,下次迭代时使用,
//如果没有下次迭代,那么pre就是反转后的头节点
curr = curr.next;
}
return pre;
}
// 最后: 如果按照上面的代码去执行,会发现根本不是想要的结果,1->null
// 原因: curr.next = pre 这行,curr的下个节点已经被改变了(null),所以 curr = curr.next 其实等于curr = pre;
// 修改: 将curr原始下个节点预存,给一个变量 temp,然后curr = temp;
Node reserver(Node node) {
Node pre = null; //作为全局变量,保证下次遍历时能访问到,初始值null,因为第一次遍历头节点的下个节点是null
while(node != null){
//头节点(1)的前一个节点是null:curr.next = null;
//但是下一个节点(2)的前一个节点是头节点(1) curr.next =?,所以我们需要将前一个节点保存起来,下次迭代还能访问
Node temp = node.next;
node.next = pre; // 当前节点curr的下个节点指向上一个 pre
pre = node; //将当前节点给pre预存起来,保证当前处理不会改变,下次迭代时使用
node = temp;
}
return pre;
}
合并排序
//两个有序链表合并一个有序链表
// 其中有一个为null,或者都为null
if(l1 == null) {
return l2;
}
if(l2 == null) {
return l1;
}
ListNode listNode = new ListNode(-1);
ListNode pre = listNode;
while(l1 != null && l2 != null){
if(l1.val <= l2.val){
pre.next = l1;
l1 = l1.next;
}else{
pre.next = l2;
l2 = l2.next;
}
pre = pre.next;
return listNode.next;
}
//统计链表长度
ListNode removeNthFromEnd(ListNode head, int n) {
int length = 0;
ListNode first = head;
while (first != null) {
length++;
first = first.next;
}
//移除元素的前个节点的位置(length-=n)
//例如:1->2->3->4->5,5个元素移除倒数第2个节点(4),也就是3=5-2是4的前节点。只要将3 ->5 就完成了删除。
length -= n;
//定义一个带有头节点的链表
ListNode dummy = new ListNode(0);
dummy.next = head;
first = dummy;
while (length > 0) {
length--;
first = first.next;
}
//如果length = 0,first = 3节点。所以将3.next = 3.next.next;
first.next = first.next.next;
return dummy.next;
}
//最后:边界判断 需要考虑,上面默认是参数是合法有效
//1、链表为null,head=null,n=0 其中first.next =first.next.next 相当于 head = head.next,head=null,所以会npt
//2、链表为一个元素/两个元素
//3、头/尾节点
//4、n值非法的情况
// 听到中间立马想到,快慢指针
public ListNode middleNode(ListNode head) {
ListNode p = head; ListNode q = head;
while(q!=null && q.next!=null){
p = p.next;
q = p.next.next;
}
return p;
}
public static boolean hasCycle(ListNode head) {
// 如果2个节点以上,有循环链表,现象:迭代一直会走下去。如何判断有循环,第一个想法某个节点迭代了两次。
// 1->2->3->4 4又指向了1 环就形成了,如果依次迭代,将之前元素放入集合,如果集合中存在,则又环
Set<ListNode> listNodes = new HashSet<>();
while (head!=null){
if (listNodes.contains(head)){
return true;
}
listNodes.add(head);
head = head.next;
}
return false;
}
//第二种 方式,就是使用快慢指针,想一下,如果存在环的话。走的快的人肯定会跟走的慢的相遇,
//想一下操场上,有一个人速度是你的两倍,如果你跑完一圈,那个人肯定会跟你相遇。
//如果你跑到终点了,还没和你没有相遇说明人家早到终点,停下来了。
public static boolean hasCycle(ListNode head) {
// 判断边界 ,1、head = null 2、head.next = null 3、head.next.next =null 4、头尾节点。
// 如果null 或者只有一个节点是不能形成循环节点(本身指向本身节点除外)
if(head == null || head != null){
return false;
}
// 是否是统一起点没有关系,如果是环肯定会相遇
ListNode p = head; ListNode q = head.next;
while(q != null && q.next != null){
if(p == q){
return true;
}
p = p.next;
q = q.next.next;
}
return false;
}