LinkedList算法题目

1:按照左右半区的方式重新组合单链表

难度:❤️

给定一个单链表的头部节点head,链表长度为N,如果N为偶数,那么前N/2个节点算作左半区,后N/2个节点算作右半区;如果N为奇数,那么前N/2个节点算作左半区,后N/2 + 1个节点算作右半区。左半区从左到右依次记为L1 -> l2 -> …,右半区从左到右依次记为R1 -> R2 ->…,请将单链表调整成L1 -> R1 -> L2 -> R2 -> …的形式。

1.架构

链表的分区域后,合并思想

2.笔记

1.解题思路

首先关注题目要求,找出其特点:

  • 将mid = head, right= head.next,将mid进行预处理:如果right每次向右移动两个节点且不为null,则mid右移一位。最终mid将指向左区域的尾结点,故mid+1为右区域首节点。

  • 将left(左区域首节点)和right(右区域首节点)进行merge时,须要使用一个临时变量(next),记录被断开的节点。

    • 第一次merge完,left和right需要重新赋值,直到left.next = null

      left = right.next right = next

每次找到左区域的头节点(left)和右区域的投节点(right),然后进行merge

merge的过程就是将left和right拼接

2.遇到的问题

  • 为什么next每次移动2,左区域长度就+1
  • 如何merge左右两个区域

3.我与大神的代码

代码:

public class Solution{

    public static void relocate(Node head){
        if (head == null || head.next == null) {
            return;
        }

        //划分左右两区域
        Node mid = head;
        Node right = head.next;
        while (right.next != null && right.next.next != null) {
            mid = mid.next;
            right = right.next.next;
        }

        //将左右两区域断开 用作合并完成 退出的条件
        mid.next = null;

        //合并左右两区域
        mergeLR(head, mid.next);
    }
    private static void mergeLR(Node left, Node right){
        //left和right分别代表左右区域首节点
        Node next = null;
        while (left.next != null) {
            next = right.next;
            right.next = left.next;
            left.next = right;

            left = right.next;
            right = next;
        }
        left.next = right;
    }
}

class Node{
    public int value;
    public Node next;

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

2:合并两个有序的单链表

难度:❤️

给定两个有序单链表的头节点head1和head2,请合并两个有序链表,合并后的链表依然有序,并返回合并后链表的头节点

1.架构

采用范围(指针)合并的思想

2.笔记

1.解题思路

2.遇到的问题

  • pre变量的作用
  • next变量的作用
  • cur1和cur2初始化的含义

3.我与大神的代码

代码:

public class Solution{

	public static Node merge(Node head1, Node head2){
		if (head1 == null || head2 == null) {
			return head1 != null ? head1 : head2;
		}

		//cur1总是指向头节点值小的那个  cur2则相反
		//这是为了保证第一次能进if (cur1.value <= cur2.value)分支
		Node head = head1.value <= head1 ? head1 : head2;
		Node cur1 = head == head1 ? head1 : head2;
		Node cur2 = head == head1 ? head2 : head1;
		Node pre = null;
		Node next = null;

		while (cru1 != null && cur2 != null) {
			if (cur1.value <= cur2.value) {
				pre = cur1;
				cur1 = cur1.next
			}
			else{
				next = cur2.next;
				cur2.next = cur1.next;
				cur1.next = cur2;

				pre = cur2;
				cur2 = next;
			}
		}

		pre.next = cur1 == null ? cur2 : cur1;

		return head;
	}
}

3:向有序的环形单链表中插入新节点

难度:❤️

一个环形单链表从头节点head开始不降序,同时由最后的结点指回头节点。给定这样一个环形单链表的头节点head和一个整数num,请生成节点值为num的新节点,并插入到这个环形链表中,保证调整后的链表依然有序。

1.架构

双指针确定插入的范围,然后合并

2.笔记

1.解题思路

插入过程分为2种情况

  1. 插入中间
  2. 插入到结尾
    1. num比所有值大
    2. num比所有值小

如果是2.1直接返回头节点,如果是2.2则须将插入的值作为新的头节点返回

2.遇到的问题

3.我与大神的代码

代码:

public class Solution{

	public static Node insertNum(Node head, int num){
		Node node = new Node(num);

		//如果head为空 则将自己指向本省
		if (head == null) {
			node.next = node;
			return node;
		}

		Node pre = head;
		Node cur = head.next;

		while (cur != head) {
			if (num >= pre.value && num <= cur.value) {
				break;
			}
			pre = cur;
			cur = cur.next;
		}

		//insert
		pre.next = node;
		node.next = cur;

		//is new head?
		return num > head.value ? head : node;
	}
}

class Node{
	public int value;
	public Node next;

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

总结:

到此为止,链表的遍历须涉及到两个重要变量,pre和next。pre代表cur前一个结点,next代表cur后一个结点

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

难度:❤️

1.解题思路

由于链表是有序的,所以只需同时遍历两个链表即可

2.遇到的问题

3.代码

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

public class Solution {
	public void printCommonPart(Node head1, Node head2) {
		System.out.println("Common part:");

		while (head1 != null && head2 != null) {
			if (head1.value > head2.value) {
				head2 = head2.next;
			}
			else if (head1.value < head2.value) {
				head1 = head1.next;
			}
			else {
				System.out.println(head1.value);
				head1 = head1.next;
				head2 = head2.next;
			}
		}
		System.out.println();

	}
}

4.收获

学会了链表的遍历方法

2.在单链表和双链表中删除倒数第K个节点

难度:❤️

1.解题思路

要想删除节点,必须要知道前一个结点(除了链表头节点)

观看视频

2.遇到的问题

  • 为什么第二次遍历, k = 0的位置是要删除节点的前一个节点的位置

3.代码

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

public class Solution {
	public Node removeLastKthNode(Node head, int k) {

		Node cur = head;

		//第一次遍历 k--
		while (cur != null) {
			k--;
			cur = cur.next;
		}

		//判断k
		if (k == 0) {
			return head.next;
		}


		if (k < 0) {
			//第二次遍历 k++ 找到要删除的节点的前一个节点
			cur = head;
			while (++k != 0) {
				cur = cur.next;
			}
			//k = 0
			//这里的cur表示的是倒数第k - 1个节点
			cur.next = cur.next.next;

		}
		return head;

	}

	//单链表换成双链表

	public Node removeLastKthNode(DoubleNode head, int k) {

		Node cur = head;

		//第一次遍历 k--
		while (cur != null) {
			k--;
			cur = cur.next;
		}

		//判断k
		if (k == 0) {
			head.last = null;
			return head.next;
		}


		if (k < 0) {
			//第二次遍历 k++ 找到要删除的节点的前一个节点
			cur = head;
			while (++k != 0) {
				cur = cur.next;
			}
			//k = 0
			//这里的cur表示的是倒数第k - 1个节点
			DoubleNode newNext = cur.next.next;
			cur.next = cur.next.next;
			//注意删除最后一个节点的情况
			if (newNext != null) {
				newNext.last = cur;
			}

		}
		return head;

	}
}

4.收获

学会了链表结点的遍历和删除操作

3.删除链表的中间节点和a/b处的节点

难度:❤️

1.解题思路

视频讲解

其中视频出现的错误:

  • k取整的表达式,必须先把a,b强转成double进行除法运算,然后最后将结果强转为整数

2.遇到的问题

3.代码

public class Solution {

    public Node removeMidNode(Node head) {
        if (head == null || head.next == null) {
            return head;
        }

        Node pre = head;
        if (pre.next.next == null) {
            return head.next;
        }

        Node cur = pre.next.next;

        while (cur.next != null && cur.next.next != null) {
            pre = pre.next;
            cur = cur.next.next;
        }
        //delete
        pre.next = pre.next.next;

        return head;
    }



    public Node removeByRatio(Node head, int a, int b) {

        if (head == null || a > b) {
            return head;
        }

        //求链表的长度N
        int n = 0;
        Node cur = head;
        while (cur != null) {
            n++;
            cur = cur.next;
        }

        //计算要删除第几个节点 向上取整
        int k = (int) Math.ceil(((double)a / (double)b) * n );

        if (k == 1){
            return head.next;
        }

        //找到该节点的前一个节点
        if (k > 1) {
            cur = head;
            while (--k != 1) {
                cur = cur.next;
            }
            //delete
            cur.next = cur.next.next;
        }
        return head;
    }
}

4.收获

学会了求链表的长度

删除节点一定要找到前一个节点

4.反转单向和双向链表

难度:❤️

1.解题思路

找到节点的前继节点和后继节点

2.遇到的问题

3.代码

public class Solution {
	public Node reverseLinkedList(Node head){

		Node pre = null;
		Node next = null;
		//head兼顾遍历变量的作用
		while (head != null) {
			next = head.next;
			head.next = pre;
			pre = head;
			head = next;
		}

		return pre;

	}

	public Node reverseLinkedList(Double head){

		Double pre = null;
		Double next = null;
		//head兼顾遍历变量的作用
		while (head != null) {
			next = head.next;
			head.next = pre;
			head.last = next;
			pre = head;
			head = next;
		}

		return pre;

	}
}

4.收获

学会了如何反转链表及注意到的事项

5.反转部分单向链表

难度:❤️

1.解题思路

视频讲解

2.遇到的问题

  • 部分链表反转后连接问题
  • 返回新头节点问题

3.代码

public class Solution {
	public Node reversePart(Node head, int from, int to) {

		Node cur = head;
		Node fPre = null;
		Node tPos = null;
		int n = 0;
		while (cur != null) {
			n++;
			fPre = from - 1 == n ? cur : fPre;
			tPos = to + 1 == n ? cur : tPos;
			cur = cur.next;
		}

		if (to < 1 || to > from ||from > n || head) {
			return head;
		}

		//对内部链表进行反转
		Node pre = null;
		Node next = null;
		cur = fPre == null ? head : fPre.next;
		Node temp = cur;

		while (cur != tPos) {
			next = cur.next;
			cur.next = pre;
			pre = cur;
			cur = next;
		}

		//将反转后的链表连接到原始链表

		//判断是不是新的头节点
		if (fPre == null) {
			temp.next = tPos;
			return pre;
		}

		fPre.next = pre;
		temp.next = tPos;

		return head;

	}
}

4.收获

巩固了链表的反转操作

6.环形单链表的约瑟夫问题

原问题:❤️

进阶问题:❤️❤️❤️

1.解题思路

  • 根据报数得到编号
    • B = (A - 1) % i + 1
    • 其中A代表报数(假设无穷大),B代表对应的编号,i表示当前环长度
  • 根据具体报数s得到i环中的编号和i - 1环中的编号关系
    • old = (new + s - 1) % i + 1
    • 其中old为i环对应的编号,new为i - 1环对应的编号,s为报数
  • 结合两式得
    • old = (new + m - 1) % i + 1
    • 该式子即为递归函数
    • 递归函数返回值是指,在最深的那层递归(i = 1),返回最后存活的节点在长度为1(i = 1)的环中的编号,然后依次返回上层递归,即返回存活节点在长度为2(i + 1)的环中的编号,在环i + 2…i + n中的编号,即找到了存活节点的编号

2.遇到的问题

  • 如何得到上述两个式子
  • 递归函数的作用

3.代码

public class Solution {

    //方法一:不停的删除报到m数的节点 最后返回存活的数
    publc Node josefphusKill1(Node head, int m) {
        if (head == null || head.next = head || m  < 1) {
            return head;
        }

        Node last = head;
        while (last.next != head) {
            last = last.next;
        }

        int count = 0;
        while (last != head) {
            if (++count == m) {
                last.next = head.next;
                count = 0;
            }

            else {
                last = head;
            }

            head = last.next;
        }
        return head;
    }


    //方法二:直接找到最后生存的节点
    publc Node josefphusKill2(Node head, int m) {
        if (head == null || head.next = head || m  < 1) {
            return head;
        }

        //求链表i的长度
        int i = 0;
        cur = head;
        while (cur != null) {
            i++;
            cur = cur.next;
        }

        //在i环中 节点的编号是old
        //在i - 1环中 节点的编号是new
        //可以得到递归函数(其中s为要删除的这样一个报数):old = (new + s - 1) % i + 1 
        int live = getLive(i, m);

        //找到存活的节点后 返回即可
        cur = head;
        while (--live != 0) {
            cur = cur.next;
        }
        cur.next = cur;
        return cur;
    }
    private int getLive(int i, int m){
        //base case: 只剩下i = 1个节点的环 该节点就是最终存活的
        if (i == 1) {
            return 1;
        }

        return ( getLive(i - 1, m) + m - 1) % i + 1
    }
}

4.收获

  • 函数表达式与递归函数结合
  • 编号问题的处理

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

原问题:❤️

进阶问题:❤️❤️

1.解题思路

根据回文的特点

  • ​ 从左往右读 = 从右往左读

此时有两个突破方向

    • 出栈等同于从右往左读
  • 反转局部链表
    • 也等同于从右往左读

2.遇到的问题

  • 如何找到链表的 N/2 处的节点
  • 如何将局部链表反转后恢复
    • 只需记住反转链表第一个节点的前一个结点,即mid的处位置

3.代码

public class Solution {

    //method-1:
    public boolean isPalindrome1 (Node head){
        //利用stack 比较压栈后出栈后的节点
        Stack<Node> stack = new Stack<>();

        //压栈
        Node cur = head;
        while(cur != cur) {
            stack.push(cur);
            cur = cur.next
        }

        //出栈
        cur = head;
        while (cur != null) {
            if (cur.value != stack.pop().value) {
                return false;
            }
        }

        return true;
    }

    //method-2:
    public boolean isPalindrome2 (Node head){
        if (head == null || head.next == null) {
            return true;
        }
        //利用stack 比较压栈后出栈后的节点
        Stack<Node> stack = new Stack<>();


        //寻找右半部分链表第一个节点 奇数则中间数不比较 然后压栈
        //不过只压入一半的链表
        Node right = head.next;
        Node cur = head;
        while (cur.next != null && cur.next.next != null) {
            right = right.next;
            cur = cur.next.next;
        }

        while (right != null) {
            stack.push(right);
            right = right.next;
        }

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



    //method-3:
    public boolean isPalindrome3 (Node head){
        if (head == null || head.next == null) {
            return true;
        }	

        //寻找中间节点  time:O(n) space:O(1)
        Node cur = head;
        Node mid = head;
        while (cur.next != null && cur.next.next != null) {
            mid = mid.next;
            cur = cur.next.next;
        }

        //反转右部分链表节点 time:O(n) space:O(1)
        Node pre = null;
        Node next = null;
        cur = mid.next;
        while (cur != null) {
            next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }
        mid.next = pre;

        //比较左 右部分链表节点
        Node left = head;
        Node right = mid.next;
        boolean res = true;
        while (right != null){
            if (left.value != right.value) {
                res = false;
                break;
            }
            right = right.next;
        }

        //恢复被反转的链表部分
        pre = null;
        next = null;
        cur = mid.next;
        while (cur != null) {
            next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }
        mid.next = pre;
    }

}

4.收获

学会寻找链表的2/N处的位置

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

难度:❤️❤️

1.解题思路

  • 原问题
    • 将问题扁平化为数组的快排问题
  • 进阶问题
    • 空间复杂度为O(1)

2.遇到的问题

  • 如何进行分区
  • 分区后如何连接

3.代码

public class Solution {

    public Node listPartition1 (Node head, int pivot){
        if (head == null) {
            return head;
        }

        //扁平化数组---快排问题

        //求链表长度
        int n = 0;
        Node cur = head;
        while (cur != null) {
            n++;
            cur = cur.next;
        }

        Node[] nArr = new Node[n];

        cur = head;
        for (int i = 0; i < n; i++) {
            nArr[i] = cur;
            cur = cur.next;
        }

        partition(nArr, pivot);

        for (int i = 1; i < n; i++) {
            nArr[i - 1].next = nArr[i];
        }

        nArr[i - 1].next = null;
        return nArr[0];
    }

    private void partition (Node[] nArr, int pivot){
        int small = -1;
        int big = nArr.length;
        int index = 0;

        while (index != big) {
            if (nArr[index] < pivot) {
                swap(nArr, ++small, index++);
            }
            else if (nArr[index] = pivot) {
                index++;
            }
            else {
                swap(nArr, index++, --big);
            }
        }
    }
    private void swap (Node[] nArr, int i, int j){
        Node temp = nArr[i];
        nArr[i] = nArr[j];
        nArr[j] = temp;
    }


    public Node listPartition2 (Node head, int pivot) {

        Node cur = head;
        Node next = null;

        Node smallHead = null;
        Node smallTail = null;
        Node equalHead = null;
        Node equalTail = null;
        Node bigHead = null;
        Node smallTail = null;


        //分为small equal big三个区域
        while (cur != null) {
            next = cur.next;
            cur.next = null; 

            if (cur.value < pivot) {

                if (smallHead == null) {
                    smallHead = cur;
                    smallTail = cur;
                }
                else {
                    smallTail.next = cur;
                    smallTail = smallTail.next;
                }
            }

            else if (cur.value == pivot) {
                if (equalHead == null) {
                    equalHead = cur;
                    equalTail = cur;
                }
                else {
                    equalTail.next = cur;
                    equalTail = equalTail.next;
                }
            }

            else {
                if (bigHead == null) {
                    bigHead = cur;
                    bigTail = cur;
                }
                else {
                    bigTail.next = cur;
                    bigTail = bigTail.next;
                }
            }
            cur = next;
        }

        //将三个区域连接起来
        if (smallTail != null) {
            smallTail.next = equalHead;

            //equalTail可能为空
            equalTail = equalTail == null ? smallTail : equalTail;
        }
        if (equalTail != null) {
            equalTail.next = bighead;
        }
        bigTail.next = null;

        return smallHead != null ? smallHead : equalHead != null ? equalHead : bigHead ;

    }
}

4.收获

学会了处理链表的分区问题

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

难度:❤️❤️

1.解题思路

  • 原问题
    • 时间复杂度为O(n)
    • 利用HashMap数据结构,key为原节点,value为复制原节点后的新节点
    • 利用HashMap很容易找到新节点的next节点和rand节点
  • 进阶问题
    • 时间复杂度围为O(1)
    • 利用将复制后的节点设置为原节点的next,这样也容易找到新节点的rand节点
      • 比如:
      • 原链表:1 -> 2 -> 3 -> null
      • 新链表:1 -> 1’ -> 2 -> 2’ -> 3 -> 3’ -> null

2.遇到的问题

  • 如何理解题意
    • 比如复制是什么含义
      • 新节点的next和rand不能指向原来的节点,只能指向新复制出来的节点
  • 如何找到新节点的next节点和rand节点

3.代码

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

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

public class Solution {

	//方法一  space: O(n)
	public Node copyListWithRand1 (Node head){
		if (head == null) {
			return head;
		}

		HashMap<Node, Node> map = new HashMap<>();

		Node cur = head;
		while (cur != null) {
			map.put(cur, new Node(cur.value));
			cur = cur.next;
		}

		//设置next和rand
		cur = head;
		while (cur != null) {
			map.get(cur).next = map.get(cur.next);
			map.get(cur).rand = map.get(cur.rand);
		}

		return map.get(head);
	}

	//方法二  space: O(1)
	public Node copyListWithRand2 (Node head){
		if (head == null) {
			return head;
		}


		//将每个节点的副本添加为节点的next
		Node cur = head;
		Node next = null;
		while (cur != null) {
			next = cur.next;
			cur.next = new Node(cur.value); 
			cur.next.next = next;
			cur = next;
		}

		//接下来关键找到新节点的rand(也为新节点)
		cur = head;
		next = null;
		Node curCopy = null;
		while (cur != null) {
			next = cur.next.next;
			curCopy = cur.next;
			curCopy.rand = cur.rand != null ? cur.rand.next : null;
			cur = next;
		}

		//拆分链表
		cur = head;
		next = null;
		curCopy = null;
		Node res = head.next;
		while (cur != null) {
			next = cur.next.next;
			curCopy = cur.next;
			cur.next = next;
			curCopy.next = next != null ? next.next : null;
			cur = next;
		}

		return res;

	}

}

4.收获

对陌生数据结构的处理,主要关注新的属性,本题就是rand,关键在于如何找到新节点的next和rand

10.两个单链表生成相加链表

1.解题思路

  • 方法一

    • 遍历两个链表,将他们的节点组成数字,然后进行相加,更具相加得到的数生成新的链表
    • 但是这里可能存在溢出问题(如果链表长度很长),所以此方法并不推荐
  • 方法二

    • 利用栈,将链表中的节点压栈,出栈时就将节点值相加(即个位数相加),然后生成新的节点
    • 这种方法要注意以下几点
      • 个位数相加存在进位
      • 进位数不可能 大于1
      • 生成新节点要和前一个节点连接
  • 方法三

    • 方法二是通过栈来进行”逆序“操作,但如果将链表进行逆序,就可以减少栈的空间

2.遇到的问题

  • 新节点之间的连接问题

3.代码

public class Solution {

    public Node addList1(Node head1, Node head2) {
        if (head1 == null || head2 == null) {
        }

        //压栈 进行尾数相加
        Stack<Integer> stack1 = new Stack<>();
        Stack<Integer> stack2 = new Stack<>();

        Node cur = head1;
        while (cur != null) {
            stack1.push(cur.value);
            cur = cur.next;
        }

        cur = head2;
        while (cur != null) {
            stack2.push(cur.value);
            cur = cur.next;
        }

        Node node = null; //新建节点
        Node pre = null; //前一个节点 用于连接新节点
        int n = 0; //当前新节点值
        int n1 = 0;	//第一个链表节点值
        int n2 = 0; //第二个链表的节点值
        int ca = 0; //进位数

        while (!stack1.isEmpty() || !stack2.isEmpty()) {
            pre = node;

            n1 = !stack1().isEmpty() ? 0 : stack1().pop;
            n2 = !stack2().isEmpty() ? 0 : stack2().pop;

            n = n1 + n2 + ca;
            node = new Node(n % 10);

            ca = n / 10;

            node.next = pre;

        }

        //进位不可能超过2
        if (ca == 1) {
            //最后还要生成进位节点
            pre = node;
            node = new Node(1);
            node.next = pre;
        }

        return node;

    }

    public Node addList2(Node head1, Node head2) {

        head1 = reverseList(node1);
        head2 = reverseList(node2);

        Node c1 = head1;
        Node c2 = head2;

        Node node = null;
        Node pre = null;

        int n = 0;
        int n1 = 0;
        int n2 = 0;
        int ca = 0;


        while (c1 != null || c1 != null) {


            n1 = c1 != null ? c1.value : 0;
            n2 = c2 != null ? c2.value : 0;
            n = n1 + n2 + ca;

            pre = node;
            node = new Node(n % 10);
            node.next = pre;

            c1 = c1 != null ? c1.next : null;
            c2 = c2 != null ? c2.next : null;

            ca = n / 10;

        }

        if (ca == 1) {
            pre = node;
            node = new Node(ca);
            node.next = pre;
        }

        reverseList(head1);
        reverseList(head2);

        return node;

    }


    private Node reverseList(Node head) {
        Node pre = null;
        Node next = null;
        Node cur = head;

        while (cur != null) {
            next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }

        return pre;
    }

}

4.收获

逆序(反转)链表的两种方式:

​ 1.利用栈

​ 2.对链表本身进行逆序 不过最后要将原链表恢复

11.两个单链表相交的一系列问题

什么是单链表相交?

单链表相交就是链表共享某一连续部分,并且持续到尾结点end(但是尾结点可能进入循环)

1.解题思路

  • 先判断两个链表的类型
    • list1
      • 无环
      • 有环
    • list2
      • 无环
      • 有环
  • 无环-无环
    • 该情况只需判断end节点是否相等,不相等直接返回null,相等则继续往前找
  • 无环-有环
    • 这种情况必不共享部分,因为如果共享,则有环的情况,两个链表的end部分必有环
  • 有环-无环
    • 同上
  • 有环-有环
    • 判断两个链表进入环的第一个节点是否相等(实际上也是判断end节点是否相等)
    • 如果相等,则需往前找第一个相同的节点,如果不相等,则需往环里找到第一个相同的节点

2.遇到的问题

  • 题意 ”相交部分“指的时哪里

  • n可能为负值

    • 所以要用n = Math.abs(n)
  • getLoopNode()函数的调用时机

3.代码

public class Solution {


	//如果有结尾节点 则只需判断结尾节点
	//如果没有结尾节点 即结尾部分是个环形链表 则将进入环形链表的第一个节点当作结尾节点 然后比较结尾节点是否相等
	//总之还是比较结尾节点
	public Node getLoopNode (Node head){
		if (head == null || head.next == null || head.next.next == null) {
			return null;
		}

		Node slow = head; //慢指针每次移动一个节点
		Node fast = head; //快指针每次移动两个节点
		while (fast != slow) {

			if (fast.next == null || fast.next.next == null) {
				//如果无环 fast首先到终点
				return null;
			}

			slow = slow.next;
			fast = fast.next.next;
		}

		//此时fast与slow相遇:
		//将fast设为head 每次移动一个节点 fast与slow再一次相遇的节点则为进入环的第一个节点
		fast = head;
		while (fast != slow) {

			slow = slow.next;
			fast = fast.next;
		}

		return fast;

	}

	public Node noLoop(Node head1, Node head2) {
		if (head1 == null || head2 == null) {
			return null;
		}

		int n = 0;
		Node c1 = head1;
		Node c2 = head2;
		while (c1 != null) {
			n++;
			c1 = c1.next;
		}

		while (c2 != null) {
			n--;
			c2 = c2.next;
		}

		//end1 != end2 说明无共享部分
		if (c1 != c2) {
			return null;
		}

		c1 = n > 0 ? head1 : head2;
		c2 = c1 != head1 ? head1 : head2;

		Math.abs(n);

		while (n != 0) {
			n--;
			c1 = c1.next;
		}

		//第一次相等的节点
		while (c1 != c2) {
			c1 = c1.next;
			c2 = c2.next;
		}

		return c1;

	}

	public Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) {

		Node c1 = null;
		Node c2 = null;

		//结尾节点相等的话 则要往前找共享的第一个节点
		if (loop1 == loop2) {

			c1 = head1;
			c2 = head2;
			int n = 0;
			
			while (c1 != loop1) {
				n++;
				c1 = c1.next;
			}
			while (c2 != loop2) {
				n--;
				c2 = c2.next;
			}

			if (c1 != c2) {
				return loop1;
			}

			cur1 = n > 0 ? head1 : head2;
			cur2 = cur1 != head1 ? head1 : head2;

			n = Math.abs(n);

			while (n != 0) {
				n--;
				cur1 = cur1.next;
			}

			while (cur1 != cur2) {
				cur1 = cur1.next;
				cur2 = cur2.next;
			}

			return cur1;
		}

		//结尾节点不相等 则往环里寻找第一个相等的节点
		else {
			c1 = loop1.next;
			while (c1 != loop1) {
				if (c1 == loop2) {
					return loop1;
				}
				c1 = c1.next;
			}
			return null;
		}
	}

	public Node getIntegersectNode(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;
	}

}

4.收获

了解了有环链表和无环链表的特点,并学会求进入环形链表的第一个节点

慢指针、快指针操作有环链表

12.将单链表的每K个节点之间逆序

1.解题思路

  • 利用栈将链表反转并拼接
  • 直接对链表本身进行操作

2.遇到的问题

  • 如何拼接反转后的局部链表
    • 增加辅助变量left和right,还要注意拼接第一组的链表

3.代码

public class Solution {

	public Node reverseKNodes1(Node head, int K) {
		if (head == null || k < 2) {
			return head;
		}

		Stack<Node> stack = new Stack<>();

		Node newHead = head;

		Node cur = head;
		Node pre = null;
		Node next = null;

		while (cur != null) {

			next = cur.next;
			stack.push(cur);
			if (stack.size() == K) {
				pre = resign1(stack, pre, next);
				//change head
				newHead = newHead == head ? cur : newHead;
			}

			cur = next;
		}
		return newHead;

	}

	//left代表前一组的最后一个节点 
	//right代表后一组的第一个节点
	//返回值表示返回逆序后该组链表的最后一个节点
	private Node resign1(Stack<Node> stack, Node left, Node right) {
		Node cur = stack.pop();
		Node next = null;

		//连接前一组 null
		if (left != null) {
			left.next = cur;
		}
		
		while (!stack.isEmpty()) {
			next = stack.pop();

			cur.next = next;
			cur = next;
		}
		//连接后一组
		cur.next = right;

		return cur;
	}

	public Node reverseKNodes2(Node head, int K) {
		if (head == null || k < 2) {
			return head;
		}

		int count = 1; //
		Node cur = head;
		Node pre = null;
		Node next = null;
		Node start = null;

		while (cur != null) {
			next = cur.next;

			if (count == K) {
				start = pre == null ? head : pre.next;
				head = pre == null ? cur : head; //
				resign2(pre, start, cur, next);
				pre = cur;
				// start = next; //
				count = 0; //
			}

			count ++;
			cur = next;
		}
		return head;

	}

	//left:前一组的最后一个节点
	//right:后一组的第一个节点
	//start:待逆序组的第一个节点
	//end:待逆序组的最后一个节点
	private void resign2(Node left, Node start, Node end, Node right) {
		Node pre = start;
		Node cur = start.next;
		Node next = null;

		while (cur != right) {
			next = cur.next;

			cur.next = pre;

			pre = cur;

			cur = next;
		}

		if (left != null) {
			left.next = end;
		}

		start.next = right;

	}

}

4.收获

加强了链表局部反转(逆序)的一些细节操作,如局部反转需从第2个节点开始连接第1个节点,最后将第一个节点的next指向下一组的第一个节点。需要2个变量完成拼接,left和right

13.删除无序单链表中值重复出现的节点

要求:

方法一:时间复杂度为O(n)

方法二:空间复杂度为O(1)

1.解题思路

  • 方法一
    • 未限制空间复杂度,可以使用HashSet数据结构(因为存储的是无重复元素)
  • 方法二
    • 未限制时间复杂度,可以使用O(n^2)的解法
      • 思路就是第一次遍历第一个节点,然后在剩余节点中删除重复出现的节点,然后遍历第二个节点,在剩余节点中删除重复出现的节点…

2.遇到的问题

  • 为什么想到使用HashSet数据结构
  • 方法二的遍历及删除细节操作

3.代码

public class Solution { 

	public void removeRep1(Node head) {
		if (head == null) {
			return;
		}

		HashSet<Integer> set = new HashSet<>();

		Node cur = head;
		Node pre = null;
		while (cur != null) {

			if (set.contains(cur.value)) {
				pre.next = cur.next;
			}
			else {
				set.add(cur.value);
				pre = cur;
			}
			cur = cur.next;
		}
	}

	public void removeRep2(Node head) {
		if (head == null) {
			return;
		}

		Node target = head;
		Node cur = null;
        
		while (target != null) {
			cur = target.next;
			pre = target;
			while (cur != null) {
				if (cur.value == target.value) {
					pre.next = cur.next;
				}
				else {
					pre = cur;
				}
				cur = cur.next;
			}
			target = target.next;
		}

	}
}

4.收获

双遍历链表并删除节点的细节代码操作,pre从head开始

HashSet的用法

14.在单链表中删除指定值的节点

1.解题思路

  • 方法一
    • 利用栈,将等于指定值的节点不入栈
  • 方法二
    • 对链表本身操作,正常删除指定节点

2.遇到的问题

  • pre从null开始的遍历删除操作代码如何编写

3.代码

public class Solution { 

	//方法一
	public Node removeValue1(Node head, int num) {
		if (head == null) {
			return head;
		}

		Stack<Node> stack = new Stack<>();

		while (head != null) {

			if (head.value != num) {
				stack.push(head);
			}
			head = head.next;
		}

		while (!stack.isEmpty()) {
			stack.peek().next = head;
			head = stack.pop();
		}
		return head;
	}

	//方法二
	public Node removeValue2(Node head, int num) {
		if (head == null) {
			return head;
		}

        //头节点没有判断到
		Node cur = head.next;
		Node pre = head;

		while (cur != null) {

			if (cur.value == num) {
				pre.next = cur.next;
			}
			else {
				pre = cur;
			}

			cur = cur.next;
		}

		return head.value == num ? head.next : head;
	}
}

4.收获

灵活利用栈的特性;

删除链表中的指定值,pre从null开始

15.将搜索二叉树转换成双向链表

1.解题思路

观看视频

  • 方法一
    • 利用队列,将BST的中序遍历节点添加到队列
    • 然后遍历队列 拼接双向队列
  • 方法二
    • 递归二叉树,递归函数作用是拼接左右孩子到父节点,返回值为链表的尾结点
    • 这里的尾节点进行了特殊处理,原来的尾结点的next是指向null,这里指向头节点,便于找到头节点

2.遇到的问题

  • 递归函数如何设计
  • 递归函数的流程(作用)
  • 递归函数的返回值
  • 为什么使用后序遍历
    • 因为要将父节点与左右孩子连接,所以必须要知道左右孩子(节点)

3.代码

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

public class Solution {
	public Node convert1(Node head) {
		if (head == null) {
			return head;
		}

		Queue<Node> queue = new Queue<>();
		inOrderToQueue(queue, head);

		head = queue.poll();

		Node cur = head;
		Node pre = head;
		//头节点的left为null
		pre.left = null;
		while (!queue.isEmpty()) {
			cur = queue.poll();
			pre.right = cur;
			cur.left = pre;
			pre = cur;
		}
		//尾结点的right为null
		pre.right = null;
		return head;

	}
	private void inOrderToQueue(Queue<Node> queue, Node head) {
		if (head == null) {
			return;
		}
		inOrderToQueue(queue, head.left);
		queue.offer(head);
		inOrderToQueue(queue, head.right);
	}

	public Node convert2(Node head) {

		if (head == null) {
			return head;
		}

		Node temp = process();
		head = temp.right;
		temp.right = null;
		return head;
	}
	private Node process(Node head) {
		if (head == null) {
			return null;
		}

		Node leftE = process(head.left);
		Node rightE = process(head.right);

		//因为尾结点进行了特殊处理: 将尾结点的right指向头节点 于是可快速的找到头节点
		Node leftS = leftE == null ? null : leftE.right; 
		Node rightS = rightE == null ? null : rightE.right;

		if (leftE != null && rightE != null) {
			//形成新的链表 需进行拼接
			leftE.right = head;
			head.left = leftE;
			head.right = rightS;
			rightS.left = head;

			//将新链表的尾结点的right指向头节点 并返回新链表的尾结点
			rightE.right = leftS;
			return rightE;
		}
		else if (leftE != null) {
			//右孩子为空
			leftE.right = head;
			head.left = leftE;

			//将新链表的尾结点(head)的right指向头节点 并返回head
			head.right = leftS;
			return head;
		}
		else if (rightE != null) {
			head.right = rightS;
			rightS.left = head;

			rightE.right = head;
			return rightE;
		}
		else {
			head.right = head;
			//返回end节点
			return head;
		}
	}
}

4.收获

链表与二叉树结合的问题;

二叉树递归函数的设计;

对链表的尾结点进行特殊的技巧操作

16.单链表的选择排序

1.解题思路

观看视频

2.遇到的问题

  • 每次循环cur如何赋值
  • 如果getSmallestPre方法的参数中,head就是该链表中最小的节点,那么又该如何删除
    • 头节点是无法删除的,因为头节点没有前继节点(preNode = null)

3.代码

public class Solution {

	public Node listSelection(Node head) {

		if (head == null) {
			return head;
		}

		Node cur = head;
		Node small = null; //未排序链表中值最小的节点
		Node smallPre = null; //small前一个结点
		Node tail = null; //排序链表中的尾结点
		while (cur != null) {

			small = cur;
			smallPre = getSmallestPre(cur);

			if (smallPre != null) {
				small = smallPre.next;
				smallPre.next = small.next;
			}
			cur = cur == small ? cur.next : cur;

			if (tail == null) {
				head = small; 
			}
			else {
				tail.next = small;
			}
			tail = small;

		}

		return head;

	}

	//删除最小结点必须找到该节点的前一个节点
	//在以head为头节点的链表中寻找
	private void getSmallestPre(Node head) {

		Node small = head;
		Node smallPre = null;
		Node cur = head.next;
		Node pre = head;
		while (cur != null) {

			if (cur.value < small.value) {
				small = cur;
				smallPre = pre;
			}

			pre = cur;
			cur = cur.next;
		}
		return smallPre;
	}

}

4.收获

选择排序的思想;

链表中找最小值并删除

链表总结篇

通过这20几道的算法题,可以将链表类算法题目进行如下总结:

  1. 链表的结构

    1. 拆分
      • 将链表拆分成多个链表
    2. 整合
      • 将多个链表整合
    3. 反转
      • 将链表(或局部)进行反转
  2. 链表的遍历

    1. 链表的长度
    2. 寻找最小值节点
  3. 链表的局部操作

    1. 删除节点
    2. 增加节点
    3. 链表排序
  4. 链表的类型

    1. 直链
    2. 环链
  5. 链表的操作方法

    1. 双指针

      • pre

      • cur

        或者

    2. 针对环形链表

      • last

      • head

        或快慢指针

      • fast

      • low

        快慢指针用于判断有无环或者进入环的第一个节点

总之,链表的操作细节麻烦之处在于链表的连接,写这部分代码时必须仔细

你可能感兴趣的:(程序员代码面试指南,链表,算法,数据结构,java)