回归正题,今天的主题是链表反转。顾名思义,就是将链表进行反转,通俗的来讲就是将正过来的变成反过来的。
不知名网友:直接上代码不就行了。
我:有道理啊!好吧,大早上起来上班,身体是不愿意的,脑子是昏昏的。
输入:
{ 1, 5, 6, 9, 7, 8 }
输出:
{ 8, 7, 9, 6, 5, 1 }
解题思路:我们既然要解题,我们首先要理解题目,理解题目过后,我们要思考我们要做什么才能解决题目? 那我们又要采取什么技术来达到我们想做的呢? 这些步骤明确了,那我们也就自然的把问题解决了。
现在我们题目已经理解了,我们需要做的就是将之前指向下一个节点反过来指向上一个节点。那我们又要采取什么方法来实现这个目的呢?
实现:既然这个题目是链表的题目,那我们首先想到的肯定是使用链表的方式,除此之外,思考一下其他的常用的数据结构和算法,发现利用栈应该也可以。
1、链表
1.1头插法
既然使用链表,我们最容易想到的是创建一个新的链表newlist,然后遍历原来的链表,将当前的节点利用我们之前讲过的头插法插入到newlist中。最后得到的newlist就是反转后的链表。
重点: 我们需要的哪些节点的信息?我们直接将当前节点插入行不行?
我们需要当前节点的信息curr,和下一节点的信息curr.next。直接插入是不行的,试想一下,假如你直接将当前节点curr插入newlist中,那curr.next就是newlist中的,那你再想获取原本链表中的next节点怎么办?因此我们要在每次插入的时候保存curr.next。(因为采用头插的方法,我们也叫头插法)
代码:
public static ListNode reverseListByDummyNotCreate(ListNode head) {
ListNode ans = new ListNode(-1); 创建虚拟节点
ListNode cur = head;
while (cur != null) {
ListNode next = cur.next; //保存当前节点的下一个节点信息
cur.next = ans.next; //将当前节点利用头插法插入到新链表中
ans.next = cur;
cur = next; //继续原节点的遍历
}
return ans.next;
}
1.2 穿针引线法(重要!!)
有网友就讲了,你这直接反转不就行了为什么还要弄一个新的链表,这位网友说的很好。我很赞同,那么接下来我们就来看一下穿针引线法。(处理方式类似于穿针引线)
重点: 我们需要的哪些节点的信息?我们直接将当前节点指向反转后的头结点行不行?
这种方法我们还是需要三个节点信息,curr当前节点,curr.next当前节点的下一个节点,prev反转后的链表的头节点。 那肯定不行呀,你看上一种解题思路,那你当前节点的下一个节点不就获取不到了。因此我们要将curr.next存储下来。
代码:
public static ListNode reverseListSimple(ListNode head) {
ListNode prev = null; //创建反转后的头结点
ListNode curr = head;
while (curr != null) {
ListNode next = curr.next; //保存当前节点的下一个节点信息
curr.next = prev; ///当前节点指向反转链表的头节点
prev = curr; //反转后的链表的头结点为当前节点
curr = next; //继续原链表的遍历
}
return prev;
}
2、栈
我们都知道栈的特点是:先入后出!那我们是不是可以将原链表中的节点进行入push入栈处理,之后再pop()取栈顶元素直至栈为空实现链表的反转。
代码:
public ListNode ReverseList(ListNode head) {
Stack<ListNode> stack = new Stack<>(); // 栈
while(head != null){ //把链表中的节点入栈
stack.push(head);
head = head.next;
}
//判断栈为空,则返回null
if(stack.isEmpty()) {
return null;
}
//从栈中取出元素,然后组成链表,则为反转后的链表
ListNode dummy = stack.pop();
ListNode temp = dummy;
while(!stack.isEmpty()){
temp.next = stack.pop();
temp = temp.next;
}
//最后一个节点的next要置为空,否则会构成环
temp.next = null;
return dummy;
}
扩展:递归法!
一般这种递归法,只有做了很多算法的老手,才会想起来使用,同时递归也比较难理解。
什么是递归?
递归就是在运行过程中不断的调用自己。递归有两个重要的过程:一个是递的过程,一个是归的过程。
上代码:
public void fun(参数) {
if (终止条件) {
return;
}
fun(参数);
(其他判断条件或语句);
}
- 当第一次进入函数时,先判断是否符合终止条件,符合则直接结束函数,不符合入下一语句,调用自己重新进入下一层自身函数,(注意这是最外一层将不向下继续执行语句,外层卡在fun(参数处)),这个调用自己进入自身函数的操作过程即为“递”的过程。
- 假设进入下一层后符合终止条件,返回结果,此时之前进入自身函数执行完成返回最外一层函数,最外一层函数递归调用处得到结果,(即内层函数执行完成得到结果返回值),这个过程即为“归”的过程。这时最外一层函数才能继续执行下一语句,直至函数运行完成。
什么场景适合用递归?
1.大问题可以拆分为多个子问题。
2.原问题和拆分后的子问题除了数据规模不同,解决思路完全相同。
3.存在递归终止条件。
注:递归在线性数据结构中使用不太明显,迭代基本可以很容易地解决问题。
递归在非线性结构中非常重要,比如二叉树,回溯,典型的树形问题-九宫格字母组合。
递归必须满足的条件?
- 有边界,即终止条件。
- 需要调用自己。
注:这两个条件缺一不可,并且其中终止条件语句必须在递归调用语句之前。如果顺序颠倒则递归函数会进入死循环,永远退不出来,会出现堆栈溢出异常(StackOverflowError)。
更多关于递归的知识,想学习的友友可以自己学看~
我们希望n(k+1)的下一个节点指向n(k)。所以:n(k).next.next = n(k)
注:n1的下一个节点必须指向∅。要不然可能会形成环。
代码:
/**
* 以链表1->2->3->4->5举例
* @param head
* @return
*/
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
/*
直到当前节点的下一个节点为空时返回当前节点
由于5没有下一个节点了,所以此处返回节点5
*/
return head;
}
//递归传入下一个节点,目的是为了到达最后一个节点
ListNode newHead = reverseList(head.next);
/*
第一轮出栈,head为5,head.next为空,返回5
第二轮出栈,head为4,head.next为5,执行head.next.next=head也就是5.next=4,
把当前节点的子节点的子节点指向当前节点
此时链表为1->2->3->4<->5,由于4与5互相指向,所以此处要断开4.next=null
此时链表为1->2->3->4<-5
返回节点5
第三轮出栈,head为3,head.next为4,执行head.next.next=head也就是4.next=3,
此时链表为1->2->3<->4<-5,由于3与4互相指向,所以此处要断开3.next=null
此时链表为1->2->3<-4<-5
返回节点5
第四轮出栈,head为2,head.next为3,执行head.next.next=head也就是3.next=2,
此时链表为1->2<->3<-4<-5,由于2与3互相指向,所以此处要断开2.next=null
此时链表为1->2<-3<-4<-5
返回节点5
第五轮出栈,head为1,head.next为2,执行head.next.next=head也就是2.next=1,
此时链表为1<->2<-3<-4<-5,由于1与2互相指向,所以此处要断开1.next=null
此时链表为1<-2<-3<-4<-5
返回节点5
出栈完成,最终头节点5->4->3->2->1
*/
head.next.next = head;
head.next = null;
return newHead;
}