从尾到头打印链表的多种实现方式

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动。

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

题目

输入一个链表的头结点,从尾到头反过来打印出每个结点的值。

考察要点

链表的特性, 链表的遍历, 链表的访问

自己的思路

当我看到这个题目时, 首先想到的是用栈来实现从尾到头打印链表. 当我们遍历链表时, 将遍历到的结点放入栈中; 然后访问栈顶元素, 并将该元素出栈.

然后想到的是翻转链表, 也就是每访问到一个结点时, 就将该结点的next指针指向它的前一个结点; 然后再遍历这个翻转后的链表, 就可以做到从尾到头输出链表了.

代码

前期准备

链表结点类

 class ListNode {
     int value;
     ListNode next;
 }
复制代码

数据准备

 public class LinkedList {
     public ListNode head = new ListNode(-1);
 ​
     public static void main(String[] args) {
         LinkedList list = new LinkedList();
 ​
         ListNode l1 = new ListNode(1);
         ListNode l2 = new ListNode(3);
         ListNode l3 = new ListNode(5);
         ListNode l4 = new ListNode(7);
         ListNode l5 = new ListNode(9);
 ​
         list.head.next = l1;
         l1.next = l2;
         l2.next = l3;
         l3.next = l4;
         l4.next = l5;
 ​
         list.printLinkedListReverselyByStack();
     }
 }
复制代码

基于翻转链表的实现方式

思路: 每访问一个结点的时候, 就将该结点的next指针指向它的前一个结点, 再遍历翻转后的链表中的每个结点的数据

时空复杂度: O(1)的空间复杂度, O(n)的时间复杂度

优点: 内存占用少

缺点: 更改了原链表的结构, 代码复杂, 不容易理解

 public void printLinedListReverselyByOverturn() {
 ​
     // 拿到头结点
     ListNode p = this.head;
 ​
     // 用来指向首结点
     ListNode q = p;
 ​
     // 用来记录未翻转链表的首结点
     ListNode next = p.next;
 ​
     // 当前头结点的next指针置空
     q.next = null;
 ​
     // 只要下个结点不为空就继续遍历
     while (next != null) {
         // 用来完成next指针的翻转
         p = next;
         next = p.next;
         p.next = q;
         q = p;
     }
 ​
     // 更改头结点的指向
     this.head.next = q;
 ​
     // 遍历翻转后的链表
     while (q != null) {
         if (q.value == -1) return;
         System.out.print(q.value + " ");
         q = q.next;
     }
     System.out.println();
 }
复制代码

基于栈的实现方式

思路: 每访问一个结点的时候, 就将该结点中的数据放入栈中, 再遍历栈中的数据

时空复杂度: O(n)的空间复杂度, O(n)的时间复杂度

优点: 思路明确, 代码量少, 结构简单, 易于理解

缺点: 会开辟对应结点数量的内存空间, 有O(n)的空间复杂度

 public void printLinkedListReverselyByStack() {
 ​
     // 拿到头结点
     ListNode p = this.head;
 ​
     // 创建一个栈
     Stack<Integer> stack = new Stack<>();
 ​
     // 遍历链表, 将数据存储到栈中
     while (p.next != null) {
         p = p.next;
         stack.push(p.value);
     }
 ​
     // 遍历栈, 取出栈顶元素, 直到栈空为止
     while (!stack.isEmpty()) {
         System.out.print(stack.pop() + " ");
     }
     System.out.println();
 }
复制代码

基于递归的实现方式

思路: 每当要访问一个结点的时候, 我们就先打印它后面结点中的数据, 再打印当前结点中的数据.

时空复杂度: O(n)的空间复杂度, O(n)的时间复杂度

优点: 代码量少, 实现简单, 易于理解, 不更改原链表的结构.

缺点: 当链表中的结点非常多的时候, 就会导致递归层次加深, 占用内存递增, 有O(n)的空间复杂度, 可能会导致内存溢出.

 public void printLinkedListByRecurrence() {
     // 拿到头结点
     ListNode p = this.head;
     if (p.next != null) printLinkedListByRecurrence2(p.next);
 }
 ​
 public void printLinkedListByRecurrence2(ListNode node) {
     // 当结点为空时, 递归开始返回
     if (node != null) {
         // 当该结点不为空时, 就打印它的next指向的结点
         printLinkedListByRecurrence2(node.next);
         
         // 然后再打印自己结点中的数据
         System.out.print(node.value + " ");
     }
 }
复制代码

总结

本题考察我们对于链表的理解, 如何遍历整个链表; 也考察我们对于栈这种数据结构的使用; 又考察我们对于递归的理解. 一般我们在编程中其实应该尽量避免使用递归, 因为它会开辟大量的栈内存, 进行函数的调用与返回, 当数据量很大的时候, 性能是很低的.

猜你喜欢

转载自juejin.im/post/7016981606791381005