算法中的数据结构之链表
定义一个结点结构
public class Node {
//使用内部类定义链表的每个节点
public int value;//表示此节点上的数
public Node next;//此节点的下一个节点
public Node pro;//此节点的上一个节点
//构造函数,给此节点赋值
public Node(int data){
this.value = data;
}
public Node(){
}
}
题目一
反转单向链表和双向链表
分别实现反转单向链表和反转双向链表的函数
要求:如果链表长度为N,时间复杂度要求为O(N),额外空间复杂度要求为O(1)
单向链表的反转
public class ReOneLink {
//进行反转,借用数组反转
public static Node reOneLink(Node node){
List<Node> nodes = new ArrayList<Node>();
while(node != null){
nodes.add(node);
node = node.next;
}
Node head = new Node();
Node n = new Node();
for(int i=nodes.size()-1;i>=0;i--){
n = nodes.get(i);
n = n.next;
}
head = nodes.get(nodes.size()-1);
return head;
}
}
双向链表的反转
结点结构的定义
public static class Node{
public int value;//表示此节点上的数
public Node next;//此节点的下一个节点
public Node pro;//此节点的上一个节点
//构造函数,给此节点赋值
public Node(int data){
this.value = data;
}
}
public class ReTowLink {
//对于双向链表的特点,头节点有下一个next没有上一个,尾节点有上一个没有上一个
public static Node reTowLink(Node node){
Node[] nodes = null;
int i = 0;
while(node.next != null){
nodes[i++] = node;
node = node.next;
}
Node n = null;
int length = nodes.length-1;
for(i=length;i>=0;i--){
node = nodes[length];
n = node;
node.next = node.pro;
node.pro = n.next;
}
nodes[0].next = null;//变成尾结点
nodes[nodes.length-1].pro = null;//变成头结点
return nodes[nodes.length-1];
}
}
判断一个链表是否为回文结构
//题目:给定一个单链表的头节点head,请判断该链表是否为回文结构
//例子:1->2->3,返回true;1->2->3,返回false
//例子:如果链表长度为N,时间复杂度达到O(N),额外空间复杂度达到O(1)
方法1:使用栈(笔试的时候用就行)
public static boolean isPalindrome1(Node head){
//使用java中定义的栈结构
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;
}
方法2:使用n/2的方法,找快慢指针
//方法2:使用n/2的方法,找快慢指针
/*
有两个指针,一个一次跳一个,一个一次跳两个,第二个跳到尾的时候,第一个调到一半位置
实际就是想找中点位置,因为链表不能直接找到,所以靠这种方法找到
*/
public static boolean isPalindrome2(Node head){
//判断链表是否为空,或者链表的下一个节点是否为空
if(head == null || head.next == null){
return false;
}
Node right = head.next;//慢指针指向头结点的下一个位置
Node cur = head;//快指针指向头结点
//此时的条如果链表结点数为奇数,当快指针指向最后一个结点的时候,慢指针指向中间位置,偶数的时候,慢指针指向中间位置的两个数的第一个
while(cur.next != null && cur.next.next != null){//表示不为空
right = right.next;//一次跳一个节点
cur = cur.next.next;//一次跳两个结点
}
Stack<Node> stack = new Stack<>();//创建一个栈结构
while(right != null){
stack.push(right);//此时的处于一半的位置,入栈
right = right.next;
}
while (!stack.isEmpty()){//栈不为空就循环
if(head.value != stack.pop().value){//进行比较,回文判断,有一个不一样的就返回false
return false;
}
head = head.next;//当前节点指向下一个
}
return true;
}
方法3:十分重要(面试谈资)
//方法3:就是降低了空间复杂度,降低了一半,不使用栈结构,反转链表
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){
n1 = n1.next;
n2 = n2.next.next;
}
n2 = n1.next;
n1.next = null;
Node n3 = null;
//后半部分的链表反转代码,可以画图进行演示
while(n2 != null){
n3 = n2.next;
n2.next = n1;
n1 = n2;
n2 = n3;
}
n3 = n1;//让n3存储最后一个结点,用于最后的反转,因为n1要移动
n2 = head;//n2指向头结点,开始比较
boolean res = true;
while(n1 != null && n2 != null){
if(n1.value != n2.value){
res = false;
break;
}
n1 = n1.next;
n2 = n2.next;
}
n1 = n3.next;
n3.next = null;
while(n1 != null){
n2 = n1.next;
n1.next = n3;
n3 = n1;
n1 = n2;
}
return res;
}
题目二
将单向链表按某值划分成左边小、中间相等、右边大的形式
给定一个单链表的头节点head,节点的值类型是整型,再给定一个整数pivot。实现一个调整链表的函数,将链表调整为左部分都是值小于pivot的节点,中间部分都是值等于pivot的结点,右部分都是值大于pivot的节点
进阶:在实现原问题功能的基础上增加如下的要求
要求:调整后所有小于pivot的节点之间的相对顺序和调整前一样
要求:调整后所有等于pivot的节点之间的相对顺序和调整前一样
要求:调整后所有大于pivot的节点之间的相对顺序和调整前一样
要求:时间复杂度请达到O(N),额外空间复杂度请达到O(1)
public static Node listPartition2(Node head,int pivot){
//以下定义了三个区域,之后遍历完之后,会将这三个区域链接起来
Node sH = null;
Node sT = null;
Node eH = null;
Node eT = null;
Node mH = null;
Node mT = null;
Node next = null;//定义一个空节点
while(head != null){
next = head.next;//定义一个指针,从头到尾遍历比较
head.next = null;//将头节点和它的下一个节点断开,避免形成多指向
if(head.value < pivot){
if(sH == null){
//如果此时小于的区域没有数,将头尾指针都指向第一个添加进来的节点
sH = head;
sT = head;
}else{
sT.next = head;
sT = head;
}
}else if(head.value == pivot){
if(eH == null){
eH = head;
eT = head;
}else{
eT.next = head;
eT = head;
}
}else{
if(mH == null){
mH = head;
mT = head;
}else{
mT.next = head;
mT = head;
}
}
head = next;//将head节点下移一位
}
//进行连接每个区域,难点在于不知道哪个区域有值,所有要进行判断
if(sH != null){
sT.next = eH;//连接等于的区域,不管等于的区域有没有值
eT = eT == null ? sT : eT;//下一步,假如等于区域有值,则eT为等于区域的尾,如果等于区域没值,则eT为小于区域的sT
}
//上面的if,不管跑了没有,et
if(eT != null){//表示上面两个区域至少有一个
eT.next = mH;
}
return sH != null ? sH : (eH != null ? eH : mH);
}
题目3:一种特殊的单链表节点类描述如下
class Node{
int value;
Node next;
Node rand;
Node(int val){
value = val;
}
}
rand指针是单链表节点结构中新增的指针,rand可能指向链表中的任意一个节点,也可能指向null。给定一个由Node节点类型组成的无环单链表的头节点head,请实现一个函数完成这个链表的复制,并返回复制之后新链表的头节点
要求:时间复杂度O(N),额外空间复杂度O(1)
方法1:使用hash表的方式
//rand指针的
public static 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){
//cur 老
//map.get(cur)新
map.get(cur).next = map.get(cur.next);//将旧数据的next指针赋值给新数据
map.get(cur).rand = map.get(cur.rand);//将旧数据的rand指针赋值给新数据
cur = cur.next;//数据下移
}
return map.get(head);
}
方法2:不使用hash函数,将数据存在一个数组
//方法2
public static Node copyListWithRand2(Node head){
//链表不为null
if(head == null){
return null;
}
//定义一个节点,用于遍历的时候
Node cur = head;
Node next = null;//定义一个空节点,用于存储当前cur的下一个节点,一会断开
while(cur != null){
next = cur.next;//方便后面直接找到
cur.next = new Node(cur.value);//将cur的下一个变成和它一样的节点,重新给cur一个指向,此时断开了与上面的联系
cur.next.next = next;//原本是cur的下一个(也就是第二个节点),现在是下一个的下一个,也就是第三个节点
cur = next;//cur向下移动两个,也就是cur = cur.next.next
}
cur = head;//让当前的节点重新回到头结点
Node curCopy = null;//定义一个空节点,用于复制的时候使用
while(cur != null){
next = cur.next.next;//存储第三个节点,如果不事先存储,一会断开会找不到
curCopy = cur.next;//第二个节点,也就是和第一个一样的结点,需要被赋予和第一个一样的rand指针一样的东西
curCopy = cur.rand != null ? cur.rand.next : null;//赋予rand指针
cur = next;//指针移到第三个,接着循环
}
Node res = head.next;//将第二个节点变成结果的头
while(cur != null){
next = cur.next.next;//空节点next 表示此时的第三个节点
curCopy = cur.next;//空节点curCopy 表示此时的第二个节点
cur.next = next;//此时改变第一个节点指向为第三个节点,也就是变回原来的单链表
curCopy.next = next != null ? next.next : null;//赋予第二个节点指向,第四个节点或者为空
cur = next;//将指针移到第三个节点,接着循环
}
return res;//最后返回复制后的头结点
}
两个单链表相交的一系列问题
给定两个可能有环也可能无环的单链表,头节点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的第一个节点。如果不相交,返回null
要求:如果两个链表长度之和为N,时间复杂度请达到O(N),额外空间复杂度请达到O(1).
对于找入环的第一个节点
方法1:使用HashSet方法
①把链表添加进hashset
②添加进去之前,进行判断hashset里面是否有此节点
③有的话,不仅有环,而且此节点就是第一个节点
④否则,无环
代码省略
方法2:快慢指针法
①定义两个指针,都指向头
②快指针一次走两个节点,慢指针一次走一个节点
③如果有环,这俩指针一定会在环中相遇
④相遇的时候,慢指针不动,快指针指向头结点
⑤以后每次走一个节点,再次相遇的时候,一定是环的第一个节点
找到链表第一个入环节点,如果无环,返回null
//找到链表第一个入环节点,如果无环,返回null
public static Node getLoopNode(Node head){//传一个链表的头节点
//头节点不为空,头节点的下一个节点不为空,头节点下一个节点的下一个节点不为空
if(head == null || head.next == null || head.next.next == null){
return null;
}
Node n1 = head.next;//指向第二个节点
Node n2 = head.next.next;//指向第三个节点
while(n1 != n2){//如果两个节点相遇,证明有环
if(n2.next == null || n2.next.next == null){
return null;
}
n2 = n2.next.next;//快指针一次走两个
n1 = n1.next;//慢指针一次走一个
}
n2 = head;//快指针跑到头节点
while(n1 != n2){
n1 = n1.next;//慢指针一次走一个节点
n2 = n2.next;//快指针一次也走一个节点
}
return n1;
}
对于找相交的节点
如果两个链表都无环,返回第一个相交的节点,如果不相交,返回null
①两个链表找相交的节点的时候,会将指针指向同等地位
②举个例子,假如说一个链表长度10,另一个长度5
③相交的节点,一定是长链表5以后的节点
④让长链表先走到五位置,同时走,比较两个结点是否相同
⑤相同的时候,一定是相交,而且是第一个相交的节点
//如果两个链表都无环,返回第一个相交节点,如果不相交,返回null
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--;
cur1 = cur1.next;
}
if(cur1 != cur2){
return null;
}
cur1 = n > 0 ? head1 : head2;//谁长,谁的头变成cur1
cur2 = cur1 == head1 ? head2 : head1;//谁短,谁的头变成cur2
n = Math.abs(n);
while(n != 0){
n--;
cur1 = cur1.next;
}
while(cur1 != cur2){
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
}
题目的最终解法
1、可能的结构
2、解法
//有两个有环链表,返回第一个相交节点,如果不相交返回null
public static Node bothLoop(Node head1,Node loop1,Node head2,Node loop2){
Node cur1 = null;
Node cur2 = null;
if(loop1 == loop2){
//if里面的代码是进行相交节点的查找
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;
}
}