Android实习面试准备——数据结构与算法(二)

关于链表

1、查询第一个跟倒数第二个

        第一个直接返回第一个节点就可以,查询倒数第二个节点的话,可以用两个指针,pre和curr,curr在pre的前面,当curr到最后一个节点的时候,pre就是倒数第二个节点。

2、arrayList底层原理

        ArrayList的底层数据结构是一个object类型的数组Object[],默认数组大小为10。

        当调用无参构造方法时,会将默认大小为10的数组设置给成员变量elementData。

public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

        调用int类型参数构造方法时,若initialCapacity大于0,则new一个大小为initialCapacity的数组,若等于0,则直接将一个空数组赋值给成员变量,小于0就抛出异常。

public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

        参数为集合类型的构造方法:将集合c转换为数组赋给elementData,如果数组不是空的,先判断一下是否成功转化为Object[],如果没有就进行复制,转化为Object类型的数组;如果数组是空的,将一个空数组赋值给成员变量。

public ArrayList(Collection<? extends E> c) {
        Object[] a = c.toArray();
        if ((size = a.length) != 0) {
            if (c.getClass() == ArrayList.class) {
                elementData = a;
            } else {
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            // replace with empty array.
            elementData = EMPTY_ELEMENTDATA;
        }
    }

        接下来说一说非常重要的add方法,知道了这个方法的原理,arrayList的底层原理基本就搞清楚了。首先无论是add(E e)还是add(int index, E element),亦或者 add(E e, Object[] elementData, int s),当成员变量size已经等于数组的长度时,都要调用grow方法扩容。例如:

private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }

        在grow方法中参数为minCapacity=size+1,也就是添加一个元素后的大小,首先会判断成员变量elementData是不是默认大小的数组,如果是,取默认大小和minCapacity中最大的那一个为新数组的大小;若不是默认大小的数组,调用ArraysSupport.newLength方法得到新的数组大小,将原来的数组复制到新的数组中去。

private Object[] grow(int minCapacity) {
        int oldCapacity = elementData.length;
        if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            int newCapacity = ArraysSupport.newLength(oldCapacity,
                    minCapacity - oldCapacity, /* minimum growth */
                    oldCapacity >> 1           /* preferred growth */);
            return elementData = Arrays.copyOf(elementData, newCapacity);
        } else {
            return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
        }
    }

        ArraysSupport.newLength方法中有三个参数,oldLength为原来数组的大小, minGrowth最小需要扩容的量, prefGrowth为原来容量的1/2。首先将原来数组的大小加上minGrowth和prefGrowth的最大值,如果是小于ArrayList数组最大的容量的,直接返回即可;如果大于,便调hugeLength方法。

public static final int MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8;

public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
        // assert oldLength >= 0
        // assert minGrowth > 0

        int newLength = Math.max(minGrowth, prefGrowth) + oldLength;
        if (newLength - MAX_ARRAY_LENGTH <= 0) {
            return newLength;
        }
        return hugeLength(oldLength, minGrowth);
    }

        hugeLength方法中拿到数组所需要的最小的长度后,如果小于0,说明溢出了,则抛出异常;若小于MAX_ARRAY_LENGTH,则将长度设为MAX_ARRAY_LENGTH;若大于,则设为Integer.MAX_VALUE。

private static int hugeLength(int oldLength, int minGrowth) {
        int minLength = oldLength + minGrowth;
        if (minLength < 0) { // overflow
            throw new OutOfMemoryError("Required array length too large");
        }
        if (minLength <= MAX_ARRAY_LENGTH) {
            return MAX_ARRAY_LENGTH;
        }
        return Integer.MAX_VALUE;
    }

        总结成一张流程图:

3、如何在一次遍历中找到单个链表的中值

        当然说先得询问一下面试官的要求,因为这里如果是奇数个节点,中值自然只有一个,如果是偶数个节点,那么中值有两个,是要第一个还是第二个就看面试官的要求了。

        (1)直接的办法:先遍历一遍,知道了链表的长度之后,再从头走长度的一半。      

        (2)采用快慢指针法,快指针一次走两步,慢指针一次走一步,等快指针走到头的时候,慢指针就刚好到中间的位置了。可以看一下leetcode 876。

    public ListNode middleNode(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        while(fast.next!=null && fast.next.next!=null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        return slow;
    }

4、如何证明给定的链表是否包含循环?如何找到循环的头节点

        同样采用快慢指针法。快指针会先进入环里,慢指针后进入,当慢指针进入环的时候,就相当于快指针去追赶慢指针了(相当于大家一起跑1800m,跑得快的比慢的会快至少一圈,那中间肯定会追上慢的)。当快慢指针相遇的时候,就说明有环,而如果快指针走到了null的位置,那自然是没有环了。

        至于头结点的话,我看过两个解法,其实都差不多。一个数学方法,就是计算快慢指针相遇的时候各段之间的距离关系(这个看leetcode 22),另一个是剑指offer上的解法,它的思路是从相遇结点开始先走一圈,算出这一圈的长度x,然后让两个指针重新回到起点,让一个指针先走x,之后另一个指针和这个指针一起行动,当他们相遇的时候,就是循环入口所在位置。

5、两个有交叉的单链表,求交叉点

        (1)暴力法:从第一个链表开始遍历(链表长度m),每遍历一个结点的时候,就从第二个链表开始遍历(链表长度n),寻找是否有这个结点。这是O(n*m)的时间复杂度。

        (2)利用辅助空间。申请两个栈,两个单链表如果有公共结点,那么看起来应该是躺着的Y字型。想找到公共结点,最好是从最右边的结点向左遍历,找到最后一个公共结点就可以,但是链表是单向的,只能从左到右,那么就利用栈“先进后出”的特性,将两个链表的结点放入两个栈中,不断弹出结点,直到最后一个相同的结点。这需要O(n+m)的辅助空间,但是是O(n)的时间复杂度。

         (3)先遍历一遍两个链表,搞清楚他们各自的长度,这样就知道了长度差x,之后从较长的链表上先走x步,然后和较短链表开始同时遍历,直到第一个公共结点。这不需要辅助空间,并且是O(n)的时间复杂度。

   public static void main(String[] args) {
        int length1 = getListLength(ListNode p1);
        int length2 = getListLength(ListNode p2);
        ListNode pShort = p1;
        ListNode pLong = p2;
        int lengthDiff = length2 - length1;
        if(lengthDiff < 0) {
            pShort = p2;
            pLong = p1;
            lengthDiff = length1 - length2;
        }
        for(int i = 0; i < lengthDiff; i++) {
            pLong = pLong.next;
        }
        while(pLong!=null && pShort!=null && pShort!=pLong) {
            pLong = pLong.next;
            pShort = pShort.next;
        }
        return pLong;
    }

    public static int getListLength(ListNode head) {
        ListNode ptr = head;
        int length = 0;
        while(ptr!=null) {
            length++;
            ptr = ptr.next;
        }
        return length;
    }

 6、如何得到单链表的长度

        除了遍历计数,也没啥其他办法了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值