2022年1月4日|5日|6日|7日|
生活随笔
收集整理的這篇文章主要介紹了
2022年1月4日|5日|6日|7日|
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
2022年1月第一周周記
圖算法
【思路】對于不同的圖算法,可能牽扯到不同方式的圖表達。所以選擇一個普適性強的圖結構來表達所有圖算法基本圖結構
package class06;import java.util.HashMap; import java.util.HashSet; import java.util.ArrayList;public class GraphGenerator {public static Graph createGraph(Integer[][] matrix) {// 數組記錄出發節點(編號)和目的節點(編號)以及權重Graph graph = new Graph();for (int i = 0; i < matrix.length; i++) {Integer weight = matrix[i][0];Integer from = matrix[i][1];Integer to = matrix[i][2];// 如果該邊的出入節點不在圖結構中 ==> 添加// key:節點編號// value:以節點編號為值的節點if (!graph.nodes.containsKey(from)) {graph.nodes.put(from, new Node(from));}if (!graph.nodes.containsKey(to)) {graph.nodes.put(to, new Node(to));}// 以出發節點和目的節點構造邊Node fromNode = graph.nodes.get(from);Node toNode = graph.nodes.get(to);Edge newEdge = new Edge(weight, fromNode, toNode);// 將目的節點添加至出發節點的連接數組中// 出發節點出度++// 目的節點入度++// 將上步構造好的邊加入到節點的連接邊數組中fromNode.nexts.add(toNode);fromNode.out++;toNode.in++;fromNode.edges.add(newEdge);graph.edges.add(newEdge);}return graph;}public class Graph {public HashMap<Integer,Node> nodes;public HashSet<Edge> edges;public Graph() {nodes = new HashMap<>();edges = new HashSet<>();}}public class Edge {public int weight;public Node from;public Node to;public Edge(int weight, Node from, Node to) {this.weight = weight;this.from = from;this.to = to;}}public class Node {public int value;public int in;public int out;public ArrayList<Node> nexts;public ArrayList<Edge> edges;public Node(int value) {this.value = value;in = 0;out = 0;nexts = new ArrayList<>();edges = new ArrayList<>();}} }圖的寬度優先遍歷
【思路】 1、利用隊列實現 2、從源節點開始依次按照寬度進隊列,然后彈出 3、每彈出一個點,把該點所有沒有進過隊列的鄰接點放入隊列 4、直到隊列變空 ------------------------------- 圖和二叉樹的BFS區別在于,圖可能存在環結構,如果按照二叉樹的方式來遍歷會死循環 package class06;import java.util.HashSet; import java.util.LinkedList; import java.util.Queue;public class Code01_BFS {// 從Node出發,進行BFSpublic static void bfs(Node node) {if (node == null) {return;}Queue<Node> queue = new LinkedList<>();HashSet<Node> map = new HashSet<>();queue.add(node);map.add(node);while (!queue.isEmpty()) {Node cur = queue.poll();System.out.println(cur.value);// 可以根據具體更改操作for (Node next : cur.nexts) {if (!map.contains(next)) {map.add(next);queue.add(next); }}}} }圖的深度優先便利
【思路】 1、利用棧實現 2、從源節點開始把節點按照深度 3、每彈出一個點,把該節點下一個沒有進過棧的鄰接點放入棧 4、直到棧變空 package class06;import java.util.HashSet; import java.util.Stack;public class Code02_DFS {public static void dfs(Node node) {if (node == null) {return;}Stack<Node> stack = new Stack<>();HashSet<Node> set = new HashSet<>();stack.add(node);set.add(node);System.out.println(node.value); // 可以根據實際修改操作while (!stack.isEmpty()) {Node cur = stack.pop();for (Node next : cur.nexts) {if (!set.contains(next)) {stack.push(cur);stack.push(next);set.add(next);System.out.println(next.value);break;}}}} }拓撲排序
【適用范圍】要求為有向圖且有入度為0的節點,并且沒有環 package class06;import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Queue;public class Code03_TopologySort {// directed graph and no looppublic static List<Node> sortedTopology(Graph graph) {// key:某一個Node// Value:剩余的入度HashMap<Node, Integer> inMap = new HashMap<>();// 入度為0的點才能進入這個隊列Queue<Node> zeroInQueue = new LinkedList<>();for (Node node : graph.nodes.values()) {inMap.put(node, node.in);if (node.in == 0) {zeroInQueue.add(node);}}// 拓撲排序的結果,依次加入resultList<Node> result = new ArrayList<>();while (!zeroInQueue.isEmpty()) {Node cur = zeroInQueue.poll();result.add(cur);for (Node next : cur.nexts) {inMap.put(next, inMap.get(next) - 1);if (inMap.get(next) == 0) {zeroInQueue.add(next);}}}return result;} }最小生成樹算法
【定義】簡單的說就是尋找一個連接所有圖節點的無環子集且保證該子集所有邊的權重和最小克魯斯卡爾算法(學完并查集后再看)
【概述】 克魯斯卡爾(Kruskal)算法從另一途徑求圖的最小生成樹。其基本思想是: 假設連通圖 G=(V,E),令最小生成樹的初始狀態為只有n個頂點而無邊的非連通圖T=(V,{}) 該圖中每個頂點自成一個連通分量。 在E中選擇代價最小的邊,若該邊依附的頂點分別在T中不同的連通分量上,則將此邊加入到T中; 否則,舍去此邊而選擇下一條代價最小的邊。依此類推,直至T中所有頂點構成一個連通分量為止 package class06;import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.PriorityQueue; import java.util.Set;//undirected graph only public class Code04_Kruskal {// Union-Find Setpublic static class UnionFind {private HashMap<Node, Node> fatherMap;private HashMap<Node, Integer> rankMap;public UnionFind() {fatherMap = new HashMap<Node, Node>();rankMap = new HashMap<Node, Integer>();}private Node findFather(Node n) {Node father = fatherMap.get(n);if (father != n) {father = findFather(father);}fatherMap.put(n, father);return father;}public void makeSets(Collection<Node> nodes) {fatherMap.clear();rankMap.clear();for (Node node : nodes) {fatherMap.put(node, node);rankMap.put(node, 1);}}public boolean isSameSet(Node a, Node b) {return findFather(a) == findFather(b);}public void union(Node a, Node b) {if (a == null || b == null) {return;}Node aFather = findFather(a);Node bFather = findFather(b);if (aFather != bFather) {int aFrank = rankMap.get(aFather);int bFrank = rankMap.get(bFather);if (aFrank <= bFrank) {fatherMap.put(aFather, bFather);rankMap.put(bFather, aFrank + bFrank);} else {fatherMap.put(bFather, aFather);rankMap.put(aFather, aFrank + bFrank);}}}}public static class EdgeComparator implements Comparator<Edge> {@Overridepublic int compare(Edge o1, Edge o2) {return o1.weight - o2.weight;}}public static Set<Edge> kruskalMST(Graph graph) {UnionFind unionFind = new UnionFind();unionFind.makeSets(graph.nodes.values());PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());for (Edge edge : graph.edges) {priorityQueue.add(edge);}Set<Edge> result = new HashSet<>();while (!priorityQueue.isEmpty()) {Edge edge = priorityQueue.poll();if (!unionFind.isSameSet(edge.from, edge.to)) {result.add(edge);unionFind.union(edge.from, edge.to);}}return result;} }普里姆算法
1).輸入:一個加權連通圖,其中頂點集合為V,邊集合為E; 2).初始化:Vnew = {x},其中x為集合V中的任一節點(起始點),Enew = {},為空; 3).重復下列操作,直到Vnew = V:a.在集合E中選取權值最小的邊<u, v>,其中u為集合Vnew中的元素,而v不在Vnew集合當中,并且v∈V(如果存在有多條滿足前述條 件即具有相同權值的邊,則可任意選取其中之一);b.將v加入集合Vnew中,將<u, v>邊加入集合Enew中; 4).輸出:使用集合Vnew和Enew來描述所得到的最小生成樹。 package class06;import java.util.Comparator; import java.util.HashSet; import java.util.PriorityQueue; import java.util.Set;// undirected graph only public class Code05_Prim {public static class EdgeComparator implements Comparator<Edge> {@Overridepublic int compare(Edge o1, Edge o2) {return o1.weight - o2.weight;}}public static Set<Edge> primMST(Graph graph) {// 解鎖的邊進入小根堆PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());HashSet<Node> set = new HashSet<>();// 依次挑選的邊在result里Set<Edge> result = new HashSet<>();for (Node node : graph.nodes.values()) {// 隨機挑選一個點// Node是開始點if (!set.contains(node)) {set.add(node);for (Edge edge : node.edges) { // 由一個點,解鎖所有相連的邊priorityQueue.add(edge);}while (!priorityQueue.isEmpty()) {// 彈出解鎖的邊中,最小權值的邊Edge edge = priorityQueue.poll();// 可能的一個新節點Node toNode = edge.to;// 不含有的時候就是新節點if (!set.contains(toNode)) {set.add(toNode);result.add(edge);for (Edge nextEdge : toNode.edges) {priorityQueue.add(nextEdge);}}}}}return result;}// 請保證graph是連通圖// graph[i][j]表示點i到點j的距離,如果是系統最大值代表無路// 返回值是最小連通圖的路徑之和public static int prim(int[][] graph) {int size = graph.length;int[] distances = new int[size];boolean[] visit = new boolean[size];visit[0] = true;for (int i = 0; i < size; i++) {distances[i] = graph[0][i];}int sum = 0;for (int i = 1; i < size; i++) {int minPath = Integer.MAX_VALUE;int minIndex = -1;for (int j = 0; j < size; j++) {if (!visit[j] && distances[j] < minPath) {minPath = distances[j];minIndex = j;}}if (minIndex == -1) {return sum;}visit[minIndex] = true;sum += minPath;for (int j = 0; j < size; j++) {if (!visit[j] && distances[j] > graph[minIndex][j]) {distances[j] = graph[minIndex][j];}}}return sum;}public static void main(String[] args) {System.out.println("hello world!");} }迪杰斯特拉算法–求解最短路徑
package class06;import java.util.HashMap; import java.util.HashSet; import java.util.Map.Entry;// no negative weight public class Code06_Dijkstra {public static HashMap<Node, Integer> dijkstra1(Node head) {// 從Head出發到所有點的最小距離// key:從head出發到達key// value:從head出發到達key的最小距離// 如果在表中,沒有T的記錄,含義是從head出發到T這個點的距離為正無窮HashMap<Node, Integer> distanceMap = new HashMap<>();distanceMap.put(head, 0);// 已經求過距離的節點,存在selectedNodes中,之后不修改HashSet<Node> selectedNodes = new HashSet<>();Node minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);while (minNode != null) {int distance = distanceMap.get(minNode);for (Edge edge : minNode.edges) {Node toNode = edge.to;if (!distanceMap.containsKey(toNode)) {distanceMap.put(toNode, distance + edge.weight);}distanceMap.put(edge.to, Math.min(distanceMap.get(toNode), distance + edge.weight));}selectedNodes.add(minNode);minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);}return distanceMap;}public static Node getMinDistanceAndUnselectedNode(HashMap<Node, Integer> distanceMap, HashSet<Node> touchedNodes) {Node minNode = null;int minDistance = Integer.MAX_VALUE;for (Entry<Node, Integer> entry : distanceMap.entrySet()) {Node node = entry.getKey();int distance = entry.getValue();if (!touchedNodes.contains(node) && distance < minDistance) {minNode = node;minDistance = distance;}}return minNode;}public static class NodeRecord {public Node node;public int distance;public NodeRecord(Node node, int distance) {this.node = node;this.distance = distance;}}public static class NodeHeap {private Node[] nodes;private HashMap<Node, Integer> heapIndexMap;private HashMap<Node, Integer> distanceMap;private int size;public NodeHeap(int size) {nodes = new Node[size];heapIndexMap = new HashMap<>();distanceMap = new HashMap<>();this.size = 0;}public boolean isEmpty() {return size == 0;}public void addOrUpdateOrIgnore(Node node, int distance) {if (inHeap(node)) {distanceMap.put(node, Math.min(distanceMap.get(node), distance));insertHeapify(node, heapIndexMap.get(node));}if (!isEntered(node)) {nodes[size] = node;heapIndexMap.put(node, size);distanceMap.put(node, distance);insertHeapify(node, size++);}}public NodeRecord pop() {NodeRecord nodeRecord = new NodeRecord(nodes[0], distanceMap.get(nodes[0]));swap(0, size - 1);heapIndexMap.put(nodes[size - 1], -1);distanceMap.remove(nodes[size - 1]);nodes[size - 1] = null;heapify(0, --size);return nodeRecord;}private void insertHeapify(Node node, int index) {while (distanceMap.get(nodes[index]) < distanceMap.get(nodes[(index - 1) / 2])) {swap(index, (index - 1) / 2);index = (index - 1) / 2;}}private void heapify(int index, int size) {int left = index * 2 + 1;while (left < size) {int smallest = left + 1 < size && distanceMap.get(nodes[left + 1]) < distanceMap.get(nodes[left])? left + 1 : left;smallest = distanceMap.get(nodes[smallest]) < distanceMap.get(nodes[index]) ? smallest : index;if (smallest == index) {break;}swap(smallest, index);index = smallest;left = index * 2 + 1;}}private boolean isEntered(Node node) {return heapIndexMap.containsKey(node);}private boolean inHeap(Node node) {return isEntered(node) && heapIndexMap.get(node) != -1;}private void swap(int index1, int index2) {heapIndexMap.put(nodes[index1], index2);heapIndexMap.put(nodes[index2], index1);Node tmp = nodes[index1];nodes[index1] = nodes[index2];nodes[index2] = tmp;}}public static HashMap<Node, Integer> dijkstra2(Node head, int size) {NodeHeap nodeHeap = new NodeHeap(size);nodeHeap.addOrUpdateOrIgnore(head, 0);HashMap<Node, Integer> result = new HashMap<>();while (!nodeHeap.isEmpty()) {NodeRecord record = nodeHeap.pop();Node cur = record.node;int distance = record.distance;for (Edge edge : cur.edges) {nodeHeap.addOrUpdateOrIgnore(edge.to, edge.weight + distance);}result.put(cur, distance);}return result;} }前綴樹
【簡介】 前綴樹是N叉樹的一種特殊形式。通常來說,一個前綴樹是用來存儲字符串的。 前綴樹的每一個節點代表一個字符串(前綴)。每一個節點會有多個子節點,通往不同子節點的路徑上有著不同的字符。 子節點代表的字符串是由節點本身的原始字符串,以及通往該子節點路徑上所有的字符組成的。 【特征】 節點所有的后代都與該節點相關的字符串有著共同的前綴。這就是前綴樹名稱的由來。 package class07;public class Code01_TrieTree {public static class TrieNode {// 邊代表字符,節點僅僅相當于占位 public int path;public int end;public TrieNode[] nexts;// 當字符內容不僅僅是26字母的時候??梢杂肏ashMap<Char,Node> nextspublic TrieNode() {path = 0;end = 0;nexts = new TrieNode[26];}}public static class Trie {private TrieNode root;public Trie() {root = new TrieNode();}public void insert(String word) {if (word == null) {return;}char[] chs = word.toCharArray();TrieNode node = root;int index = 0;for (int i = 0; i < chs.length; i++) {index = chs[i] - 'a';if (node.nexts[index] == null) {node.nexts[index] = new TrieNode();}node = node.nexts[index];node.path++;}node.end++;}public void delete(String word) {if (search(word) != 0) {char[] chs = word.toCharArray();TrieNode node = root;int index = 0;for (int i = 0; i < chs.length; i++) {index = chs[i] - 'a';if (--node.nexts[index].path == 0) {node.nexts[index] = null;return;}node = node.nexts[index];}node.end--;}}public int search(String word) {if (word == null) {return 0;}char[] chs = word.toCharArray();TrieNode node = root;int index = 0;for (int i = 0; i < chs.length; i++) {index = chs[i] - 'a';if (node.nexts[index] == null) {return 0;}node = node.nexts[index];}return node.end;}public int prefixNumber(String pre) {if (pre == null) {return 0;}char[] chs = pre.toCharArray();TrieNode node = root;int index = 0;for (int i = 0; i < chs.length; i++) {index = chs[i] - 'a';if (node.nexts[index] == null) {return 0;}node = node.nexts[index];}return node.path;}}public static void main(String[] args) {Trie trie = new Trie();System.out.println(trie.search("zuo"));trie.insert("zuo");System.out.println(trie.search("zuo"));trie.delete("zuo");System.out.println(trie.search("zuo"));trie.insert("zuo");trie.insert("zuo");trie.delete("zuo");System.out.println(trie.search("zuo"));trie.delete("zuo");System.out.println(trie.search("zuo"));trie.insert("zuoa");trie.insert("zuoac");trie.insert("zuoab");trie.insert("zuoad");trie.delete("zuoa");System.out.println(trie.search("zuoa"));System.out.println(trie.prefixNumber("zuo"));} }貪心算法
【簡介】 在某一個標準下,優先考慮最滿足標準的樣本,最后考慮最不滿足標準的樣本 最終得到一個答案的算法就稱之為貪心算法 ------------------------------------------- 不從整體最優上加以考慮,所做出的是在某種意義上的局部最優解 ------------------------------------------- 【解題套路】 1,實現一個不依靠貪心策略的解法X,可以用最暴力的嘗試 2,腦補出貪心策略A、貪心策略B、貪心策略C... 3,用解法X和對數器,去驗證每一個貪心策略,用實驗的方式得知哪個貪心策略正確 4,不要去糾結貪心策略的證明 ------------------------------------------- 【技巧】 1,根據某標準建立一個比較器來排序 2,根據某標準建立一個比較器來組成堆字符拼接最小字典序
【題目】 給定一個字符串類型的數組strs,找到一種拼接方式。 使得把所有字符串拼起來之后形成的 字符串具有最小的字典序。 package class07;import java.util.Arrays; import java.util.Comparator;public class Code02_LowestLexicography {public static class MyComparator implements Comparator<String> {@Overridepublic int compare(String a, String b) {return (a + b).compareTo(b + a);}}// String.compareTo的源碼: // public int compareTo(String anotherString) {// int len1 = value.length;// int len2 = anotherString.value.length;// int lim = Math.min(len1, len2);// char v1[] = value;// char v2[] = anotherString.value;// int k = 0;// while (k < lim) {// char c1 = v1[k];// char c2 = v2[k];// if (c1 != c2) {// return c1 - c2;// }// k++;// }// return len1 - len2;// }public static String lowestString(String[] strs) {if (strs == null || strs.length == 0) {return "";}Arrays.sort(strs, new MyComparator());String res = "";for (int i = 0; i < strs.length; i++) {res += strs[i];}return res;}public static void main(String[] args) {String[] strs1 = { "jibw", "ji", "jp", "bw", "jibw" };System.out.println(lowestString(strs1));String[] strs2 = { "ba", "b" };System.out.println(lowestString(strs2));} }切金條–哈夫曼樹
【題目】 一塊金條切成兩半,是需要花費和長度數值一樣的銅板的。 比如長度為20的金條,不管切成長度多大的兩半,都要花費20個銅板。 ----------------------------------------------- 一群人想整分整塊金條,怎么分最省銅板? 例如,給定數組{10,20,30},代表一共三個人,整塊金條長度為10+20+30=60。 金條要分成10,20,30三個部分。 如果先把長度60的金條分成10和50,花費60; 再把長度50的金條分成20和30,花費50;一共花費110銅板。 但是如果先把長度60的金條分成30和30,花費60; 再把長度30金條分成10和20, 花費30;一共花費90銅板。 輸入一個數組,返回分割的最小代價。 package class07;import java.util.Comparator; import java.util.PriorityQueue;public class Code03_LessMoneySplitGold {public static int lessMoney(int[] arr) {PriorityQueue<Integer> pQ = new PriorityQueue<>();for (int i = 0; i < arr.length; i++) {pQ.add(arr[i]);}int sum = 0;int cur = 0;while (pQ.size() > 1) {cur = pQ.poll() + pQ.poll();sum += cur;pQ.add(cur);}return sum;}public static class MinheapComparator implements Comparator<Integer> {@Overridepublic int compare(Integer o1, Integer o2) {return o1 - o2; // < 0 o1 < o2 負數}}public static class MaxheapComparator implements Comparator<Integer> {@Overridepublic int compare(Integer o1, Integer o2) {return o2 - o1; // < o2 < o1}}public static void main(String[] args) {// solutionint[] arr = { 6, 7, 8, 9 };System.out.println(lessMoney(arr));int[] arrForHeap = { 3, 5, 2, 7, 0, 1, 6, 4 };// min heapPriorityQueue<Integer> minQ1 = new PriorityQueue<>();for (int i = 0; i < arrForHeap.length; i++) {minQ1.add(arrForHeap[i]);}while (!minQ1.isEmpty()) {System.out.print(minQ1.poll() + " ");}System.out.println();// min heap use ComparatorPriorityQueue<Integer> minQ2 = new PriorityQueue<>(new MinheapComparator());for (int i = 0; i < arrForHeap.length; i++) {minQ2.add(arrForHeap[i]);}while (!minQ2.isEmpty()) {System.out.print(minQ2.poll() + " ");}System.out.println();// max heap use ComparatorPriorityQueue<Integer> maxQ = new PriorityQueue<>(new MaxheapComparator());for (int i = 0; i < arrForHeap.length; i++) {maxQ.add(arrForHeap[i]);}while (!maxQ.isEmpty()) {System.out.print(maxQ.poll() + " ");}} }會議室問題
【題目】 一些項目要占用一個會議室宣講,會議室不能同時容納兩個項目的宣講。 給你每一個項目開始的時間和結束的時間(給你一個數 組,里面是一個個具體 的項目), 你來安排宣講的日程,要求會議室進行的宣講的場次最多。 返回這個最多的宣講場次。 package class07;import java.util.Arrays; import java.util.Comparator;public class Code04_BestArrange {public static class Program {public int start;public int end;public Program(int start, int end) {this.start = start;this.end = end;}}public static class ProgramComparator implements Comparator<Program> {@Overridepublic int compare(Program o1, Program o2) {return o1.end - o2.end;}}public static int bestArrange(Program[] programs, int start) {Arrays.sort(programs, new ProgramComparator());int result = 0;for (int i = 0; i < programs.length; i++) {if (start <= programs[i].start) {result++;start = programs[i].end;}}return result;}public static void main(String[] args) {} }利益最大化(賺錢問題/IPO)
【題目】 輸入: 正數數組costs 正數數組profits 正數k 正數m 含義: costs[i]表示i號項目的花費profits[i]表示i號項目在扣除花費之后還能掙到的錢(利潤) k表示你只能串行的最多做k個項目 m表示你初始的資金 說明: 你每做完一個項目,馬上獲得的收益,可以支持你去做下一個項目。 輸出: 你最后獲得的最大錢數。 package class07;import java.util.Comparator; import java.util.PriorityQueue;public class Code05_IPO {public static class Node {public int p;public int c;public Node(int p, int c) {this.p = p;this.c = c;}}public static class MinCostComparator implements Comparator<Node> {@Overridepublic int compare(Node o1, Node o2) {return o1.c - o2.c;}}public static class MaxProfitComparator implements Comparator<Node> {@Overridepublic int compare(Node o1, Node o2) {return o2.p - o1.p;}}public static int findMaximizedCapital(int k, int W, int[] Profits, int[] Capital) {// w ==> 初始資金 // k ==> 最多買幾件商品Node[] nodes = new Node[Profits.length];for (int i = 0; i < Profits.length; i++) {nodes[i] = new Node(Profits[i], Capital[i]);}PriorityQueue<Node> minCostQ = new PriorityQueue<>(new MinCostComparator());PriorityQueue<Node> maxProfitQ = new PriorityQueue<>(new MaxProfitComparator());for (int i = 0; i < nodes.length; i++) {minCostQ.add(nodes[i]);}for (int i = 0; i < k; i++) {while (!minCostQ.isEmpty() && minCostQ.peek().c <= W) {maxProfitQ.add(minCostQ.poll());}if (maxProfitQ.isEmpty()) {return W;}W += maxProfitQ.poll().p;}return W;} }總結
以上是生活随笔為你收集整理的2022年1月4日|5日|6日|7日|的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: base64转音频blob
- 下一篇: 使用 Stylish 插件替换浏览器的默