Java链表练习题小结
链表(Linked List)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。一个链表节点至少包含一个 数据域和一个指针域在Java中需要定义一个类来实现,如下:
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 void add(int newVal){
ListNode newNode=new ListNode(newVal);
if(this.next==null){
this.next=newNode;//递归调用
}
else this.next.add(newVal);
}
//打印链表
public void Print(){
System.out.print(this.val);
if (this.next!=null){
System.out.print("-->");
this.next.Print();//递归调用
}
}
}
由于不必须按顺序存储,链表在插入的时候可以达到 O(1)的复杂度,比另一种线性表 —— 顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要 O(n) 的时间,而顺序表相应的时间复杂度分别是O(log n) 和 O(1)。
链表插入删除快,但是访问慢。
使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。
在计算机科学中,链表作为一种基础的数据结构可以用来生成其它类型的数据结构。链表通常由一连串节点组成,每个节点包含任意的实例数据(data fields)和一或两个用来指向上一个/或下一个节点的位置的链接(links)。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的访问往往要在不同的排列顺序中转换。而链表是一种自我指示数据类型,因为它包含指向另一个相同类型的数据的指针。
链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同的类型:单向链表,双向链表以及循环链表。
链表通常可以衍生出循环链表,静态链表,双链表等。对于链表使用,需要注意头结点的使用。
Q160
求两个链表headA和headB的交点。
方法:不能直接对两个链表进行操作,必须分别定义两个指针指向这两个链表,找交点。
由于不知道两个链表的具体长度。所以可以考虑,当两个指针不同的时候就不断挪动指针,一旦某一个遇到末尾,就将其更新到另外一个链表的头部去,这样就实现了循环比较,便不需要单独去处理两个链表的长度的各种情况了。
public static ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode l1 = headA, l2 = headB;
while (l1 != l2) {
l1 = (l1 == null) ? headB : l1.next;
l2 = (l2 == null) ? headA : l2.next;
}
return l1;
}
Q206
反转一个链表。有两种方法。
1.头插法,不断迭代做
//迭代法,将指针顺序颠倒即可,但第一个节点前没有节点,需要新建一个prev
public static ListNode reverseList1(ListNode head) {
ListNode prev=null;
ListNode curr=head;
//分别定义当前节点前一个节点
while(curr!=null){
ListNode next=curr.next;
curr.next=prev;
prev=curr;
curr=next;//修改顺序即可
}
return prev;
}
通过修改链表中指针的指向即可。需要定义一个指针指向当前的头,一个前驱指针,预先为null。当前的指针不为空,则不断迭代实现,最后,curr为null,返回prev即可。
2.递归法
要反转整个链表,先反转链表最后的节点,然后不断递归。
public static ListNode reverseList2(ListNode head){
//递归终止条件
if(head==null||head.next==null){return head;}
//不断递,直到最后一个节点开始返回
ListNode newHead=reverseList2(head.next);
//未到达最后的节点前的处理
head.next.next=head;
head.next=null;
return newHead;
}
Q21
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
public static ListNode mergeTwoLists(ListNode l1,ListNode l2){
ListNode prehead = new ListNode(-1);
ListNode prev = prehead;//用一个新的链表来指示移动。一个输出。哑节点
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
prev.next = l1;
l1 = l1.next;
} else {
prev.next = l2;
l2 = l2.next;
}
//每次挪一位
prev = prev.next;
}
// 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
prev.next = l1 == null ? l2 : l1;
return prehead.next;
}
Q83
存在一个按升序排列的链表,给你这个链表的头节点 head
,请你删除所有重复的元素,使每个元素 只出现一次 。
返回同样按升序排列的结果链表。
Java代码如下
public static ListNode deleteDuplicates(ListNode head) {
if(head==null){return head;}
//首先处理空的情况
ListNode p=head;//新建一个指针节点指向head;
while(p.next!=null){//当前节点的下一个不是null时移动。
if(p.val==p.next.val){
p.next=p.next.next;//相等时,往后挪
}
else p=p.next;
}
return head;
}
Q19
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。删除指定位置的链表都可以用这个类似的方法
方法1:容易想到,要删除倒数第n个节点,找到这个节点的直接前驱即可。
public static ListNode removeNthFromEnd1(ListNode head,int n) {
//已经规定了链表长度至少为1;
/*需要使用节点的前驱的时候,在此题中需要删除节点,就必须要直到其前驱,所以
哑节点很有用处
*/
ListNode dummy=new ListNode(0,head);
ListNode p=dummy;
int sz=size(head);
for(int i=1;i<sz+1-n;i++){
p=p.next;
}
//找到待删除节点的前驱,修改其指针即可
p.next=p.next.next;
ListNode ans=dummy.next;
return ans;//返回即可
}
public static int size(ListNode head){
int len=0;
while (head!=null){
len++;
head=head.next;
}
return len;
}
方法2:倒数第n个节点,有逆序的存在,考虑使用栈放置所有的节点,然后弹出节点,直到待删除的元素,将其删除即可。
public static ListNode removeNthFromEnd2(ListNode head,int n){
Deque<ListNode> stack =new LinkedList<ListNode>();
ListNode dummy=new ListNode(0,head);
ListNode p=dummy;
//所有元素入栈。
while(p!=null){
stack.push(p);
p=p.next;
}
for (int i = 0; i < n; i++) {
stack.pop();
}
//prev是待删除节点的前驱
ListNode prev=stack.peek();//peek弹出栈顶元素但是不删除
prev.next=prev.next.next;
ListNode ans=dummy.next;
return ans;
}
方法3:方法3:方法1中需要求出链表长度,耗费空间,可以不用求其长度也可以找到目标点 使用双指针方法:first和second。first超前second指针n,当first到达最后 second就恰好到达目标节点。
代码如下:
public static ListNode removeNthFromEnd3(ListNode head,int n){
//构造两个指针,f1用于指向头部,f2指向待删除节点的前驱
ListNode dummy=new ListNode(0,head);
ListNode f1=head;
ListNode f2=dummy;
//让第一个指针超前第二个n个距离
for (int i = 0; i <n ; i++) {
f1=f1.next;
}
//遍历,结束后f2指向的是待删除的前驱
while (f1!=null){
f1=f1.next;
f2=f2.next;
}
f2.next=f2.next.next;
ListNode ans=dummy.next;
return ans;
}
三种方法核心都是要找到待删除节点的直接前驱,核心就是找到他。双指针运算速度最快。
Q24
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
public static ListNode swapPairs(ListNode head){
ListNode dummy=new ListNode(0,head);
ListNode p=dummy;//p最开始指向哑节点
//创建哑节点,指针p指向当前节点。由于是相邻节点间进行两两交换必须创建指针表示,n1,n2为相邻的待交换节点
//考虑到最后可能没有剩的节点或可能仅有一个节点剩下,循环结束条件如下
while(p.next!=null&&p.next.next!=null){
ListNode N1=p.next;
ListNode N2=p.next.next;
p.next=N2;
N1.next=N2.next;
N2.next=N1;
p=N1;//进行交换,每次更新p指向交换后的后一个节点
}
return dummy.next;//返回值
}
Q445
给你两个非空链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。假设除了数字0之外,这两个数字都不会以零开头。
方法:做加法时,我们从末尾开始算起,但是链表第一个节点是最高位,所以考虑用两个栈存储两个链表的每一位元素,弹出栈顶元素进行加减。需要的变量有两个栈,每次弹出节点的两个值a,b。进位carry,每个位计算的结果cur。
代码:
public static ListNode addTwoNumbers(ListNode l1, ListNode l2) {
//新建两个存储整数的链式栈。
Deque<Integer> stack1=new LinkedList<Integer>();
Deque<Integer> stack2=new LinkedList<Integer>();
//值压入栈
while (l1!=null){
stack1.push(l1.val);
l1=l1.next;
}
while (l2!=null){
stack2.push(l2.val);
l2=l2.next;
}
//
int carry=0;
ListNode ans=null;
//注意循环条件,两个栈都不空时,要进行,且进位数也不为空时需要进位入循环进行计算。
while(!stack1.isEmpty()||!stack2.isEmpty()||carry!=0){
int a=stack1.isEmpty()?0:stack1.pop();
int b=stack2.isEmpty()?0:stack2.pop();
int cur=a+b+carry;
carry=cur/10;//进位数
cur=cur%10;//得到这一位的数
//新建节点,第一个结果是个位的数,以此类推
ListNode curNode=new ListNode(cur,ans);
ans=curNode;//更新,让ans指向当前的curNode,结束结算后是最高位。输出ans即可。
}
return ans;
}
Q 234
判断一个链表是不是回文链表
方法:将链表中的值全部放进数组中,然后用双指针一个指向头,另外一个指向尾,进行判断。
public static boolean isPalindrome(ListNode head) {
//放到数组中来进行
int[] num=new int[size(head)];
for (int i = 0; i < num.length; i++) {
num[i]= head.val;
head=head.next;
}
//双指针处理,奇数个恰好剩最后中间一个,偶数也可以完全比较完成
int front=0;
int back= num.length-1;
while (front<back){
if(num[front]!=num[back]){
return false;
}
front++;
back--;
}
return true;
}
public static int size(ListNode head){
int len=0;
while (head!=null){
len++;
head=head.next;
}
return len;
}
Q725
给定一个头结点为 root 的链表, 编写一个函数以将链表分隔为 k 个连续的部分。
每部分的长度应该尽可能的相等: 任意两部分的长度差距不能超过1,也就是说可能有些部分为 null。
这k个部分应该按照在链表中出现的顺序进行输出,并且排在前面的部分的长度应该大于或等于后面的长度。
返回一个符合上述规则的链表的列表。
方法:设链表长度为N,分割方法为:将N长的链表分为N/k长的段,前N%k段长度为N/k+1
代码:
public static ListNode[] splitListToParts(ListNode root, int k) {
//指针指向头
ListNode cur = root;
//N为链表的长度
int N = 0;
while (cur != null) {
cur = cur.next;
N++;
}
//每一段的基础宽度,rem为余数,前rem段的长度为width+1;
int width = N / k, rem = N % k;
//用一个链表数组存储k个链表段
ListNode[] ans = new ListNode[k];
cur = root;
//有k个段
for (int i = 0; i < k; i++) {
ListNode head = new ListNode(0);//待写入的链表
ListNode write=head;//定义当前待写入的节点的指针
for (int j=0; j<width+(i<rem?1:0);j++) {
write.next=new ListNode(cur.val);
write=write.next;//插入节点
if(cur!=null)cur=cur.next;//cur更新为下一个。
}
ans[i] = head.next;
}
return ans;
}
Q328
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
方法概述:将偶数链表接到奇数链表末尾即可。奇数链表的头为head,偶数新建一个为evenhead
代码:
public static ListNode oddEvenList(ListNode head) {
//先判断特殊情况,空时直接返回
if(head==null){
return head;
}
//定义奇数部分指针Odd,偶数指针,和偶数链表的头。
ListNode odd=head;
ListNode even=head.next;
ListNode evenhead=even;
//注意循环条件,原链表为奇数个时候,是even==null,偶数时,even.next==null终止
while(even!=null&&even.next!=null){
odd.next=odd.next.next;
odd=odd.next;
even.next=even.next.next;
even=even.next;
//循环体内进行修改指针
}
odd.next=evenhead;//evenhead接入到odd下
return head;
}