死磕算法-链表相关

反转单向链表

在这里插入图片描述
反转
在这里插入图片描述

利用栈反转单向链表

通过栈的先进后出,反转单向链表

 public class Node{
        public int value;
        public Node next;
        public Node(int data){
            this.value=data;
        }
    }

    public Node reverseListStack(Node head){
        Stack<Node> stack=new Stack<Node>();
        while(head!=null){
            stack.push(head);
            head=head.next;
        }
        return stack.peek();
    }
直接移位反转单向链表
 public class Node{
        public int value;
        public Node next;
        public Node(int data){
            this.value=data;
        }
    }

public Node reverseList(Node head){
        Node pre=null;
        Node next=null
        while(head!=null){
           //先用next保存head的下一个节点的信息
           next=head.next;
           //保存完next,就可以让head从指向next变成指向pre,实现反转
           head.next=pre;
           //将head指向了pre,继续依次反转下一个节点
           pre=head;
           //将next赋值给head,即head指向了节点2,将节点2设置位头节点
           //让pre,head,next依次向后移动一个节点,继续下一次指针反转
           head=next;
        }
        //如果head为null,pre就为最后一个节点了,但是链表已经反转完毕,pre就是反转后链表的第一个节点
        return pre;
    }

单链表的反转

  1. 保存当前头结点的下个节点
  2. 将当前头结点的下一个节点指向"上一个节点",这一步实现了反转
  3. 将当前头结点设置为"上一个节点"
  4. 将保存的下一个节点设置为头结点

在这里插入图片描述


反转双向链表

package QuestionTest;

/**
 * Created by L_kanglin on 2017/3/17.
 * 反转双向链表
 */
public class Test9 {
    public class DoubleNode{
        public int value;
        public DoubleNode next;
        public DoubleNode last;

        public DoubleNode(int data){
            this.value=data;
        }
    }
    public DoubleNode reverseList(DoubleNode head){
        DoubleNode pre = null;
        DoubleNode next = null;
        while(head!=null){
            //保存当前结点的下一个节点
            next = head.next;
            //保存完next,就可以让head的next节点从指向next变成指向pre
            head.next=pre;
            //让head的pre节点从指向pre变成指向next
            head.last=next;
            //将头结点指向原头结点的下一个节点
            pre=head,继续反转其他结点
            //让pre,head,head依次向后移动一个节点,继续下一次指针的反转
            head=next;
        }
        return pre;
    }
}

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

题目:给定一个链表的头节点head,请判断该链表是否为回 文结构。 例如: 1->2->1,返回true。 1->2->2->1,返回true。 15->6->15,返回true。 1->2->3,返回false
所谓回文结构就是,正着念和反着念都一样

压栈判断是否为回文结构

将链表压入栈,借助栈的先进后出特点,边弹栈边和原来的链表比较是否一致,会造成O(N)的空间复杂度。

public static class Node {
		public int value;
		public Node next;

		public Node(int data) {
			this.value = data;
		}
	}

// need n extra space
	public static boolean isPalindrome1(Node head) {
		Stack<Node> stack = new Stack<Node>();
		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;
	}
快慢指针
  1. 获取链表的中点,使用两个快慢指针,一个遍历速度是另外一个两倍,找到中点
  2. 然后反转链表的后半部分
  3. 对比链表的前后部分是否一样
  4. 不管是否为回文,最后都将原链表的后半部分反转恢复原来的链表(恢复作案现场)

在这里插入图片描述

// need O(1) extra space
	public static boolean isPalindrome3(Node head) {
		if (head == null || head.next == null) {
			return true;
		}
		//定义两个节点,为快慢指针
		Node n1 = head;
		Node n2 = head;
		while (n2.next != null && n2.next.next != null) { // find mid node
			n1 = n1.next; // n1 -> mid
			n2 = n2.next.next; // n2 -> end
		}
		n2 = n1.next; // n2 -> right part first node
		n1.next = null; // mid.next -> null
		Node n3 = null;
		//右半部分逆序
		while (n2 != null) { // right part convert
			n3 = n2.next; // n3 -> save next node
			n2.next = n1; // next of right node convert
			n1 = n2; // n1 move
			n2 = n3; // n2 move
		}
		n3 = n1; // n3 -> save last node
		n2 = head;// n2 -> left first node
		boolean res = true;
		//判断左右部分的链表是否相等
		while (n1 != null && n2 != null) { // check palindrome
			if (n1.value != n2.value) {
				res = false;
				break;
			}
			n1 = n1.next; // left to mid
			n2 = n2.next; // right to mid
		}
		//将左右两边连接起来
		n1 = n3.next;
		n3.next = null;
		//恢复作案现场
		while (n1 != null) { // recover list
			n2 = n1.next;
			n1.next = n3;
			n3 = n1;
			n1 = n2;
		}
		return res;
	}

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

【题目】 一种特殊的链表节点类描述如下: public class Node { public int value; public Node next; public Node rand; public Node(int data) { this.value = data; } }

Node类中的value是节点值,next指针和正常单链表中next指针的意义 一 样,都指向下一个节点,rand指针是Node类中新增的指针,这个指 针可 能指向链表中的任意一个节点,也可能指向null。 给定一个由 Node节点类型组成的无环单链表的头节点head,请实现一个 函数完成 这个链表中所有结构的复制,并返回复制的新链表的头节点。 进阶: 不使用额外的数据结构,只用有限几个变量,且在时间复杂度为 O(N) 内完成原问题要实现的函数

使用哈希表
public static class Node {
		public int value;
		public Node next;
		//random指针指向其他任意节点
		public Node rand;

		public Node(int data) {
			this.value = data;
		}
	}

	public static Node copyListWithRand1(Node head) {
		HashMap<Node, Node> map = new HashMap<Node, Node>();
		Node cur = head;
		while (cur != null) {
			//key为原节点,value为拷贝节点
			map.put(cur, new Node(cur.value));
			cur = cur.next;
		}
		cur = head;
		while (cur != null) {
			//连接链表
			//得到cur的拷贝节点,将该拷贝节点的next指向cur.next的拷贝节点
			map.get(cur).next = map.get(cur.next);
			//得到cur的拷贝节点,将该拷贝节点的rand指向cur.rand的拷贝节点
			map.get(cur).rand = map.get(cur.rand);
			cur = cur.next;
		}
		return map.get(head);
	}
无额外空间解法

基本思路:拷贝节点,例如对于 1->2->3->4,我们插入每个节点的后面插入其copy节点,使之为
1->1’->2->2’->3->3’->4->4’。那么我们找到源节点,即可找到其copy节点的位置(源节点.next),相当于哈希表的作用
,接着根据原链表的rand关系,链接copy节点的rand指针,最后将链表拆分为原链表和copy链表。

public static class Node {
		public int value;
		public Node next;
		//random指针指向其他任意节点
		public Node rand;

		public Node(int data) {
			this.value = data;
		}
	}



public static Node copyListWithRand2(Node head) {
		if (head == null) {
			return null;
		}
		Node cur = head;
		Node next = null;
		// copy node and link to every node
		//拷贝节点
		while (cur != null) {
			next = cur.next;
			cur.next = new Node(cur.value);
			cur.next.next = next;
			cur = next;
		}
		cur = head;
		Node curCopy = null;
		// set copy node rand
		//设置rand
		while (cur != null) {
			next = cur.next.next;
			curCopy = cur.next;
			curCopy.rand = cur.rand != null ? cur.rand.next : null;
			cur = next;
		}
		Node res = head.next;
		cur = head;
		// split
		//原链表和拷贝链表分离
		while (cur != null) {
			next = cur.next.next;
			curCopy = cur.next;
			cur.next = next;
			curCopy.next = next != null ? next.next : null;
			cur = next;
		}
		return res;
	}

在这里插入图片描述


单链表相交

题目: 在本题中,单链表可能有环,也可能无环。给定两个 单链表的头节点 head1和head2,这两个链表可能相交,也可能 不相交。请实现一个函数, 如果两个链表相交,请返回相交的 第一个节点;如果不相交,返回null 即可

单链表相交涉及的问题比较复杂,需要先考虑单链表中是否有环,再进一步按照无环链表和有环链表的情况进行分析。

判断链表是否有环
方法一.穷举遍历

首先从头节点开始,依次遍历单链表中的每一个节点。每遍历到一个新节点,就从头节点重新遍历新节点之前的所有节点,用新节点的内存地址和之前所有节点的内存地址,如果发现新节点之前的节点存在相同的内存地址,则说明该节点被遍历过两次,链表有环,直到next指针为空,则该链表无环,否则继续遍历。

方法二.哈希表缓存

创建Key为节点的HashSet集合,用来存储曾经遍历过的节点,然后同样是从头节点开始的,依次遍历链表中的节点,每遍历到一个新节点,就用新节点和HashSet集合当中存储的节点作比较,如果发现HashSet当中存在相同节点内存地址,则说明链表有环,如果HashSet当中不存在相同的节点,就把这个新节点存入HashSet,之后进入下一节点,继续重复刚才的操作

这个方法在流程上和方法一类似,本质的区别是使用了HashSet作为额外的缓存

方法三.快慢指针

首先创建两个指针1和2,同时指向这个链表的头节点。然后开始一个大循环,在循环体中,让指针1每次向下移动一个节点,让指针2每次向下移动两个节点,然后比较两个指针指向的节点是否相同。如果相同,则判断出链表有环,如果不同,则继续下一次循环。

例如链表A->B->C->D->B->C->D,两个指针最初都指向节点A,进入第一轮循环,指针1移动到了节点B,指针2移动到了C。第二轮循环,指针1移动到了节点C,指针2移动到了节点B。第三轮循环,指针1移动到了节点D,指针2移动到了节点D,此时两指针指向同一节点,判断出链表有环、

此方法也可以用一个更生动的例子来形容:在一个环形跑道上,两个运动员在同一地点起跑,一个运动员速度快,一个运动员速度慢。当两人跑了一段时间,速度快的运动员必然会从速度慢的运动员身后再次追上并超过,原因很简单,因为跑道是环形的

public static Node getLoopNode(Node head) {
		if (head == null || head.next == null || head.next.next == null) {
			return null;
		}
		Node n1 = head.next; // n1 -> slow
		Node n2 = head.next.next; // n2 -> fast
		while (n1 != n2) {
			if (n2.next == null || n2.next.next == null) {
				return null;
			}
			n2 = n2.next.next;//快指针走两步
			n1 = n1.next;//慢指针走一步
		}
		//当n2 == n1时,让快指针回到头节点
		n2 = head; // n2 -> walk again from head
		while (n1 != n2) {
			n1 = n1.next;
			n2 = n2.next;
		}
		//返回环的入口
		return n1;
	}
单链表相交

在这里插入图片描述
工作流程:

  1. 先判断两个链表的尾节点是否相等,如果连尾节点都不相等,则这两个链表必定不相交
  2. 若这两个链表尾节点相等,则需要判断这两个链表的长度,长度较长的链表需要走到和长度较短的链表的头节点位置,之后"一起走"寻找开始相交的节点
public static Node noLoop(Node head1, Node head2) {
		if (head1 == null || head2 == null) {
			return null;
		}
		Node cur1 = head1;
		Node cur2 = head2;
		int n = 0;
		while (cur1.next != null) {
			//计算链表一的长度
			n++;
			cur1 = cur1.next;
		}
		while (cur2.next != null) {
			//计算链表一和链表二的差值
			n--;
			cur2 = cur2.next;
		}
		//先直接获取两个链表的尾节点,如果两个尾节点都不相同,则这两个链表一定不相交
		if (cur1 != cur2) {
			return null;
		}
		//如果n>0,说明链表一的长度大于链表二的长度,则cur1设为head1
		cur1 = n > 0 ? head1 : head2;
		//如果cur1为head1,则cur2设为head2
		cur2 = cur1 == head1 ? head2 : head1;
		//将n设为绝对值,n为链表一和链表二长度的差
		n = Math.abs(n);
		//cur1被设为长度较长的链表,先让cur1走掉两个链表之间的差值,即让两个链表回到共同的起跑线
		while (n != 0) {
			n--;
			cur1 = cur1.next;
		}
		//寻找共同的节点
		while (cur1 != cur2) {
			cur1 = cur1.next;
			cur2 = cur2.next;
		}
		//返回相交节点
		return cur1;
	}

在这里插入图片描述

链表带环相交

不带环的单链表和带环的单链表不会相交,必须是两个不带环的单链表或者两个都带环的单链表才会相交。而两个带环链表的相交又分为:环内相交和环外相交。

  • 环外相交:两个链表中的环的入口点相同,可转化为两个链表不带环部分的尾,从而求交点
  • 环内相交:两个链表中的环的入口点不同

在这里插入图片描述

//传入链表节点的同时再传入链表中的环的入口节点
public static Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) {
		Node cur1 = null;
		Node cur2 = null;
		//环外相交情况--可转化为不带环的相遇情况
		if (loop1 == loop2) {
			cur1 = head1;
			cur2 = head2;
			int n = 0;
			while (cur1 != loop1) {
				n++;
				cur1 = cur1.next;
			}
			while (cur2 != loop2) {
				n--;
				cur2 = cur2.next;
			}
			cur1 = n > 0 ? head1 : head2;
			cur2 = cur1 == head1 ? head2 : head1;
			n = Math.abs(n);
			while (n != 0) {
				n--;
				cur1 = cur1.next;
			}
			while (cur1 != cur2) {
				cur1 = cur1.next;
				cur2 = cur2.next;
			}
			return cur1;
		}//环内相交 
		else {
			cur1 = loop1.next;
			//遍历一周,直到回来环的入口
			while (cur1 != loop1) {
				//相遇到了链表二的入环处,则返回链表一的入环处
				if (cur1 == loop2) {
					return loop1;
				}
				cur1 = cur1.next;
			}
			return null;
		}
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值