首页 > 编程语言 >【算法】004_链表

【算法】004_链表

时间:2024-01-29 12:11:55浏览次数:42  
标签:Node 结点 head next 链表 算法 004 null

哈希表

  • 哈希表增删改查是常数时间,但是这个常数时间比较大
  • 放入哈希表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小
  • 放入哈希表的东西,如果不是基础类型,内部按引用传递,内存占用就是这个东西的内存地址的大小

有序表

  • 有序表的增删改查是O(logn)级别的
  • 放入有序表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小
  • 放入有序表的东西,如果不是基础类型,必须提供比较器,内部按引用传递,内存占用就是这个东西内存地址的大小

链表

反转单向和双向链表

https://leetcode.cn/problems/reverse-linked-list/

分别实现反转单向链表和反转单向链表的函数

如果链表长度为N,时间复杂度要求为O(N),额外空间复杂度要求为O(1)

package com.algorithm.class04;

public class Code01_ListReverse {
	//单链表结点
	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; }
	}
	
    //反转单向链表
	public ListNode reverseList(ListNode head) {
		ListNode pre = null;
		ListNode next = null;
		while (head != null) {
			next = head.next;
			head.next = pre;
			pre = head;
			head = next;
		}
		return pre;
	}
	
	//-----------------------------------------------------------------------------------------
	
	//双链表结点
	public class DoubleListNode {
		int value;
		DoubleListNode last;
		DoubleListNode next;

		DoubleListNode(int v) {
			value = v;
		}
	}
	
    //反转双向链表
	public DoubleListNode reverseList2(DoubleListNode head) {
		DoubleListNode pre = null;
		DoubleListNode next = null;
		while (head != null) {
			next = head.next;
			head.next = pre;
			head.last = next;
			pre = head;
			head = next;
		}
		return pre;
	}
}

打印两个有序链表的公共部分

给定两个有序链表的头指针head1和head2,打印两个链表的公共部分。

如果两个链表的长度之和为N,时间复杂度要求为O(N),额外空间复杂度要求为O(1)

head1指向第一个链表,head2指向第二个链表

比较两个指针指向的结点:

较小的那个结点的指针往后移动

如果相等,就是链表的公共部分,打印

package com.algorithm.class04;

public class Code02_PrintCommonPart {
	public class Node {
		int value;
		Node next;
		Node(int data) {
			this.value = data;
		}
	}
	
	public void printCommonPart(Node head1, Node head2) {
		System.out.print("Common Part: ");
		while (head1 != null && head2 !=null) {
			if (head1.value < head2.value) {
				head1 = head1.next;
			}else if (head1.value > head2.value) {
				head2 = head2.next;
			}else {
				System.out.println(head1.value + " ");
				head1 = head1.next;
				head2 = head2.next;
			}
			System.out.println();
		}
	}
}

判断一个链表是否是回文结构

LCR 027. 回文链表 - 力扣(LeetCode)

给定一个单链表的头结点head,请判断该链表是否为回文结构

如果链表长度为N,时间复杂度为O(N),额外空间复杂度为O(1)

  • 不同空间复杂度的实现方法:

  • 额外空间O(n):将链表的结点依次入栈,然后每弹出一个结点,就与链表的结点比较

  • 额外空间O(n/2):只把链表的后半部分结点入栈,栈中的结点与链表前半部分比较。就相当于把链表对折,比较对折后的两半是否一样

    用快慢结点的方式找到链表的中点

  • 额外空间O(1):翻转后半部分链表,·

额外空间复杂度O(n)

把所有结点都入栈,然后再出栈

每弹出一个,就和链表的结点依次进行比较

弹出的顺序刚好是从尾到头,链表的顺序又是从头到尾

public boolean isPalindrome1(Node head) {
    Stack<Node> stack = new Stack<>();
    Node cur = head;
    while (cur != null) {
        stack.push(cur);
        cur = cur.next;
    }
    while (head != null) {
        if (head.value != stack.pop().value) {
            return false;
        }
        head = head.next;
    }
    return true;
}

额外空间复杂度O(n/2)

用快慢指针找到链表的中点

把链表的右半部分入栈

然后依次弹出,和左半边的链表进行比较

就相当于比较链表的左半部分和右半部分

而关于中点:

  • 快慢指针都指向第一个结点,最后慢指针指向会:如果是奇数个就指向中点,如果是偶数个就指向第n/2个
  • 快指针先走一步,奇数个时慢指针指向中点,偶数个时指向中点的下一个
  • 快指针先走两步,奇数个时慢指针指向中点的前一个,偶数个时指向中点的上一个
public boolean isPalindrome2(Node head) {
    //链表为空或者链表只有一个结点
    if (head == null || head.next == null) {
        return true;
    }
    /**写法一:
		 * 慢指针指向的结点就是第一个要入栈的结点
		 * 奇数个时慢指针指向中点,偶数个时指向第n/2+1
		 */
    Node slow = head;
    Node fast = head;
    /**写法二
		 * 回文链表可以看成两半,重叠的结点不进行比较
		 * 最终慢指针指向的是右边第一个不重复的结点,比如1 2 3 2 1:则指向右边的2;1 2 3 3 2 1,也是指向右边的2n
		 */
    // Node slow = head.next;
    // Node fast = head;

    while (fast != null && fast.next != null){
        slow = slow.next;
        fast = fast.next.next;
    }

    Stack<Node> stack = new Stack<>();
    //把后半部分入栈
    while (slow != null){
        stack.push(slow);
        slow = slow.next;
    }

    //比较
    while (!stack.isEmpty()){
        if (head.value != stack.pop().value){
            return false;
        }
        head = head.next;
    }
    return true;
}

额外空间复杂度O(1)

快慢指针找到链表的中点,如果是奇数个就慢指针就指向中点处,如果是偶数个就指向中点的下一个

翻转右半边的链表

然后比较两个半链表是否一样

public boolean isPalindrome3(Node head){
    if (head == null || head.next == null){
        return true;
    }

    //找到中点 奇数个时刚好是中点,偶数个时是第n/2个结点
    Node slow = head;
    Node fast = head;
    while (fast != null && fast.next != null){
        slow = slow.next;
        fast = fast.next.next;
    }

    //翻转右半边链表
    Node pre = null;
    Node cur = slow;
    Node next = null;
    while (cur != null){
        next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }//pre指向翻转后的右边链表的头结点

    //判断左右链表是否相同
    Node head_left = head;
    Node head_right = pre;
    boolean res = true;
    while (head_left != null && head_right != null){
        if (head_left.value != head_right.value){
            res = false;
            break;
        }
        head_left = head_left.next;
        head_right = head_right.next;
    }

    //把右边链表翻转回来
    cur = pre;
    pre = null;
    while (cur != null){
        next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    return res;
}

将单向链表按某值划分成左边小、中间相等、右边大的形式

给定一个单链表的头结点head,结点的值类型都是整型,再给定一个整数pivot。实现一个调整链表的函数,将链表调整为左部分都是小于pivot的结点,中间都是等于pivot的结点,右部分都是大于pivot的结点

把结点存储到数组中,然后后面就是荷兰国旗问题

public Node listPartition1(Node head, int pivot){
    if (head ==null){
        return head;
    }
    Node cur = head;
    // int i = 0;
    // while (cur != null){
    //     i++;
    //     cur = cur.next;
    // }
    //把链表的结点放入数组中
    List<Node> nodeList = new ArrayList<>();
    while (cur != null){
        nodeList.add(cur);
        cur = cur.next;
    }
    //划分
    arrPartition(nodeList, pivot);
    //将划分好的结点放入原链表中
    int i;
    for (i = 1; i < nodeList.size(); i++){
        nodeList.get(i - 1).next = nodeList.get(i);
    }
    nodeList.get(i - 1).next = null;
    return nodeList.get(0);
}

public void arrPartition(List<Node> nodeArr, int pivot){
    int small = -1;//小于区
    int big = nodeArr.size();//大于区
    int index = 0;//指向当前遍历的结点

    while (index < big){
        if (nodeArr.get(index).value < pivot){//如果当前结点小于pivot,就要把它放在小于区;放在小于区的下一个位置,同时扩大小于区
            swap(nodeArr, index++, ++small);//交换,同时扩大小于区
        }else if (nodeArr.get(index).value > pivot){//放在小于区同时扩大大于区
            swap(nodeArr, index, --big);//这里index不++,因为大于区的前一个结点,是没有经过比较的,不知道它的大小;而上面的index++,是因为index已经在小于区边界的后面的,那么边界到index这之间的结点一定是等于pivot的
        }else {//等于pivot,继续比较下一个结点
            index++;
        }
    }
    for (Node node : nodeArr) {
        System.out.println(node);
    }
}

public void swap(List<Node> nodeArr, int a, int b){
    Node temp = nodeArr.get(a);
    nodeArr.set(a, nodeArr.get(b));
    nodeArr.set(b, temp);
}

在实现原问题功能的基础上增加如下的要求

  • 调整后所有小于pivot的结点之间的相对顺序和调整前一样
  • 调整后所有等于pivot的结点之间的相对顺序和调整前一样
  • 调整后所有大于pivot的结点之间的相对顺序和调整前一样
  • 时间复杂度达到O(N),额外空间复杂度达到O(1)

只用链表实现,不用数组

6个变量,分别是:小于区的头指针和尾指针,等于区的头指针和尾指针,大于区的头指针和尾指针

初始时每个区的头尾指针指向null,当有第一个结点进入时,头指针就指向这个结点

遇到小于pivot的结点时,接入到小于区的链表,然后更新尾指针指向该结点

遇到等于pivot的结点时,接入到等于区的链表,然后更新尾指针指向该结点

遇到大于pivot的结点时,接入到大于区的链表,然后更新尾指针 指向该结点

最后将三个链表连接起来

public Node listPartition2(Node head, int pivot){
    Node small_head = null;
    Node small_tail = null;
    Node equal_head = null;
    Node equal_tail = null;
    Node big_head = null;
    Node big_tail = null;
    Node next = null;

    while (head != null){
        next = head.next;
        head.next = null;//使得每个结点在进入自己的区域的时候是独立的,断开自己区域的尾结点,如果不这样断开,尾结点可能还连接的有结点,这个结点可能不属于这个区域,会有不该有的指针指向不该有的结点,画图
        if (head.value < pivot){//小于区
            if (small_head == null){//是小于区的第一个结点,也就是小于区的头结点
                small_head = head;
                small_tail = small_head;//更新尾结点
            }else {
                small_tail.next = head;
                small_tail = small_tail.next;
            }
        } else if (head.value > pivot) {//大于区
            if (big_head == null){
                big_head = head;
                big_tail = big_head;
            }else {
                big_tail.next = head;
                big_tail = big_tail.next;
            }
        }else {//等于区
            if (equal_head == null){
                equal_head = head;
                equal_tail = equal_head;
            }else {
                equal_tail.next = head;
                equal_tail = equal_tail.next;
            }
        }
        head = next;
    }
    //连接小于区和等于区
    if (small_tail != null){//可能没有小于的数
        small_tail.next = equal_head;
        //有等于的数就把小于区和等于区相连,没有的话就将small_tail指向equal_tail,以便于后面和大于区相连  把当前存在的链表的末尾当成等于区的末尾
        equal_tail = equal_tail == null ? small_tail : equal_tail;
    }
    //连接等于区和大于区
    if (equal_tail != null){
        equal_tail.next = big_head;
    }
    //每个区都可能为空,返回不为空的头就是整个链表的头结点
    return small_head != null ? small_head : equal_head != null ? equal_head : big_head;
}

复制含有随机指针结点的链表

rand指针是单链表结点结构中新增的指针,rand可能指向链表中的任意一个结点,也可能指向null。给定一个由Node结点类型组成的无环单链表的头结点head,请实现一个函数完成这个链表的复制,并返回复制的新链表的头结点。要求时间复杂度O(N),额外空间复杂度O(1)

空间复杂度O(N)

用一个哈希表存储链表的所有结点,那么1’的next就是1的next复制后的结点,也就是map.get(1->next)这个结点。同理1‘的rand也是根据1的rand找到的

public Node copyListWithRand1(Node head){
    HashMap<Node, Node> map = new HashMap<>();
    Node cur = head;
    while (cur != null){
        map.put(cur, new Node(cur.value));
        cur = cur.next;
    }
    cur = head;
    while (cur != null){
        map.get(cur).next = map.get(cur.next);//1'->next是1->next复制过来的结点,这个结点存储在map中,key是1->next
        map.get(cur).rand = map.get(cur.rand);
        cur = cur.next;
    }
    return map.get(head);
}

空间复杂度O(1)

链表的每个结点都复制一个连接到原始结点的后面,指针的指向不变

1的rand指向3,就说明1'的rand指向了3‘,而3’又是3的next。

两个单链表相交

给定两个可能有环也可能无环的单链表,头结点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的第一个结点。如果不相交,返回null。

要求如果两个链表长度之和为N,时间复杂度达到O(N),额外空间复杂度达到O(1)。

求链表的相交结点有以下三种情况:

  • 两个无环链表
  • 两个有环链表
  • 一个有环一个无环

所以要先判断链表是否有环

主调用函数

//链表的相交结点
public Node getIntersectNode(Node head1, Node head2){
    if (head1 == null || head2 == null){
        return null;
    }

    //寻找两个链表的入环结点
    Node loop1 = getLoopNode(head1);
    Node loop2 = getLoopNode(head2);

    //两个链表都无环
    if (loop1 == null && loop2 == null){
        return noLoop(head1, head2);
    }
    //两个链表都有环
    if (loop1 != null && loop2 != null){
        return bothLoop(head1, loop1, head2, loop2);
    }
    //其中一个链表有环,不会相交
    return null;
}

先判断链表是否有环

设置快慢指针,慢指针每次走一步,快指针每次走两步。相遇之后,慢指针停在原地,快指针指向头结点。快慢指针每次移动一步,再次相遇的时候一定是入环结点处。

//找到第一个人入环节点,如果无环,返回null
public Node getLoopNode(Node head){
    //结点数小于3不会形成环
    if (head == null || head.next == null || head.next.next == null){
        return null;
    }
    Node slow = head.next;
    Node fast = head.next.next;//由于while的判断条件限制,slow和fast不能都设置为head  这里设置为slow、fast分别已经走了一步和两步
    //查找首次相遇的结点
    while (slow != fast){
        if (fast == null || fast.next == null){//链表无环
            return null;
        }
        slow = slow.next;
        fast = fast.next.next;
    }
    fast = head;//快指针指向头结点
    while (slow != fast){
        slow = slow.next;
        fast = fast.next;
    }
    return slow;//再次相遇时就是入环结点
}

情况一:两个无环单链表相交

  • 遍历链表1,记尾结点end1,长度len1;遍历链表2,记尾结点end2,长度len2。
  • 如果end1 != end2,则两个链表一定不相交。(看图)。否则:
  • 两个链表的长度的差值x,长链表先走x步。然后两个链表一起走
  • 第一个相同的结点就是它们的相交结点
public Node noLoop(Node head1, Node head2){
    if (head1 == null || head2 == null){
        return null;
    }
    Node cur1 = head1;
    Node cur2 = head2;
    int size1 = 1;
    int size2 = 1;
    //遍历两个链表,同时计算两个链表的长度
    while (cur1.next != null){//判断条件是cur1.next,因为后面要判断两条链表的 最后一个结点是否相同
        size1++;
        cur1 = cur1.next;
    }
    while (cur2.next != null){
        size2++;
        cur2 = cur2.next;
    }
    //最后一个结点不相同,说明两条链表没有相交
    if (cur1 != cur2){
        return null;
    }
    int d = size1 - size2;
    //d>0说明链表1更长
    cur1 = d > 0 ? head1 : head2;//cur1代表长链表
    cur2 = cur1 == head1 ? head2 : head1;//cur2就是另一条链表
    d = Math.abs(d);
    //长链表先走d步
    while (d > 0){
        cur1 = cur1.next;
        d--;
    }
    //相交结点
    while (cur1 != cur2){
        cur1 = cur1.next;
        cur2 = cur2.next;
    }
    return cur1;
}

情况二:两个链表都有环

  • 1.两个链表不相交

    先求两个链表的入环结点loop1和loop2

    链表1先走,走的过程中如果没有遇到loop2,那么就是第1种

    如果遇到了loop2,就是第3种情况。这时loop1和loop2都是第一个相交结点,只是一个离链表1近,一个离链表2近

  • 2.相交时,入环结点相同,共用环

    可以看作是无环链表求相交结点。

    把它们的入环结点当作两个链表的尾结点,丢弃环,就变成了无环链表求相交结点

  • 3.入环结点不同,共用环

//两个链表都有环
public Node bothLoop(Node head1, Node loop1, Node head2, Node loop2){
    Node cur1 = null;
    Node cur2 = null;
    //入环结点相同,看作无环链表相交问题
    if (loop1 == loop2){
        cur1 = head1;
        cur2 = head2;
        int size1 = 0;
        int size2 = 0;
        while (cur1 != loop1){//注意这里判断的是是否等于loop1,无环链表的尾结点这里是空的,但这里有环链表相交,我们只是看作无环链表不是真的无环
            size1++;
            cur1 = cur1.next;
        }
        while (cur2 != loop2){
            size2++;
            cur2 = cur2.next;
        }
        int d = size1 - size2;
        cur1 = d > 0 ? head1 : head2;
        cur2 = cur1 == head1 ? head2 : head1;
        d = Math.abs(d);
        while (d > 0){
            cur1 = cur1.next;
            d--;
        }
        while (cur1 != cur2){
            cur1 = cur1.next;
            cur2 = cur2.next;
        }
        return cur1;
    }else {//入环结点不同
        cur1 = loop1.next;//先走一步,否则进不了while
        while (cur1 != loop1){//cur1走一圈的过程中,是否会遇到loop2
            if (cur1 == loop2){//链表1遍历的过程中遇到了链表2的入环结点
                return loop1;
            }
            cur1 = cur1.next;
        }
        return null;
    }
}

情况三:一个链表有环,一个链表无环

两个链表一定不相交

标签:Node,结点,head,next,链表,算法,004,null
From: https://www.cnblogs.com/jyyyy/p/17994226

相关文章

  • RL5 PPO算法
    PPO算法算法是一类典型的算法,既适用于连续动作空间,也适用于离散动作空间。算法是一种基于策略梯度的强化学习算法,由的研究人员等人在年提出。算法的主要思想是通过在策略梯度的优化过程中引入一个重要性权重来限制策略更新的幅度,从而提高算法的稳定性和收敛性。算法......
  • (算法)快速幂运算和取模的基本知识
    引子:在高精度中的麦森数中运用到了快速幂运算求一个数的多少次方可以用到快速幂,原理a^11=a^1*a^3*a^8,而为什么是拆成1,3,8而不是其他的呢,是因为11转化为二进制码是1011,这就分别对应了他的权重,有了这个基本知识后,执行这种类似的运算就可以大幅度减少时间。实现这个代码还需要用到位......
  • 今日回顾-回溯算法-17. 电话号码的字母组合
    注意点&感悟:我知道为什么,当初有些学霸说要复习了。因为有的知识点,你一遍没学会,自然要重复学习。所为复习,就是再学一遍。而简单的知识点,就不需要复习了,你已经明显知道自己掌握了,就不需要复习了。而预习呢?是为了,让提前学一遍,更多的是针对那些上课时间有限,以及学生等不及的情况......
  • 算法笔记 pdf下载
    《算法笔记》内容包括:C/C++快速入门、入门模拟、算法初步、数学问题、C++标准模板库(STL)、数据结构专题(二章)、搜索专题、图算法专题、动态规划专题、字符串专题、专题扩展。《算法笔记》印有二维码,用来实时更新、补充内容及发布勘误的。《算法笔记》可作为计算机专业研究生入学考......
  • (4/60)两两交换链表结点、删除链表倒数第N个结点、链表相交、环形链表
    两两交换链表结点leetcode:24.两两交换链表中的节点迭代法思路第一步cur->next=cur->next->next第二步cur->next->next=原cur->next第三步cur->next->next->next=原cur->next->next->next注意用临时变量保存指针位置。复杂度分析时间复杂度:O(N)。空间复杂度:O(1)。......
  • 链表操作
    代码随想录移除元素。不设置虚拟头节点,分类讨论。structListNode*removeElements(structListNode*head,intval){structListNode*temp;//当头结点存在并且头结点的值等于val时while(head&&head->val==val){temp=head;//将新的头结点设置为head->next并删......
  • 莫队算法/分块思想
    莫队算法/分块思想引入对于区间问题,常常会使用线段树维护,但是对于一些数据合并复杂度无法\(O(1)\)解决。所以不能使用,应当使用莫队算法。定义对于离线处理的查询问题,通过合理安排这些计算的次序,得到一个较优的复杂度例题1一个长度为\(n\)的序列,询问\(m\)次\([L,R]\)......
  • 读论文-基于Python的协同过滤算法的研究与应用实现
    前言今天读的论文为一篇名为《基于Python的协同过滤算法的研究与应用实现》的论文,文章是在2019年9月发表于《电脑知识与技术》的一篇期刊论文。摘要随着科学技术的快速发展和知识产权的日益重要,大多数用户会选择在播放平台上看电影。例如腾讯视频、爱奇艺等,用户迫切需要一个合......
  • 补充:基于项目的协同过滤推荐算法(Item-Based Collaborative Filtering Recommendation
    前言继续上篇博客,继续读论文。想看上篇论文的同学可以点击这里相关工作Inthissectionwebrieflypresentsomeoftheresearchliteraturerelatedtocollaborativefiltering,recommendersystems,dataminingandpersonalization.在本节中,我们简要介绍了一些与协同......
  • 数据结构与算法:递归算法
    递归算法什么是递归?函数直接或间接调用自身的过程称为递归,相应的函数称为递归函数。使用递归算法,可以很容易地解决某些问题。此类问题的示例包括汉诺塔(TOH)、中序/先序/后序树遍历、图的DFS递归函数通过调用自身的副本并解决原始问题的较小子问题来解决特定问题。需要时可以生......