【4】链表

目录

一.哈希表的简单介绍

二.有序表的简单介绍

三.链表

1.单链表结构

2.双链表

3.判断一个链表是否是回文结构

4.将单向链表按照某值划分成左边小,中间相等,右边大的形式

5.复制还有随机指针节点的链表

6.两个单链表相交的一系列问题


一.哈希表的简单介绍

  1. 哈希表在使用层面可以理解为一种集合结构
  2. 如果只有key,没有伴随数据value,可以使用HashSet结构(C++中叫UnOrderedSet)
  3. 如果既有key,又有伴随数据value,可以使用HashMap结构(C++中叫UnOrderedMap)
  4. 有无伴随数据,是HashMap和HashSet唯一区别,底层的实际结构式一回事
  5. 使用哈希表增(put/add)、删(remove)、改(put)和查(get)操作,可以认为时间复杂度为O(1),但是常熟时间比较大
  6. 放入哈希表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小
  7. 放入哈希表的东西,如果不是基础类型,内部按引用传递,内存占用式这个东西内存地址的大小

二.有序表的简单介绍

  1. 有序表在使用层面上可以理解为一种集合结构
  2. 如果只有key,没有伴随数据value,可以使用TreeSet结构(C++中叫OrderedSet)
  3. 如果既有key,又有伴随数据value,可以使用treeMap结构(C++中叫OrderedMap)
  4. 有无伴随数据,是TreeSet和TreeMap唯一的区别,底层的实际结构是一回事
  5. 有序表和哈希表的区别是,有序表把key按照顺序组织起来,而哈希表完全不组织
  6. 红黑树、AVL树、size-balance-tree和跳表等都属于有序表结构,只是底层具体实现不同
  7. 放入有序表的东西,如果是基础类型,内部按照值传递,内存占用就是这个东西的大小
  8. 放入有序表的东西,如果不是基础类型,必须提供比较器,内部按照引用传递,内存占用是这个东西内存的大小
  9. 不管是什么底层具体实现,只要是有序表,都是一下固定的基本功能和固定的时间复杂度
  1. void put(K key, V value):将一个(key,value)记录加入到表中,或者将key的记录更新成value
  2. V get(K key):根据给定的key,查询value并返回
  3. void remove(K key):溢出key的记录
  4. boolean containsKey(K key):查询时否关于key的记录
  5. K firstKey():返回所有键值的排序结果中,最左(最小)的那个
  6. K lastKey():返回所有键值的排序结果中,最右(最大)的那个
  7. K floorKey(K key):如果表中存入过key,返回key;否则返回所有键值排序结果中,key的前一个
  8. K ceilingKey(K key):如果表中存入过key,返回key;否则返回所有键值排序结果中,key的后一个

以上所有操作时间复杂度时O(logN),N为有序表含有的记录数

三.链表

1.单链表结构

public static class Node{
    int value;
    Node next;
}

反转单向链表:要求时间复杂度O(N),额外空间复杂度O(1)

	public static class Node {
		public int value;
		public Node next;

		public Node(int data) {
			this.value = data;
		}
	}

	public static Node reverseList(Node head) {
		Node pre = null;
		Node next = null;
		while (head != null) {
			next = head.next;
			head.next = pre;
			pre = head;
			head = next;
		}
		return pre;
	}

2.双链表

public static class Node{
    int value;
    Node next;
    Node last;
}

反转双向链表:要求时间复杂度O(N),空间复杂度O(1)

	public static class DoubleNode {
		public int value;
		public DoubleNode last;
		public DoubleNode next;

		public DoubleNode(int data) {
			this.value = data;
		}
	}

	public static DoubleNode reverseList(DoubleNode head) {
		DoubleNode pre = null;
		DoubleNode next = null;
		while (head != null) {
			next = head.next;
			head.next = pre;
			head.last = next;
			pre = head;
			head = next;
		}
		return pre;
	}

3.判断一个链表是否是回文结构

题目:给定一个但蛋链表的头节点head,请判断该链表是否为回文结构

例子:1->2->1 返回true     1->2->2->1 返回true    15->6->15 返回true    1->2->3 返回false

要求

对于笔试,如果链表长度为N,时间复杂度达到O(N),额外空间复杂度达到O(N)

对于面试,如果链表长度为N,时间复杂度达到O(N),额外空间复杂度达到O(1)

 将链表依次压入栈中,然后不断弹栈并重前先后依次比较依次,如果全部一样就是回文。

更省空间的做法,只将链表有伴部分压栈,然后弹栈并依次和左半部分从左到右比较,如果组后栈空,说明是回文,相比上面的做法省了一半的栈空间。 

首先使用快慢指针,当快指针指向结尾时,慢指针指向中间位置。同时要设法将右半部分的指针全部反向,同时记录头元素和最后一个元素的位置,然后同时向中间靠拢,逐一比较是否相等来判断是否时回文,判断结束后还要设法将原先方向的指针返回去。(这样实现的空间复杂度是O(1))

	// need O(1) extra space
	public static boolean isPalindrome3(Node head) {
		if (head == null || head.next == null) {
			return true;
		}
		Node n1 = head;
		Node n2 = head;
		while (n2.next != null && n2.next.next != null) { // find mid node
			n1 = n1.next; // n1 -> mid
			n2 = n2.next.next; // n2 -> end
		}
		n2 = n1.next; // n2 -> right part first node
		n1.next = null; // mid.next -> null
		Node n3 = null;
		while (n2 != null) { // right part convert
			n3 = n2.next; // n3 -> save next node
			n2.next = n1; // next of right node convert
			n1 = n2; // n1 move
			n2 = n3; // n2 move
		}
		n3 = n1; // n3 -> save last node
		n2 = head;// n2 -> left first node
		boolean res = true;
		while (n1 != null && n2 != null) { // check palindrome
			if (n1.value != n2.value) {
				res = false;
				break;
			}
			n1 = n1.next; // left to mid
			n2 = n2.next; // right to mid
		}
		n1 = n3.next;
		n3.next = null;
		while (n1 != null) { // recover list
			n2 = n1.next;
			n1.next = n3;
			n3 = n1;
			n1 = n2;
		}
		return res;
	}

4.将单向链表按照某值划分成左边小,中间相等,右边大的形式

题目

给定一个单链表的头节点head,节点的值类型是整型,再给定一个整数pivot。实现一个调整链表的函数,将链表调整为左部分都是值小于pivot的节点,中间部分都是只等于pivot的节点,右部分是值大于pivot的节点。

【进阶】在实现原问题功能的基础上增加如下的要求

【要求】调整后所有小于pivot的节点之间的相对顺序和调整之前一样

【要求】调整后所有等于pivot的节点之间的相对顺序和调整之前一样

【要求】调整后所有大于pivot的节点之间的相对顺序和调整之前一样

【要求】时间复杂度请达到O(N),额外空间复杂度请达到O(1)

 如果是笔试,将节点全部放到数组中,在数组中用patatition,最后将数组中的数用节点串起来。

如果面试,不使用额外空间,仅需要6个变量。

链表比较灵活,可以使用用6个指针(<区域的头尾指针,=区域的头尾指针,>区域的头尾指针)维护几个指针,依次将数接到对应区域尾部,最后将三块区域串起来即可。

连接三个区域的时候一定要充分讨论是否有<\=\>区域。

	public static Node listPartition2(Node head, int pivot) {
		Node sH = null; // small head
		Node sT = null; // small tail
		Node eH = null; // equal head
		Node eT = null; // equal tail
		Node bH = null; // big head
		Node bT = null; // big tail
		Node next = null; // save next node
		// every node distributed to three lists
		while (head != null) {
			next = head.next;
			head.next = null;
			if (head.value < pivot) {
				if (sH == null) {
					sH = head;
					sT = head;
				} else {
					sT.next = head;
					sT = head;
				}
			} else if (head.value == pivot) {
				if (eH == null) {
					eH = head;
					eT = head;
				} else {
					eT.next = head;
					eT = head;
				}
			} else {
				if (bH == null) {
					bH = head;
					bT = head;
				} else {
					bT.next = head;
					bT = head;
				}
			}
			head = next;
		}
		// small and equal reconnect
		if (sT != null) {
			sT.next = eH;
			eT = eT == null ? sT : eT;
		}
		// all reconnect
		if (eT != null) {
			eT.next = bH;
		}
		return sH != null ? sH : eH != null ? eH : bH;
	}

5.复制还有随机指针节点的链表

 题目

一种特殊的单链表节点类描述如下:

class Node{
    int value;
    Node next;
    Node rand;
    Node(int val){
        value=val;
    }
}

rand指针是单链表节点结构中新增的指针,rand可能指向链表中的任意一个节点,也可能指向null。给定一个由Node节点类型组成的无环单链表的头节点head,请实现一个函数完成这个链表的复制,并返回复制的新链表的头节点。

【要求】时间复杂度O(N),额外空间复杂度O(1)

这里利用了哈希表实现了老节点和克隆节点的一一对应,随后遍历一遍老链表,根据老节点的连接关系为新链表构建相同的连接关系。 

	public static class Node {
		public int value;
		public Node next;
		public Node rand;

		public Node(int data) {
			this.value = data;
		}
	}

	public static Node copyListWithRand1(Node head) {
		HashMap<Node, Node> map = new HashMap<Node, Node>();
		Node cur = head;
		while (cur != null) {
			map.put(cur, new Node(cur.value));
			cur = cur.next;
		}
		cur = head;
		while (cur != null) {
			map.get(cur).next = map.get(cur.next);
			map.get(cur).rand = map.get(cur.rand);
			cur = cur.next;
		}
		return map.get(head);
	}

 如上图所示,也可以不用使用HashMap实现链表的复制

首先,将每个节点的克隆节点插入在cur和cur.next之间(此时cur.next就是对应节点的克隆节点)

随后,将节点一对一对处理rand指针,比如1节点的rand指向3,1的克隆节点是1.next,3的克隆节点是3,next,则可以将1的克隆节点的rand指针指向3的克隆节点。其他节点如上处理。

最后,将克隆链表从其中分离出来即可。

	public static Node copyListWithRand2(Node head) {
		if (head == null) {
			return null;
		}
		Node cur = head;
		Node next = null;
		// copy node and link to every node
		while (cur != null) {
			next = cur.next;
			cur.next = new Node(cur.value);
			cur.next.next = next;
			cur = next;
		}
		cur = head;
		Node curCopy = null;
		// set copy node rand
		while (cur != null) {
			next = cur.next.next;
			curCopy = cur.next;
			curCopy.rand = cur.rand != null ? cur.rand.next : null;
			cur = next;
		}
		Node res = head.next;
		cur = head;
		// split
		while (cur != null) {
			next = cur.next.next;
			curCopy = cur.next;
			cur.next = next;
			curCopy.next = next != null ? next.next : null;
			cur = next;
		}
		return res;
	}

6.两个单链表相交的一系列问题

题目

给定两个可能有环也可能无环的单链表,头节点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的第一个节点。如果不相交,返回null。

【要求】如果两个链表长度之和为N,时间复杂度请达到O(N),额外空间复杂度达到O(1)

判断一个链表是否有环:有环则返回进入环的第一个节点

可以使用哈希表来找到第一个入环节点, 如果一个链表有环,一定有会重复记录多个节点,当第一次发现一个节点已经在哈希表里,那么这个节点就是入环节点。

利用快慢指针判断是否有环,同时返回第一个入环的节点。(额外的空间复杂度为O(1))

如上图S所在位置是快慢指针相遇的位置。

魔性的结论:慢指针S从相遇位置开始,快指针F从头开始,F和S指针都每次走一步,当F和S相遇的节点就是第一个入环节点。 

	public static Node getLoopNode(Node head) {
		if (head == null || head.next == null || head.next.next == null) {
			return null;
		}
		Node n1 = head.next; // n1 -> slow
		Node n2 = head.next.next; // n2 -> fast
		while (n1 != n2) {
			if (n2.next == null || n2.next.next == null) {
				return null;
			}
			n2 = n2.next.next;
			n1 = n1.next;
		}
		n2 = head; // n2 -> walk again from head
		while (n1 != n2) {
			n1 = n1.next;
			n2 = n2.next;
		}
		return n1;
	}

情况一:链表一和链表二都无环 

 分别记录两个链表的终止节点,如果两个链表的终止节点是同一个节点,说明两个链表有相交。那么如何确定第一个相遇的节点呢?分别算出链表一长度len1和链表二长度len2,让短一点的链表先走|len1-len2|差值步,随后一起走,相遇的节点就是所求的相交的节点。

	public static Node noLoop(Node head1, Node head2) {
		if (head1 == null || head2 == null) {
			return null;
		}
		Node cur1 = head1;
		Node cur2 = head2;
		int n = 0;
		while (cur1.next != null) {
			n++;
			cur1 = cur1.next;
		}
		while (cur2.next != null) {
			n--;
			cur2 = cur2.next;
		}
		if (cur1 != cur2) {
			return null;
		}
		cur1 = n > 0 ? head1 : head2;
		cur2 = cur1 == head1 ? head2 : head1;
		n = Math.abs(n);
		while (n != 0) {
			n--;
			cur1 = cur1.next;
		}
		while (cur1 != cur2) {
			cur1 = cur1.next;
			cur2 = cur2.next;
		}
		return cur1;
	}

情况二:一个链表无环,一个链表有环

不存在这样的情况。

情况三:两个链表都有环

如何区分三种情况:(loop1、loop2分别是两个有环链表的第一个入环节点)

若loop1==loop2则是情况二,那么如何找到第一个相交的节点呢?可以把第一个入环节点看成两个链表的终止位置 => 变成了求两个无环链表第一个相交的节点

若loop1!=loop2则是情况一或情况三,那么如何区分这两种情况呢? 可以发现如果从loop1出发回到loop1的过程中遇到loop2则是情况三,遇不到则是情况一。

	public static Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) {
		Node cur1 = null;
		Node cur2 = null;
		if (loop1 == loop2) {
			cur1 = head1;
			cur2 = head2;
			int n = 0;
			while (cur1 != loop1) {
				n++;
				cur1 = cur1.next;
			}
			while (cur2 != loop2) {
				n--;
				cur2 = cur2.next;
			}
			cur1 = n > 0 ? head1 : head2;
			cur2 = cur1 == head1 ? head2 : head1;
			n = Math.abs(n);
			while (n != 0) {
				n--;
				cur1 = cur1.next;
			}
			while (cur1 != cur2) {
				cur1 = cur1.next;
				cur2 = cur2.next;
			}
			return cur1;
		} else {
			cur1 = loop1.next;
			while (cur1 != loop1) {
				if (cur1 == loop2) {
					return loop1;
				}
				cur1 = cur1.next;
			}
			return null;
		}
	}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

看未来捏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值