左程云算法笔记(四)哈希表和有序表的使用、链表

哈希表的使用

  1. 哈希表在使用层面上可以理解为一种集合结构
  2. 如果只有key,没有伴随数据value,可以使用HashSet结构
  3. 如果既有key,又有伴随数据value,可以使用HashMap结构
  4. 使用哈希表增(put) 删(remove) 改(put) 查(get) 操作,可以认为时间复杂度为O(1),但常数时间比较大
  5. 放入哈希表的东西,如果是基础类型,内部按值传递,内存占用就是这个数的大小
  6. 放入哈希表的东西,如果不是基础类型,内部按引用传递,内存占用是这个东西内存地址的大小(8字节)
HashMap<String, Integer> map = new HashMap;
map.put("zuochengyun", 30);
System.out.println(map.containsKey("zuochengyun”)); // true
System.out.println(map.containsKey("zuo”)); // false
System.out.println(map.get("zuochengyun”)); // 30

map.put("zuochengyun", 32);
System.out.println(map.get("zuochengyun”)); // 32

map.remove("zuochengyun");
System.out.println(map.containsKey("zuochengyun”)); // false
System.out.println(map.get("zuochengyun”)); // null

map.put("zuochengyun", 30);
String test1 = "zuochengyun";
String test2 = "zuochengyun";
System.out.println(map.containsKey(test1)); // true
System.out.println(map.containsKey(test2)); // true

HashMap<Integer, Sring> map2 = new HashMap<>();
map2.put(123, "hello");
Integer a = 123;
Integer b = 123;
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true
System.out.println(map2.containsKey(a)); // true
// Integer, Double, Flow, Char, String 在哈希表中都按值传递
public static class Node() {
	public int value;
	public Node(int v) {
		value = v;
	}
}

Node node1 = new Node(1);
Node node2 = new Node(1);
HashMap<Node, String> map3 = new HashMap<>();
map3.put(node1, "hello");
System.out.println(map3.containsKey(node1)); // true
System.out.println(map3.containsKey(node2)); // false
// 非原生类型按引用传递

有序表的使用

  1. 有序表在使用层面上可以理解为一种集合结构
  2. 如果只有key,没有伴随数据value,可以使用TreeSet结构
  3. 如果既有key,又有伴随数据value,可以使用TreeMap结构
  4. 有无伴随数据是TreeSet和TreeMap唯一的区别,底层的实际结构是一样的
  5. 有序表和哈希表的区别是,有序表把key按照顺序组织起来,而哈希表完全不组织
  6. 红黑树、AVL树、size-balance-tree和跳表等都属于有序表结构,只是底层具体实现不同
  7. 使用有序表增(put) 删(remove) 改(put) 查(get) 操作,可以认为时间复杂度都是O(logN)
  8. 放入有序表的东西,如果是基础类型,内部按值传递,内存占用就是这个数的大小
  9. 放入有序表的东西,如果不是基础类型,必须提供比较器,内部按引用传递,内存占用是这个东西内存地址的大小(8字节)
TreeMap<Integer, String> treeMap1 = new TreeMap<>();
treeMap1.put(7, "我是7");
treeMap1.put(5, "我是5");
treeMap1.put(4, "我是4");
treeMap1.put(3, "我是3");
treeMap1.put(9, "我是9");
treeMap1.put(2, "我是2");
// 最小的key
System.out.println(treeMap.firstKey()); // 2
// 最大的key
System.out.println(treeMap.lastKey()); // 9
// <=8的key中离8最近的
System.out.println(treeMap.floorKey(8)); // 7
// >=8的key中离8最近的
System.out.println(treeMap.ceilingKey(8)); // 9

Node node3 = new Node(3);
Node node4 = new Node(4);
TreeMap<Node, String> treemMap2 = new treeMap<>();
// 会报错。treeMap中添加的key必须是可以比较的东西。需在上一行的括号中添加比较器
treeMap2.put(node3, "hello3"); 
treeMap2.put(node4, "hello4"); 

public static class NodeComparator implements Comparator<Node> {
	@Override
	public int compare(Node o1, Node o2) {
		return o1.value-o2.value; // 升序排列
	}
}
TreeMap<Node, String> treemMap3 = new treeMap<>(new NodeComparator);
// 不再报错
treeMap2.put(node3, "hello3"); 
treeMap2.put(node4, "hello4"); 

链表

链表的各个节点不一定是连续存放的

单链表的节点结构:
Class Node {
V value;
Node next;
}

双链表的节点结构:
Class Node {
V value;
Node next;
Node last;
}

单链表和双链表只需给定一个头部节点head,就可以找到剩下的所有节点

单链表反转 (LC206)
public ListNode reverseList(ListNode head) {
    ListNode next = null; //用于暂存当前节点的next
    ListNode prev = null;
    while (head != null) {
        next = head.next;
        head.next = prev;
        prev = head;
        head = next;
    }
    return prev;
}
双向链表反转
public DoubleNode reverseDoubleList(DoubleNode head) {
	DoubleNode prev = null;
	DoubleNode next = null;
	while (head != null) {
		next = head.next;
		head.next = prev;
		head.prev = next;
		prev = head;
		head = next;
	}
	return prev;
}
打印两个有序链表的公共部分

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

思路:类似mergeSort中merge的过程。从头开始遍历两个链表,每次移动值小的指针(因为大的要等小的追上来才有相等的可能),相等的时候就打印节点的值,并同时移动两个链表的指针,直到某个链表的遍历结束。

合并两个有序链表(LC21)

思路:和上一题类似,也是使用merge的过程。首先比较head1和head2的value大小,小的作头和返回值;然后一次比较两个指针,取值小的作next,直至其中一个指向null,再将另一个非空指针作next。

public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
    if (list1 == null) {
        return list2;
    }
    if (list2 == null) {
        return list1;
    }
    ListNode head = list1.val <= list2.val ? list1 : list2;
    ListNode cur1 = head.next;
    ListNode cur2 = head == list1 ? list2 : list1;
    ListNode temp = head;
    while (cur1 != null && cur2 != null) {
        if (cur1.val <= cur2.val) {
            temp.next = cur1;
            cur1 = cur1.next;
        } else {
            temp.next = cur2;
            cur2 = cur2.next;
        }
        temp = temp.next;
    }
    temp.next = cur1 != null ? cur1 : cur2;
    return head;
}
判断一个链表是否为回文结构 (LC234)

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

空间复杂度O(N)的做法:开辟一个stack,将链表中的节点按顺序放入栈。放完后依次弹出栈里的内容,顺序即是和链表相反。依次比较弹出的内容和链表的节点,如果每个都一样即为回文链表。

省一半空间的做法:只把链表的后半部分放入栈,然后依次比较链表节点(from head)和弹出的东西。得到后半部分的方法:快慢指针

空间复杂度O(1)的做法:
左:快慢指针找到终点和结尾,反转后半部分链表,中点指向null;从头和尾分别开始check是否一致,有一个走到null停止;若每一步都一样则为回文链表。返回结果之前要将后半部分反转回去。
改:快慢指针找到中点(偶数时右中点,奇数时中点),找中点的同时反转前部半分链表
偶数情况: 1 <- 2 <- 3 4 -> 5 -> 6 (prev=3, slow=4, fast=null)
奇数情况: 1 <- 2 <- 3 4 -> 5 -> 6 -> 7 (prev=3, slow=4, fast=7)
奇数时从slow.next开始和prev进行比较;比较的同时恢复前半部分链表

// O(N) extra space
public static boolean isPalindrome1(ListNode head) {
    Stack<ListNode> stack = new Stack<>();
    ListNode curr = head;
    while (curr != null) {
        stack.push(curr);
        curr = curr.next;
    }
    while (!stack.isEmpty()) {
        if (head.val != stack.pop().val) {
            return false;
        } else {
            head = head.next;
        }
    }
    return true;
}

// O(N/2) extra space
public static boolean isPalindrome2(ListNode head) {
    if (head == null || head.next == null) {
        return true;
    }
    ListNode slow = head;
    ListNode fast = head;
    Stack<ListNode> stack = new Stack<>();
    while (fast.next != null && fast.next.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    while (slow != null) {
        stack.push(slow);
        slow = slow.next;
    }
    while (!stack.isEmpty()) {
        if (head.val != stack.pop().val) {
            return false;
        } else {
            head = head.next;
        }
    }
    return true;
}

// O(1) extra space
public static boolean isPalindrome3(ListNode head) {
    ListNode slow = head;
    ListNode fast = head;
    ListNode prev = null;
    ListNode nextTemp = null;
    while (fast != null && fast.next != null) { // slow在偶数时为右中点,奇数时为中点
        fast = fast.next.next;
        // 更新slow的同时反转链表(slow前一个node,即prev开始往前是逆序的)
        nextTemp = slow.next;
        slow.next = prev;
        prev = slow;
        slow = nextTemp;
    }
    // 此时的状况:
    // 偶数情况: 1 <- 2 <- 3  4 -> 5 -> 6 (prev=3, slow=4, fast=null)
    // 奇数情况: 1 <- 2 <- 3  4 -> 5 -> 6 -> 7 (prev=3, slow=4, fast=7)
    ListNode prepre = slow; // 之后要恢复前半段链表,slow应作为prev.next但比较时slow会后移,所以先记下slow的位置
    if (fast != null) { // fast在奇数是是最后一个数,偶数时是null;这里为奇数情况
        slow = slow.next; // slow本来为中点,但不需要比较中点,从中点后一个开始和prev比较
    }
    // prev和slow分别作为前半段和后半段的头开始比较
    boolean flag = true;
    while (prev != null) {
        if (prev.val != slow.val) {
            flag = false;
        } 
        slow = slow.next;
        // 恢复前半段链表
        nextTemp = prev.next;
        prev.next = prepre;
        prepre = prev;
        prev = nextTemp;
    }
    return flag;
}
将单链表按某值划分成左边小、中间相等、右边大的形式 (LC86 升级版)

思路:
开辟6个空间:SH, ST, EH, ET, BH, BT (small head/tail, equal head/tail, Big head/tail);
遍历链表,若小于pivot:若SH为null,则将其设置为小于区的头和尾;若SH不为null,则将ST.next设为该节点,并将ST更新为该节点。其他情况同理。
最后将SH, ST, EH, ET, BH, BT连接起来,但要注意其中有区域为空的情况。

复制带随机指针的链表 (LC138)

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

思路:
空间复杂度O(N)的做法:新建一个hashmap,遍历原链表,将原节点存为key,copy节点存为value {node1->node1’, node2->node2’, node3, node3’…}。再遍历一遍原链表,原节点中的next,rand对应的copy同样赋给copy节点。

空间复杂度O(1)的做法:遍历原链表,对于每个原节点创建一个copy节点放在原节点和原节点.next之间,node1 -> node1’ -> node2 -> node2’ -> node3 -> node3’…之后再遍历一遍原链表,将原节点的rand对应的copy赋给原节点的copy,即node1’.rand = node1.rand.next。最后第三遍遍历原链表,分离原节点和copy节点。

// extra space O(N)
public Node copyRandomList1(Node head) {
    HashMap<Node, Node> map = new HashMap<>();
    Node curr = head;
    while (curr != null) {
        map.put(curr, new Node(curr.val));
        curr = curr.next;
    }
    curr = head;
    while (curr != null) {
        // curr 老节点
        // map.get(curr) 复制节点
        map.get(curr).next = map.get(curr.next);
        map.get(curr).random = map.get(curr.random);
        curr = curr.next;
    }
    return map.get(head);
}


// extra space O(1)
public Node copyRandomList2(Node head) {
    if (head == null) {
        return null;
    }
    Node curr = head;
    while (curr != null) {
        Node nextTemp = curr.next;
        Node copy = new Node(curr.val);
        curr.next = copy;
        copy.next = nextTemp;
        curr = nextTemp;
    }
    curr = head;
    while (curr != null) {
        Node copy = curr.next;
        Node next = copy.next;
        copy.random = curr.random != null ? curr.random.next : null;
        curr = next;
    }
    curr = head;
    Node res = head.next;
    while (curr != null) {
        Node copy = curr.next;
        Node next = copy.next;
        curr.next = next;
        copy.next = next != null ? next.next : null;
        curr = next;
    }
    return res;
}
链表相交 (LC160 升级版,前置问题LC141/142)

给定两个可能有环也可能无环的单链表head1和head2。若两链表相交则返回相交的第一个节点;否则返回null。

LC160版本中明确两个链表均不带环。

step1: 判定两个链表是否带环
思路:快慢指针从头开始走,若快指针走到null则说明无环,若两者相遇则说明有环。此时将快指针移动到链表头,慢指针不动,两个指针每次都走一步,它们再次相遇的地方即为入环处。
证明:
(1) 假设从链表头到入环点有 a a a个节点,环的长度为 b b b个节点,则整个链表的长度为 a + b a+b a+b。快指针走两步,慢指针走一步,则有 f = 2 s f=2s f=2s。快指针和慢指针相遇时,快指针比慢指针多走的长度一定是环长的倍数,即 f = s + n b f=s+nb f=s+nb
(2) 由这两个式子可得 s = n b s=nb s=nb, f = 2 n b f=2nb f=2nb,即快、慢指针分别走了 2 n b 2nb 2nb n b nb nb个环长。
(3) 如果让一个指针从链表头部一直向下走并统计步数 k k k,那么所有 走到链表入口节点时的步数 是: k = a + n b k=a+nb k=a+nb(先走 a a a 步到入环处,之后每绕 1 圈环( b 步)都会再次到入口节点)。
(4) 目前,慢指针走过的步数为 n b nb nb步,即只要再走 a a a步就可以到入环处。所以此时若另一个指针从链表头开始走,每次走一步,则该指针会和慢指针在 a a a步后在入环处相遇。

public ListNode detectCycle(ListNode head) {
    if (head==null || head.next==null || head.next.next==null) {
        return null;
    }
    ListNode slow = head.next;
    ListNode fast = head.next.next;
    while (fast != slow) {
        if (fast == null || fast.next == null) {
            return null;
        }
        slow = slow.next;
        fast = fast.next.next;
    }
    fast = head;
    while (fast != slow) {
        slow = slow.next;
        fast = fast.next;
    }
    return slow;
}

step2:分类讨论

  • 若两个链表都无环(LC160):
    额外空间复杂度O(n):
    遍历list1,将每个节点放入hashSet;遍历list2,每到一个新节点就check是否在hashSet中,若在,则说明这个节点是相交节点;否则继续遍历直到null。
    额外空间复杂度O(1):
    分别从head1和head2往下走,同时记录list1和list2的长度。若最后一个node是同一个,则说明两个链表相交,否则不相交。
    若相交:长度更长的链表先从头开始走掉两个链表的长度差,然后两个head同时往下走,走到相同处即为相交点。
    另一种更简练的写法见LC160官方题解。
// 如果两个链表都无环,返回第一个相交节点,如果不想交,返回null
public ListNode noLoopFindIntersect(ListNode headA, ListNode headB) {
    if (headA == null || headB == null) {
        return null;
    }
    ListNode cur1 = headA;
    ListNode cur2 = headB;
    int len = 0;
    boolean intersect = false;
    while (cur1.next != null) {
        cur1 = cur1.next;
        len++;
    }
    while (cur2.next != null) {
        cur2 = cur2.next;
        len--;
    }
    if (cur1 != cur2) { // 结尾点不同则说明不相交
        return null;
    } else { // 否则说明相交
        // cur1为长链表的头,cur2为短链表的头
        cur1 = len>0 ? headA : headB;
        cur2 = cur1==headA ? headB : headA;
        len = Math.abs(len);
        while (len != 0) {
            cur1 = cur1.next;
            len--;
        }
        while (cur1 != null) {
            if (cur1 == cur2) {
                return cur1;
            } 
            cur1 = cur1.next;
            cur2 = cur2.next;
        }
    }
    return null;
}
  • 若一个链表有环,一个无环,则必定不可能相交
  • 若两个都有环,则又分三种情况:
    (1) 入环点不同,两链表不相交
    (2) 入环点相同,两链表相交
    (3) 入环点不同,两链表相交
    请添加图片描述
    -对于(2),只要将入环点当作链表结束点即可,按照无环链表相同的方法处理。
    -区分(1)(3),继续从loop1(list1的入环点)向下走,若回到loop1之前遇到了loop2则为情况 (3),返回loop1或loop2都对;若没有遇到loop2,则说明是情况(1),两链表不相交。
// 主函数
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
	if (headA == null || headB == null) {
		return null;
	}
	ListNode loop1 = detectCycle(headA);
	ListNode loop2 = detectCycle(headB);
	if (loop1 == null && loop2 == null) { //两个无环链表
		return noLoopFindIntersect(headA, headB); 
	}
	if (loop1 != null && loop2 != null) { //两个链表都有环
		return bothLoopFindIntersect(headA, loop1, headB, loop2);
	}
	return null; // 一个有环一个无环,必定无交点
}


// 两个有环链表,返回第一个相交节点,如果不想交返回null
public ListNode bothLoopFindIntersect(ListNode headA, ListNode loop1, ListNode headB, ListNode loop2) {
	ListNode cur1 = null;
	ListNode cur2 = null;
	if (loop1 == loop2) { // 情况2
		// 和无环链表相同的处理方法,唯独把终点判断条件从null改成了loop1
	    int len = 0;
	    boolean intersect = false;
	    while (cur1.next != loop1) {
	        cur1 = cur1.next;
	        len++;
	    }
	    while (cur2.next != loop2) {
	        cur2 = cur2.next;
	        len--;
	    }
	    if (cur1 != cur2) { // 结尾点不同则说明不相交
	        return null;
	    } else { // 否则说明相交
	        // cur1为长链表的头,cur2为短链表的头
	        cur1 = len>0 ? headA : headB;
	        cur2 = cur1==headA ? headB : headA;
	        len = Math.abs(len);
	        while (len != 0) {
	            cur1 = cur1.next;
	            len--;
	        }
	        while (cur1 != null) {
	            if (cur1 == cur2) {
	                return cur1;
	            } 
	            cur1 = cur1.next;
	            cur2 = cur2.next;
	        }
	    }
	    return null;
	} else { // 情况1或3
		cur1 = loop1.next;
		while (cur1 != loop1) {
			if (cur1 == loop2) {
				return loop1; // 情况3
			}
			cur1 = cur1.next;
		}
		return null; // 情况1
	}
}

Reference:
Java的Comparator升序降序的记法
Leetcode234-题解-蓝黑R9
Leetcode138-题解-林小鹿
Leetcode142-题解-jyd
LeetCode160-官方题解

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值