【左程云 数据结构与算法笔记】P6 链表

297 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第14天,点击查看活动详情

下面是我整理的跟着b站左程云的数据结构与算法学习笔记,欢迎大家一起学习。

排序方法总结

综合排序

快速排序与插入排序的融合使用 当数据样本比较大时,使用快速排序 时间复杂度O(N*logN)利用调度优势 当数据样本比较小时,使用插入排序,时间复杂度O(N^2) 利用小样本常数空间低的优势 在数据量比较小时,N^2 的瓶颈不明显 利用各自算法的优势让整体时间

工程上对排序的改进

  1. 一般会充分利用O(N*logN)和O(N^2)排序各自的优势
  2. 考虑稳定性

哈希表

  1. 在使用层面上可以理解为一种集合结构
  2. 如果只有key,没有伴随数据value,可以使用HashSet结构
  3. 如果既有key,又有伴随数据value,可以使用HashMap结构
  4. 有无伴随数据,是HashMap和HashSet唯一的区别,但底层的实际结构是另一回事
  5. 使用哈希表增删改查时,可以认为时间复杂度为O(1),但常数时间比较大
  6. 放入哈希表的东西,若为基础类型,直接拷贝,内部按值传递,内存占用是这个东西的大小
  7. 放入哈希表的东西,若不是基础类型,内部按引用传递,内存占用就是这个东西内存地址的大小(一律八字节)

有序表

  1. 在使用层面上可以理解为一种集合结构
  2. 若只有key,没有伴随数据value,可以使用TreeSet结构
  3. 若既有key,又有伴随数据value,可以使用TreeMap结构
  4. 有无伴随数据,是TreeMap和TreeSet唯一的区别,但底层的实际结构是另一回事
  5. 红黑树,AVL树,size-balance-tree和跳表都属于有序表结构,性能一样,时间指标也一样,只是底层具体实现不同
  6. 放入有序表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小
  7. 放入有序表的东西,如果不是基础类型,必须提供比较器,内部按引用传递,内存占用即这个东西内存地址的大小
  8. 只要是有序表,都有固定的基本功能和固定的时间复杂度 代码实现TreeMap
public static void main(String[] args) {  
    // 展示有序表常用操作  
    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");  
    System.out.println(treeMap1.containsKey(5));  
    System.out.println(treeMap1.get(5));  
    System.out.println(treeMap1.firstKey() + ", 我最小");  
    System.out.println(treeMap1.lastKey() + ", 我最大");  
    System.out.println(treeMap1.floorKey(8) + ", 在表中所有<=8的数中,我离8最近");  
    System.out.println(treeMap1.ceilingKey(8) + ", 在表中所有>=8的数中,我离8最近");  
    System.out.println(treeMap1.floorKey(7) + ", 在表中所有<=7的数中,我离7最近");  
    System.out.println(treeMap1.ceilingKey(7) + ", 在表中所有>=7的数中,我离7最近");  
    treeMap1.remove(5);  
    System.out.println(treeMap1.get(5) + ", 删了就没有了哦");  
    System.out.println("========6=========");  
}

时间复杂度为O (logN) 有序表的基本功能

  • void put(K key,V value):增加或修改
  • V get(K key):通过key获取value
  • void remove(K key)移除key的记录
  • Boolean containsKey(K key)查询指定key是否存在
  • K firstKey() 返回所有键值的排序结果中,最左(小)的key
  • K lastKey()返回所有键值的排序结果中,最右(大)的key
  • K floorKey(K key) 若存入过key,返回key,否则返回key的前一个
  • K ceilingKey(K key)若存入过key,返回key,否则返回key的 后一个 同样的用法还有各自的value方法,所有操作的时间复杂度都为O(logN)相比之下虽然hash表的增删改查时间复杂度为常数但也很小了

链表

构造一个简单的链表

代码实现

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

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

打印两个表的公共数据

解题思路:首先得是一个有序数组,谁小谁移动,打印公共部分并都向后面移动 代码比较简单就不一一列出了 链表解题的方法论

  • 笔试重视时间复杂度
  • 面试时间复杂度依然重要,但要找到空间最省的方法

认识回文

正着念与反着念相同,相当于有一个对称轴,左右成镜像对称 如 1 2 1 实现思路:将链表压进栈里,不断弹出与原来链表比较 可以将对称轴后的东西压进栈里,减少空间 也可以使用一个快指针和一个慢指针,快指针一次走两步,慢指针一次走一步 代码实现

public static boolean isPalindrome2(Node head) {
		if (head == null || head.next == null) {
			return true;
		}
		Node right = head.next;
		Node cur = head;
		while (cur.next != null && cur.next.next != null) {
			right = right.next;
			cur = cur.next.next;
		}
		Stack<Node> stack = new Stack<Node>();
		while (right != null) {
			stack.push(right);
			right = right.next;
		}
		while (!stack.isEmpty()) {
			if (head.value != stack.pop().value) {
				return false;
			}
			head = head.next;
		}
		return true;
	}

也可能存在边界问题 使用最小的空间复杂度O(1)方法 在指针走到最中间时,将中间点后面的链表倒置,并将第一个与最后一个数标识

代码实现

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;
	}

使用链表实现简单的快排 稳定性 使用六个变量 小于区域的头尾指针,还有大于和等于区域 当出现第一个值时(头和尾都为null)将头和尾都设置为该值,当该值第二次出现时,将尾巴的下一个指向该值,并将尾巴设置为该值,即为一个小于区域的链表 在最后小于区域的尾连接等于区域的头,等于区域的尾指向大于区域的头(同时也要判断各个区域是否会为null)

代码实现:


	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;
	}

复制含有随机指针节点的链表

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

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

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

实现思路1:用一个hashMap key为老节点,value为新节点

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);
	}

思路2 在该节点后设置一个对应的新节点,1的rand的next就是1'需要对应的rand指针,一对一对就能将rand指针设置好,最后,在新老链表不断next将新老链表分离出来

思路总结:用链表将hashmap省掉

代码实现

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;
	}