剑指Offer之每日五道算法题(Java)——第一天

面试题24

问题描述

输入一个链表,反转链表后,输出新链表的表头。

反转链表——牛客网测试用例

我的思路

  1. 在保留头指针的情况下,用三个指针分别指向中间的节点与前后节点,像这样:
    节点

  2. 反转中间指针的指向后,整体后移一格,变成这样:
    在这里插入图片描述
    (这个A节点是头结点,所以不用管A的指向,最后将A结点指为null即可。)

  3. 对上述操作进行循环,直到n.next指向null(也就是到了尾结点)为止,此时会变成这样:
    在这里插入图片描述
    最后把cn节点的指向反转,即可。

实现代码

上述思路实现代码如下:

/*public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}*/
public class Solution {
    public ListNode ReverseList(ListNode head) {
        ListNode headNode = head;

        /* 如果头结点为null,返回头结点 */
        if(head == null) {
            return head;
        }

        /* 将节点数分别为1、2的情况挑出来,为3个及以上节点分别设置头结点及其前后节点 */
        if(head.next == null) {
            return head;
        } else if (head.next.next == null) {
            head = head.next;
            head.next = headNode;
            headNode.next = null;

            return head;
        } else {
            ListNode nextNode = head.next.next;
            head = head.next;
            ListNode prevNode = headNode;

            /* 每次反转前一个节点的next指向,然后3个指针后移 */
            while(nextNode.next != null) {
                head.next = prevNode;
                prevNode = head;
                head = nextNode;
                nextNode = nextNode.next;
            }

            /* 改变最后两个节点next的指向 */
            head.next = prevNode;
            nextNode.next = head;
            head = nextNode;
            headNode.next = null;

            return head;
        }
    }
}

其中,headNode就是保留的原始头结点的指针,即反转链表之后的尾结点指针。
head是反转过程中的中间指针,反转之后的头结点指针。

面试题25

问题描述

输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。

合并链表——牛客网测试示例

我的思路

  1. 构建一个最终链表,初始化为null
  2. 先比较输入的两链表头结点的值的大小,将小的一方的值加入最终链表的新建节点中。
  3. 遍历两链表,每次循环比较两链表的值,将小的一方的值加入最终链表的新建节点中,直到一个链表遍历完毕。
  4. 将另一个链表的剩余节点加在最终链表之后,返回最终链表的头结点。

实现代码

方法A

上述思路实现代码如下:

/*class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}*/
public class Solution {
    public ListNode Merge(ListNode list1,ListNode list2) {

        /* 判断链表其中一个为null的情况 */
        if (list1 == null) {
            return list2;
        }
        if (list2 == null) {
            return list1;
        }

        /* 最终链表指针 */
        ListNode finalList = null;

        /* 最终链表头指针 */
        ListNode headNode = null;
        /* 先构造最终链表的头结点 */
        if(list1.val >= list2.val) {
            finalList = finalList = new ListNode(list2.val);
            list2 = list2.next;
        } else {
            finalList = finalList = new ListNode(list1.val);
            list1 = list1.next;
        }
        headNode = finalList;

        /* 遍历两个链表 */
        while(list1!=null && list2!=null) {
            /* 比较两链表数据大小,将小的一方的值加入最终链表新建节点中 */
            if(list1.val >= list2.val) {
                finalList.next = new ListNode(list2.val);
                list2 = list2.next;
            } else {
                finalList.next = new ListNode(list1.val);
                list1 = list1.next;
            }
            finalList = finalList.next;
        }
        /* 一方为空后,直接将另外一个链表的剩余节点加到最终链表上 */
        if(list1 == null) {
            finalList.next = list2;
        } else {
            finalList.next = list1;
        }

        return headNode;

    }
}

这个只是初步方法,相信很容易就看出问题,每次都要新建一个节点这也太麻烦了叭,而且最后相当于生成了一条新的链表……
我们能不能不新建节点,直接用现成的节点来完成操作呢?

方法B

一开始我想的是再构造两个链表指针互相比较,而list1list2作为每个链表的头结点,就像这样:
在这里插入图片描述
(蓝色是一开始的指向,橘色是改变之后的指向。)

后来发现,行不通…… 简单的示例确实可以通过,但是出现了两个BUG:

  1. 当两链表有很多相同的值的时候,会漏值。
  2. 当某链表出现连续的值都比另一链表小时,比如链表A{1, 3, 5},链表B{4, 6},这样就会漏掉3这个值,让1直接指向4。

为了解决这两个BUG,我的想法是:

  1. 每次相同的值就变换指向,这样就引发了另一个问题:如何判断是改变链表A中节点指向还是改变链表B中节点指向?
  2. 额外添加一个指针指向后一个节点,把后一个值与另一链表的头结点的值比较,如果小就不更改指向,再比较下一个值。

于是我构建了一个flag常量用于判断上次变换节点是在链表A还是链表B,如果上次变换节点是由链表A指向了链表B,那么下次相同的值就要从链表B指向链表A。
第一个问题解决了,但是第二个问题我想了一下,我何必要这么麻烦?我直接构建一个过程指针,用flag判断这个指针当前在哪个链表,然后去跟另一链表的值比较,小就指向下一个节点接着比较,大就将过程指针指向第二个链表然后重新和第一链表比较不就好了?
这样既不用新建节点,当链表A多个连在一起的值比链表B小时,也不用“多此一举”的去把这些节点的指向重新指一遍。

于是,叮叮当当,方法二出炉了:

public ListNode Merge(ListNode list1,ListNode list2) {

        /* 判断链表其中一个为null的情况 */
        if (list1 == null) {
            return list2;
        }
        if (list2 == null) {
            return list1;
        }

        /* 最终链表的头结点指针 */
        ListNode finalList = null;

        /* 标志位,标志“过程指针”到了哪个链表,1代表在链表A,2代表在链表B */
        int flag = 1;
        /* 先把最终链表头结点指向两者中头结点值较小的一方 */
        if(list1.val >= list2.val) {
            finalList = list2;
            flag = 2;
        } else {
            finalList = list1;
        }

        /* 两链表过程指针 */
        ListNode pList = finalList;
        /* 循环直到过程指针指向某一链表最后一个节点 */
        while (pList.next != null) {
            /* 当过程指针指向A链表时,如果指针下一个节点值比B链表指针小,则过渡到下一个节点,否则切换过程指针所在链表,并将两链表指针后移 */
            if(flag == 1) {
                if(pList.next.val < list2.val) {
                    pList = pList.next;
                } else {
                    list1 = pList.next;
                    pList.next = list2;
                    pList = list2;
                    flag = 2;
                }
            } else {
                if(pList.next.val < list1.val) {
                    pList = pList.next;
                } else {
                    list2 = pList.next;
                    pList.next = list1;
                    pList = list1;
                    flag = 1;
                }
            }
        }

        /* 将另一链表的剩余节点加在末尾 */
        if(flag == 1) {
            pList.next = list2;
        } else {
            pList.next = list1;
        }

        return finalList;
}

测试并作双手合十状(阿弥陀佛阿弥陀佛),成功!

(P.S.:我觉得还可以优化,比如有多个相同的值时直接把这两段相同的值连起来,而不用频繁的去变换过程指针,但是那样可能就要套一个循环,或者在体系外增加一个方法,所以此题暂时这样叭)

递归解法

另外,这个题解还有一种思路是用递归来做:

public ListNode Merge(ListNode list1,ListNode list2) {
		/* 基准情况,递归遍历直到一方为null */
        if(list1 == null){
            return list2;
        }
        if(list2 == null){
            return list1;
        }
        
        /* 新建最终链表保存结果 */
        ListNode finalList = null;
        /* 对于每一次遍历的每一个节点来说,最终链表指针永远指向较小的一方 */
        if(list1.val < list2.val){
            finalList = list1; 
            finalList.next = Merge(list1.next,list2);
        }else{
            finalList = list2;
            finalList.next = Merge(list1,list2.next);
        }
        return finalList;
}

这个递归思路很简单,画个图就懂了,这里以前两次递归为例:
在这里插入图片描述
蓝色的是每次进入方法新建的指针finalList(这个引用只存在于当前方法体中,但是它的next域作为下个指针指向节点的返回域),橘色的是第一次进入方法时创建的finalList,也就是最终链表的情况。

也就是说,每一次调用Merge方法,都是将上一次连入节点的下一个节点为头结点,作为新链表传入,而返回的结果就加到一开始的finalList中,持续下去,直到达到基准情况后将另一链表剩余节点返回加到finalList.next上,最后返回最终链表的头结点。

面试题2

问题描述

设计一个类,我们只能生成该类的一个实例

实现代码

class Singleton {

    /**
     * 构造方法使用private限定外部实例化该类
     */
    private Singleton(){}

    /**
     * 静态内部私有类
     * 在第一次加载时初始化并创建实例
     */
    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }

    /**
     * 实例的全局访问点
     * @return 唯一实例
     */
    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }

}

典型的多线程下的单例模式实现方法,具体可以参考我的另一篇文章:
Java 设计模式通关之路——单例模式

面试题3

问题描述

在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。

牛客网——寻找数组重复数字测试用例

方法一

实现代码

题目的测试用例要求将结果保存在duplication[0]中,存在返回true,不存在返回false

import java.util.*;

public class Solution {
	public boolean duplicate(int numbers[],int length,int [] duplication) {
		/* 构建一个哈希表作为中介 */
        Map<Integer, Integer> map = new HashMap<>(length);

		/* 如果输入为空,直接返回不存在 */
        if(length == 0) {
            return false;
        }

		/* 对数组遍历,对每个值判断其在哈希表是否存在,存在就返回,不存在就保存进去 */
        for (int num : numbers) {
            if(map.containsKey(num)) {
                duplication[0] = num;
                return true;
            } else {
                map.put(num, 1);
            }
        }

        return false;
    }
}

我的第一想法当然是构建哈希表,对数组中的每一个数字进行判断,不存在就保存在哈希表中,存在就返回。

最简单暴力的想法是对数组排序后遍历,而哈希表的介入简化了这一过程。
继续思考一下,有没有一种方法可以不需要任何的介入呢?

方法二

重新审题,有一句话引起了我的注意:所有数字都在0到n-1的范围内
题目为什么要给出这句话?显然是有原因的,这句话的意思变换一下就是:如果数组中没有重复的数字,那么当数组排序后,数组的下标就是对应的值!

一个思路渐渐清晰起来:我们可以让每一个数字都放在他应该在的位置,这样只要其他位置只要有一样的值,就是重复的。
说起来有点绕,举个例子,现在有一个数组:list {2, 1, 3, 0, 3}

  1. 先看第一个值2,它应该在哪?它应该在数组下标为2的位置,所以我们先判断list[2]的位置是不是也是2,发现不是,那么就2换到它该在的位置上。
    换过一次后数组变为:list {3, 1, 2, 0, 3}
  2. 继续检测3这个数,对应位置的数为0,不相等,交换:list {0, 1, 2, 3, 3}
  3. 检测0,发现他就在本来应该在的位置,扫描下一个,一直扫描到最后一个数,也就是list[4]位置的数3,拿它和list[3]比较,相等!重复了,返回这个数。

接下来看实现代码。

实现代码
public class Solution {
	public boolean duplicate(int numbers[],int length,int [] duplication) {
        
        for(int i=0; i<length; i++) {
            /* 如果值在对应位置上,跳过本次循环看下一个值 */
            if(numbers[i] == i) {
                continue;
            } else {
                /* 如果值与对应位置上的值相等,则重复,否则交换位置,重新检测(i-1) */
                if(numbers[i]==numbers[numbers[i]]) {
                    duplication[0] = numbers[i];
                    return true;
                } else {
                    int temp = numbers[i];
                    numbers[i] = numbers[numbers[i]];
                    numbers[temp] = temp;
                    i --;
                }
            }
        }

        return false;
    }
}

面试题4

问题描述

在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

牛客网——二维数组中的查找测试用例

我的思路

因为基本没有做过二维数组的题,所以初次见到的时候还是有点害怕的。
理了一下题目,大致在脑海中构思了一个想法(但是估计是最笨的雾):

  1. 先拿整数和二维数组的最大值比较,也就是右下角那个值,大于或者等于当然是最好,大于代表不存在,等于就直接返回了。
  2. 小于的话,考虑到二维数组可以分割成若干个一维数组,所以按照最后一列向上遍历,直到确定所在的最小一维数组。
    比如最后一列是{9, 12, 15, 20}20作为二维数组最大值已经比较过,所以和15去比较,如果大于15就从最后一行开始找,也就是20所在的一维数组,小于15就继续往下跟12比。
  3. 经过第二步操作确定了那个数在哪几行之中。
    (比如大于12小于15,那么这个数必然在15所在的一维数组或者比15大的下面那些行中)
    之后再按照最小的这个一维数组往前遍历(遍历15所在行),直到找到比这个整数小的(它都比这个整数小,那前面的就更小了),再去遍历这个数所在的列,找到比这个整数大的,再遍历行,重复以上操作。

写思路都快把我写晕了…
其实很简单,就是跟蛇形步一样,拐来拐去,从最小的行中最大的数开始按行拐,遇到较小的数就往下按列拐(因为从上到下单调递增),遇到较大的数再往左按行拐(因为从右到左单调递减),直到找到这个数或者遍历完,画个图可能清晰一点……
假如我在这个二维数组中找整数3,则遍历路径为:
在这里插入图片描述
15所在列比较,小小小,到最后一行,还是小;
那就在9所在行比较,小,大!比2大!
所以接着沿着2所在列比较,下一个数,比4要小!所以又在4所在行比较。
发现,比2大,所以沿着2所在列比较,找到了3相等,返回。

永远参考一个原则:对于二维数组中的某一个数来说,下面的数比它大,左边的数比它小。
我从右下角最大的数开始往上遍历每一行最大的值,若存在与该整数相等的值,则一定能去遍历到。

实现代码
public class Solution {
    public boolean Find(int target, int [][] array) {
        /* 得到行和列的最大下标 */
        int columnIndex = array.length - 1;
        int rowIndex = array[0].length - 1;

        /* 如果传入的二维数组为空,返回false */
        if(columnIndex == 0 && rowIndex == -1) {
            return false;
        }

        /* 如果该整数比二维数组中最大值还大,返回false */
        if(target > array[columnIndex][rowIndex]) {
            return false;
        }

        /* 遍历二维数组最后一列,直到找到大于等于该整数的值或者遍历到第一行 */
        while(target < array[columnIndex][rowIndex] && columnIndex!=0) {
            columnIndex --;
        }

        /* 如果等于就返回true,否则遍历该数所在的一维数组(所在行) */
        if(target == array[columnIndex][rowIndex]) {
            return true;
        } else {
            /* 如果不是第一行(比如小于第四行的15但是大于第三行的13),则遍历较大的数(15)所在行 */
            if(columnIndex != 0) {
                columnIndex ++;
            }

            /* 从后往前遍历上述找出的数所在的一维数组(行) */
            while(target != array[columnIndex][rowIndex]) {
                rowIndex --;

                /* 该行遍历完了或者该列遍历完了还没跳出循环,证明没有相等的数,返回false */
                if(rowIndex == -1 || columnIndex > (array.length-1) ) {
                    return false;
                }

                /* 当在该行中找到小于target整数的数时,遍历该列,直到找到大于target的数 */
                while(target > array[columnIndex][rowIndex] && columnIndex <= (array.length-1)) {
                    columnIndex ++;
                }
            }

            return true;
        }
	}
}

优化代码

看了一下书,发现思路相同…… 书上是从右上角开始遍历,而我是从最大值也就是右下角开始遍历。
再看一下代码示例…… 为什么要比我的简单这么多?!
我用了一个嵌套的循环,而它用了一个循环就解决了,先贴代码示例,再来分析一下我的代码“复杂”在哪里:

public class Solution {
    public boolean Find(int target, int [][] array) {

        if (array == null) {
            return false;
        }

        int rowIndex = 0;
        int columnIndex = array[0].length - 1;

        while (rowIndex<array.length && columnIndex>=0) {
            if (target == array[rowIndex][columnIndex]) {
                return true;
            } else if (target < array[rowIndex][columnIndex]) {
                columnIndex --;
            } else {
                rowIndex ++;
            }
        }

        return false;
    }
}

因为从右上角开始遍历,所以是从array[0][columnIndex]开始。
他的遍历条件是:只有当遍历完最后一行最后一列时才会退出循环。
而我的遍历条件,是以是否找到这个值为基准
这个谁先谁后我不知道有没有一个标准的定论,我们都在循环中包含了另一个条件判断,所以我觉得没有什么大的差别,不过外层循环尽量用“大范围”的条件可能要好一点。

我加了一个循环的原因是,他是每次循环遍历一个数,而我在里面直接用循环把该列遍历完了。

所以显而易见,我应该改进的地方:

  1. 尽量在外层循环使用“大范围”的条件。
    比如我的外层循环的条件是:找到相等的数,而内部条件判断是:是否遍历完全。“找到相等的数”相比于“是否遍历完全”可能范围要更小,所以应该把“是否遍历完全”放在外层循环上,虽然无甚差别,但是从逻辑上来讲由大到小会更通一点。
  2. 能用外层循环做的事情,就不要在内层多此一举。
    因为题目中给出的性质已经决定了“下大左小”,所以比它大往下,比它小往左,完全可以用条件判断来解决,没必要再多加一层循环。
  3. 根据性质确定起始点。
    由于“下大左小”的性质,起始点的最佳选择当然是右上角的数(因为它的右边和上边没有数,只用判断下和左即可,小往左大往下),而我顺着第一感觉选了右下角的二维数组最大值,这是不正确的,当提取了“性质”后,根据性质来选择起始点才是最佳的。

Github

所有面试题的实现我都会放在我的Github仓库中,包括多种实现与详细注释,需要的可以去以下网址查看:
剑指Offer-Java实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值