题目描述
给定一个带头结点的单链表,请将其逆序。即如果单链表原来为head→1→2→3→4→5→6→7,则逆序后变为head→7→6→5→4→3→2→1。
解题思路
由于单链表与数组不同,单链表中每个结点的地址都存储在其前驱结点的指针域中,因此,对单链表中任何一个结点的访问只能从链表的头指针开始进行遍历。在对链表的操作过程中,需要特别注意在修改结点指针域的时候,记录下后继结点的地址,否则会丢失后继节点。
方法一:就地逆序
主要思路:在遍历链表时,修改当前结点的指针域的指向,让其指向它的前驱结点。为此,需要一个指针变量来保存前驱结点的地址。此外,为了在调整当前结点指针域的指向后还能找到后继结点,还需要另外一个指针变量来保存后继结点的地址,在所有的结点都被保存好以后就可以直接完成指针的逆序了。
class LNode {
/**
* 数据域
*/
int data;
/**
* 下一个结点的引用
*/
LNode next;
}
public class Test1 {
/**方法功能:对单链表进行逆序输入参数:head:链表头结点*/
public static void reverse(LNode head){
//判断链表是否为空
if(head==null || head.next==null){
return;
}
//前驱结点
LNode pre = null;
//当前结点
LNode cur = null;
//后继结点
LNode next = null;
//把链表首结点变为尾结点
cur = head.next;
next = cur.next;
cur.next = null;
pre = cur;
cur = next;
//使当前遍历到的结点cur指向其前驱结点
while(cur.next != null){
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
//结点最后一个结点指向倒数第二个结点
cur.next = pre;
//链表头结点指向原来链表的尾结点
head.next = cur;
}
public static void main(String[] args){
//链表头结点
LNode head = new LNode();
head.next = null;
LNode tmp = null;
LNode cur = head;
//构造单链表
for(int i=1;i<8;i++){
tmp = new LNode();
tmp.data = i;
tmp.next = null;
cur.next = tmp;
cur = tmp;
}
System.out.print("逆序前:");
for(cur=head.next;cur!=null;cur=cur.next){
System.out.print(cur.data+" ");
}
System.out.print("\n逆序后:");
reverse(head);
for(cur=head.next;cur!=null;cur=cur.next){
System.out.print(cur.data+" ");
}
}
}
程序运行结果
逆序前:1 2 3 4 5 6 7
逆序后:7 6 5 4 3 2 1
算法性能分析
只需对链表进行一次遍历,时间复杂度为O(N),N为链表长度。需要常数个额外的变量来保存当前结点的前驱结点和后继结点,空间复杂度为O(1)
方法二:递归法
原链表为1→2→3→4→5→6→7,递归主要思想是:先逆序除第一个结点以外的子链表(将1→2→3→4→5→6→7变为1→7→6→5→4→3→2),接着把结点1添加到逆序的子链表的后面(将1→2→3→4→5→6→7变为7→6→5→4→3→2→1)。同理,在逆序链表2→3→4→5→6→7时,也是先逆序子链表3→4→5→6→7(逆序为7→6→5→4→3),接着实现链表的整体逆序(2→7→6→5→4→3转换为7→6→5→4→3→2)
private static LNode recursiveReverse(LNode head){
/**
* @Author: JavaRecord
* @Description:对不带头结点的链表进行逆序
* @Param [head]
* @Return linked.list1.LNode
* @Date 2020/8/10
* @Time 14:17
*/
//如果链表为空或者只有一个元素
if(head==null || head.next==null){
return head;
}else{
//反转后面的节点
LNode newhead = recursiveReverse(head.next);
//把当前遍历的结点加到后面结点逆序后的链表的尾部
head.next.next = head;
head.next = null;
return newhead;
}
}
public static void reverse(LNode head){
/**
* @Author: JavaRecord
* @Description:对带头结点的单链表进行逆序
* @Param [head]
* @Return void
* @Date 2020/8/10
* @Time 14:36
*/
if(head==null){
return;
}
//获取链表的第一个结点
LNode firstNode = head.next;
//对链表进行逆序
LNode newhead = recursiveReverse(firstNode);
//头结点指向逆序后链表的第一个结点
head.next = newhead;
}
算法性能分析
递归法也只需要对链表进行一次遍历,算法时间复杂度为O(N),N为链表长度。
优点:思路比较直观,容易理解,而且不需要保存前驱结点的地址
缺点:算法实现难度大。需要不断调用自己,需要额外的压栈和弹栈操作,与方法一相比,性能有所下降
方法三:插入法
主要思路:从链表的第二个结点开始,把遍历到的结点插入到头结点的后面,直到遍历结束。假定原链表为head→1→2→3→4→5→6→7,在遍历到2时,将其插入到头结点后,链表变为head→2→1→3→4→5→6→7,同理将后序遍历到的所有结点都插入到头结点head后,就可以实现逆序。
public static void reverse3(LNode head){
/**
* @Author: JavaRecord
* @Description:插入法
* @Param [head]
* @Return void
* @Date 2020/8/10
* @Time 14:48
*/
//判断链表是否为空
if(head==null || head.next==null) {
return;
}
LNode cur = null;
LNode next = null;
cur = head.next.next;
//设置链表第一个结点为尾结点
head.next.next = null;
//把遍历到的结点插入到头结点的后面
while (cur!=null){
next = cur.next;
cur.next = head.next;
head.next = cur;
cur = next;
}
}
算法性能分析
对单链表进行一次遍历,时间复杂度O(N)。与方法一相比,这种方法不需要保存前驱结点的地址,与方法二相比,这种方法不需要递归调用,效率更高
逆序输出
(1)就地逆序+顺序输出:改变了链表原来的结构
(2)逆序+顺序输出:申请新的空间,逆序,然后顺序输出,需要申请额外的存储空间
(3)递归输出:
public void reversePrint(LNode firstNode){
if(firstNode==null){
return;
}
reversePrint(firstNode.next);
System.out.println(firstNode.data+" ");
}