鏈表基礎知識
鏈表是一種物理存儲單元上非連續、非順序的存儲結構,數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的。鏈表由一系列結點(鏈表中每一個元素稱爲結點)組成,結點可以在運行時動態生成。每個結點包括兩個部分:一個是存儲數據元素的數據域,另一個是存儲下一個結點地址的指針域。 相比於線性表順序結構,操作複雜。由於不必須按順序存儲,鏈表在插入的時候可以達到O(1)的複雜度,比另一種線性表順序錶快得多,但是查找一個節點或者訪問特定編號的節點則需要O(n)的時間,而線性表和順序表相應的時間複雜度分別是O(logn)和O(1)。
使用鏈表結構可以克服數組鏈表需要預先知道數據大小的缺點,鏈表結構可以充分利用計算機內存空間,實現靈活的內存動態管理。但是鏈表失去了數組隨機讀取的優點,同時鏈表由於增加了結點的指針域,空間開銷比較大。鏈表最明顯的好處就是,常規數組排列關聯項目的方式可能不同於這些數據項目在記憶體或磁盤上順序,數據的存取往往要在不同的排列順序中轉換。鏈表允許插入和移除表上任意位置上的節點,但是不允許隨機存取。鏈表有很多種不同的類型:單向鏈表,雙向鏈表以及循環鏈表。
需要注意的是,在Java中沒有指針的概念,而類似指針的功能都是通過引用來實現的,爲了便於理解,我們仍然使用指針(可以認爲引用與指針是類似的)來進行描述,而在實現的代碼中,都是通過引用來建立結點之間的關係。
鏈表的分類
單向鏈表
單向鏈表(單鏈表)是鏈表的一種,其特點是鏈表的鏈接方向是單向的,對鏈表的訪問要通過順序讀取從頭部開始;
單鏈表節點:
class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
雙向鏈表
雙向鏈表也叫雙鏈表,是鏈表的一種,它的每個數據結點中都有兩個指針,分別指向直接後繼和直接前驅。所以,從雙向鏈表中的任意一個結點開始,都可以很方便地訪問它的前驅結點和後繼結點。
雙向鏈表節點:
class ListNode {
int value;
ListNode prev = null;
ListNode next = null;
ListNode(int x) {
value = x;
}
}
循環鏈表
循環鏈表是另一種形式的鏈式存貯結構。它的特點是表中最後一個結點的指針域指向頭結點,整個鏈表形成一個環。
循環鏈表可以是單向也可以是雙向,節點因可以是雙向節點也可以是單向節點
鏈表基本操作
237. 刪除鏈表中的節點
public void deleteNode(ListNode node) {
node.val = node.next.val;
node.next = node.next.next;
}
面試題18. 刪除鏈表的節點
- 通過記錄遍歷節點的前一個節點,一旦匹配上,前一個節點和後一個節點相連自然就刪掉目的節點
public ListNode deleteNode(ListNode head, int val) {
if (head.val == val) return head.next;
ListNode pre = head, suf = head.next;
while (suf != null && suf.val != val) {
pre = suf;
suf = suf.next;
}
if (suf != null) pre.next = suf.next;
return head;
}
206. 反轉鏈表
迭代模板:
- 只要將鏈表中兩兩之間的指向調換,就可以達到目的
- 注意需要一個指針指向下一個值不然在換的過程中會丟失進度
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while (cur != null) {
ListNode tmp = cur.next;
cur.next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
遞歸模板:
- 因爲是遞歸,最先操作的一定是最後一個節點,因此遞歸的返回判斷就是最後一個節點
head.next == null
- 我們需要將最後一個節點可前一個節點調轉指向
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) return head;
ListNode pre = reverseList(head.next);
head.next.next = head;
head.next = null;
return pre;
}
25. K 個一組翻轉鏈表
- K個一組,每組內部通過上面的復轉鏈表做
- 通過遞歸分組拼接
class Solution {
public static ListNode reverseKGroup(ListNode head, int k) {
if (head == null) return null;
ListNode cur = head;
int count = 0;
while (cur != null && count != k) {
cur = cur.next;
count++;
}
if (count == k) {
cur = reverseKGroup(cur, k);
while (count != 0) {
count--;
ListNode tmp = head.next;
head.next = cur;
cur = head;
head = tmp;
}
head = cur;
}
return head;
}
}
21. 合併兩個有序鏈表
迭代模板:
public static ListNode mergeTwoLists(ListNode l1, ListNode l2){
ListNode head = new ListNode(-1);
ListNode tmp=head;
while(l1!=null&&l2!=null){
if (l1.val<l2.val){
tmp.next = l1;
l1 = l1.next;
}else{
tmp.next = l2;
l2 = l2.next;
}
tmp = tmp.next;
}
tmp.next = l1==null?l2:l1;
return head.next;
}
遞歸模板:
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) {
return l2;
}
else if (l2 == null) {
return l1;
}
else if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
}
else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
23. 合併K個排序鏈表
- 使用分治的思想,通過上面的合併兩個鏈表,將多個鏈表合併
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
int len = lists.length;
if (len==0) return null;
return split(lists,0,len-1);
}
private ListNode split(ListNode[] lists, int start, int end) {
if (start == end) {
return lists[start];
}else if(end-start==1){
return merge2Lists(lists[start], lists[end]);
}else if (end-start>1){
int mid = (end+start)>>1;
ListNode left = split(lists, start, mid);
ListNode right = split(lists, mid+1, end);
return merge2Lists(left, right);
}
return null;
}
private ListNode merge2Lists(ListNode n1, ListNode n2) {
ListNode head = new ListNode(-1);
ListNode tmp = head;
while (n1!=null&&n2!=null){
if (n1.val >= n2.val) {
tmp.next = n2;
n2 = n2.next;
}else{
tmp.next = n1;
n1 = n1.next;
}
tmp=tmp.next;
}
tmp.next = n1 != null?n1:n2;
return head.next;
}
}
雙向鏈表應用
146. LRU緩存機制
- 通過雙向鏈表進行緩存
- 添加:在頭部,保持頭部是最新訪問的,尾部是最久未使用的
- 操作某個node的話,將該node刷新的head
- 如果超出緩存長度,則刪掉末尾那個,再添加新的
public class LRUCache {
private HashMap<Integer, Node> cache;
private Integer cap;
private Node tail = new Node(-1, -1);
private Node head = new Node(-1, 1);
public LRUCache(int capacity) {
cache = new HashMap<Integer, Node>(capacity);
cap = capacity;
this.head.suf = tail;
this.tail.pre = head;
}
public int get(int key) {
if (!cache.containsKey(key)){
return -1;
}else{
Node node = cache.get(key);
refresh(node);
return node.value;
}
}
public void put(int key, int value) {
if (cache.containsKey(key)){
Node node = cache.get(key);
node.value = value;
refresh(node);
}else {
if(cache.size() == cap){
cache.remove(tail.pre.key);
Node removedPreNode = tail.pre.pre;
removedPreNode.suf = tail;
tail.pre = removedPreNode;
}
Node addNode = new Node(key,value);
cache.put(key, addNode);
head.suf.pre = addNode;
addNode.suf = head.suf;
head.suf = addNode;
addNode.pre = head;
}
}
public void refresh(Node node){
Node pre = node.pre, suf = node.suf;
pre.suf = suf;
suf.pre = pre;
head.suf.pre = node;
node.suf = head.suf;
head.suf = node;
node.pre = head;
}
}
class Node {
int key;
int value;
Node pre = null;
Node suf = null;
public Node() {}
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
460. LFU緩存
- 相對於LRU多了訪問次數的存儲
- 通過訪問次數進行重新排序
public class LFUCache {
private HashMap<Integer, Node> cache;
private Integer cap;
private Node tail = new Node(-1, -1, Integer.MIN_VALUE);
private Node head = new Node(1, 1, Integer.MAX_VALUE);
public LFUCache(int capacity) {
cache = new HashMap<Integer, Node>(capacity);
cap = capacity;
this.head.suf = tail;
this.tail.pre = head;
}
public int get(int key) {
if (cap <= 0 || !cache.containsKey(key)){
return -1;
}else{
Node node = cache.get(key);
node.freq++;
node.refresh();
return node.value;
}
}
public void put(int key, int value) {
if (cap > 0){
if (cache.containsKey(key)){
Node node = cache.get(key);
node.value = value;
node.freq ++ ;
node.refresh();
}else {
if(cache.size() == cap){
cache.remove(tail.pre.key);
Node removedPreNode = tail.pre.pre;
removedPreNode.suf = tail;
tail.pre = removedPreNode;
}
Node addNode = new Node(key,value);
cache.put(key, addNode);
addNode.pre = tail.pre;
addNode.suf = tail;
tail.pre.suf = addNode;
tail.pre = addNode;
addNode.refresh();
}
}else{
System.out.println("capacity smaller then 1");
}
}
}
class Node {
int key;
int value;
int freq = 0;
Node pre = null;
Node suf = null;
public Node() {}
public Node(int key, int value) {
this.key = key;
this.value = value;
}
public Node(int key, int value, int freq) {
this.key = key;
this.value = value;
this.freq = freq;
}
public void refresh(){
Node preNode = this.pre, sufNode = this.suf;
if (preNode.freq <= this.freq) {
preNode.suf = sufNode;
sufNode.pre = preNode;
while (preNode.freq <= this.freq) {
preNode = preNode.pre;
}
sufNode = preNode.suf;
preNode.suf = sufNode.pre = this ;
this.pre = preNode;
this.suf = sufNode;
}
}
}
持續更新中。。。。。。。。。。。