概述
翻转类算法的题目类型挺多的,有数组翻转、字符串翻转、链表翻转、二叉树翻转等。有些题目虽然不叫翻转,但类型接近,比如倒序输出一组数字等。
分析
翻转算法有哪些实现方式?
方式一 :栈
在数据结构中有一个天然实现倒序的辅助工具——栈。栈先进后出的特性可以处理绝大数的翻转题。这里举个例子:
从尾到头打印链表
private static void printReverseSingleNode(SingleNode head){
Stack<Integer> stack = new Stack<>();
while (head != null){
stack.push(head.data);
head = head.next;
}
while (!stack.empty()){
System.out.print(stack.pop());
}
}
这里需要注意的是,栈里面我仅仅只是存的链表节点的值,并没有存放链表节点,这样实现起来也是最容易的。如果使用栈去倒序链表,有点得不偿失,因为需要最后会创建一个新的链表,空间复杂度高,且实现起来会比较麻烦。
方式二 :递归
关于递归的学习,我之前一直存在一个误区,就是想搞懂递归执行的每一个细节,然后我最后发现,当你深究细节时,你就糊涂了(有点类似于量子力学的测不准原理。。。)所以,只要遇到递归,我们就把它抽象成一个递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每个步骤。递归算法在二叉树上的使用非常常见,这里用二叉树举个例子:
翻转二叉树
/**
* 递归版本,其实就是左右交换
* 由于树中每一个节点都需要被访问,因此时间复杂度就是O(n),其中n 是节点个数
* 本方法使用了递归,在最坏情况下栈内存需要存放O(h)个方法调用,其中h是树的高度,由于h属于O(n),可得空间复杂度是O(n)
*/
private static TreeNode reverseTree1(TreeNode root){
if (root == null){
return null;
}
TreeNode temp = root.leftChild;
root.leftChild = reverseTree1(root.rightChild);
root.rightChild = reverseTree1(temp);
return root;
}
方式三 :迭代
这里使用的是双指针的方法。有的时候面试官可能不会让你使用递归去实现一个算法(例如二叉树的前、中、后的遍历,使用递归写太过简单),又或者你自身对递归思想理解的不深刻(最好还是要深刻下,递归很重要),那这个时候,使用迭代的方法处理问题,相比前面两种方式,就不是那么的绕。举个例子:
反转字符串
/**
* 反转方法,使用双指针法
* @param s
*/
private static void reverseString(char[] s) {
int left = 0;
int right = s.length -1;
char temp;
int len = s.length;
for (int i = 0; i < len; i++) {
if (left < right){
temp = s[left];
s[left] = s[right];
s[right] = temp;
left++;
right--;
}
}
}
使用双指针,一个指向数组的头,一个指向数组的尾(这个思想在二分查找中也会使用),然后双方开始叫唤。叫唤了N/2。时间复杂度是O(n),空间复杂度是O(1).
当然,这个双指针看起来有点简单,接下来看看稍微复杂的。
翻转单链表,先看下代码:
*/
public static SingleNode reverseSingleNode2(SingleNode head){
SingleNode current = head;
SingleNode pre = null;
SingleNode next;
while (current != null) {
// 取出 next
next = current.next;
// 将上一个赋值给 next
current.next = pre;
// 更改 上一个到当前位置
pre = current;
// 当前位置往下移动
current = next;
}
return pre;
}
翻转单链表要比翻看起来比字符串的那个复杂些,我也是单步调试,然后仔细思索才明白(我有点笨,所以就用了笨办法),理解起来你可以把第三个指针next理解成一个temp,方便后面链表向后移动。每次取一个节点,然后加到要上一个记录节点的前面。关键就是current.next = pre;和 pre = current;的理解。
其实使用栈的方式也算是迭代的一种。这里在举一个使用队列作为辅助工具,实现翻转二叉树
/**
* 非递归版本,即使用迭代法
* 创建一个队列来存储所有的左孩子和右孩子还没有被交换过的节点,开始的时候仅根节点在队列中,只要队列不为空,
* 就一直从队列中取出节点,然后交换这个节点的左右孩子节点,接着再把孩子节点放入队列中,对于其中的空节点不用加到队列中,
* 因为最后队列一定为空,这个时候所有的孩子节点已经被交换过了,所以最后再返回根节点即可。
*/
private static TreeNode reverseTree2(TreeNode root){
if (root == null){
return null;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()){
TreeNode current = queue.poll();
TreeNode tmp = current.leftChild;
current.leftChild = current.rightChild;
current.rightChild = tmp;
if (current.leftChild != null) queue.add(current.leftChild);
if (current.rightChild != null) queue.add(current.rightChild);
}
return root;
}
迭代算法实现翻转,大致分两步,对于非线性数据结构数据(你如二叉树),就需要一个辅助工具来完成数据翻转,然后再使用交换算法。对于线性结构数据,直接使用两个指针即可。