关于链表
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、如何得到单链表的长度
除了遍历计数,也没啥其他办法了。