1. 程式人生 > 實用技巧 >常用演算法例項2

常用演算法例項2

目錄

字串

檢測大寫字母

給定一個單詞,你需要判斷單詞的大寫使用是否正確。

我們定義,在以下情況時,單詞的大寫用法是正確的:

全部字母都是大寫,比如"USA"。
單詞中所有字母都不是大寫,比如"leetcode"。
如果單詞不只含有一個字母,只有首字母大寫, 比如 "Google"。
class Solution:
    def detectCapitalUse(self, word: str) -> bool:
        if word[0].islower():
            if word.islower():
                return True
        else:
            if word.isupper():
                return True
            elif word[1:].islower():
                return True
        return False

考慮三種情況,如果用java就比較囉嗦。靈活使用python內建的語法。

連結串列

連結串列題目的考慮重點,是否可以用遞迴。連結串列與遞迴之間是有很大關係的,如:

刪除排序連結串列中的重複元素

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        //空列表或者只有一個節點的列表
        if(head == null || head.next == null) return head;
        if(head.val == head.next.val){
            //頭結點需要刪除,以第一個不重複的節點為頭節點
            int tmp = head.val;
            while(head!=null && head.val==tmp)
            {
                head = head.next;
            }
            return deleteDuplicates(head);
        }else{
            //如果頭結點不需要刪除
            head.next = deleteDuplicates(head.next);
            return head;
        }
    }
}
  1. 找終止條件:當head指向連結串列只剩一個元素的時候,自然是不可能重複的,因此return

  2. 想想應該返回什麼值:應該返回的自然是已經去重的連結串列的頭節點

  3. 每一步要做什麼:巨集觀上考慮,此時head.next已經指向一個去重的連結串列了,而根據第二步,我應該返回一個去重的連結串列的頭節點。因此這一步應該做的是判斷當前的head和head.next是否相等,如果相等則說明重了,返回head.next,否則返回head

class Solution {
public ListNode deleteDuplicates(ListNode head) {
	if(head == null || head.next == null){
		return head;
	}
	head.next = deleteDuplicates(head.next);
	if(head.val == head.next.val) 
        head = head.next;
	return head;
}
}
// 要深刻理解遞迴!

判斷連結串列中是否存在環

思路

我們可以通過檢查一個結點此前是否被訪問過來判斷連結串列是否為環形連結串列。常用的方法是使用雜湊表。

演算法

我們遍歷所有結點並在雜湊表中儲存每個結點的引用(或記憶體地址)。如果當前結點為空結點 null(即已檢測到連結串列尾部的下一個結點),那麼我們已經遍歷完整個連結串列,並且該連結串列不是環形連結串列。如果當前結點的引用已經存在於雜湊表中,那麼返回 true(即該連結串列為環形連結串列)。

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public boolean hasCycle(ListNode head) {
        // 雜湊集合Set
        Set<ListNode> nodesSeen = new HashSet<>();
        while(head != null){
            if (nodesSeen.contains(head))
                return true;
            else{
                nodesSeen.add(head);
            }
            head = head.next;
        }
        return false;
    }
}

這種解法中注意雜湊表在Java中的使用:Set<ListNode> nodesSeen = new HashSet<>();

這種解法的時間複雜度是O(n),空間複雜度也是O(n)。每個節點最多訪問一次,雜湊表中最多新增n個節點元素。

另一種解法是可以用快慢指標:

public class Solution {
    public boolean hasCycle(ListNode head) {
        if (head == null || head.next == null)
            return false;
        ListNode slow = head;
        ListNode fast = head.next;
        while(fast != slow){
            //這個地方注意判斷fast.next的值,因為下面程式碼中有fast.next.next的使用
            if (fast == null || fast.next == null){
                return false;
            }
            slow = slow.next;
            fast = fast.next.next;
        }
        return true;
    }
}

為什麼快慢指標是正確的?怎麼證明?

因為只要有環,那麼快指標是一定能夠趕得上慢指標的。有環會進入一個迴圈中,連結串列的訪問不會結束。

反轉連結串列

如何原地反轉連結串列。反轉一個單鏈表。

示例:

輸入: 1->2->3->4->5->NULL
輸出: 5->4->3->2->1->NULL
進階:
你可以迭代或遞迴地反轉連結串列。你能否用兩種方法解決這道題?

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
//迭代的方法
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode prev = null; //當前的前指標
        ListNode curr = head; //當前節點
        //每次迴圈,都將當前節點指向它前面的節點,然後當前節點和籤節點後移
        while(curr!=null)
        {
            ListNode nextTemp = curr.next;
            curr.next = prev; //curr指向prev節點
            prev = curr;
            curr = nextTemp;
        }
        return prev;
    }
}

//遞迴的方法
class Solution {
    //將連結串列分為head 和 new_head兩部分。假設new_head是已經反轉好的連結串列,只是考慮head和new_head兩個節點。
    // 1. head.next要設定為null
    // 2. head.next指向head本身
    public ListNode reverseList(ListNode head) {
        if(head==null || head.next==null) return head;
        ListNode new_head = reverseList(head.next);
        head.next.next = head; //反轉
        head.next = null;
        return new_head;
    }
}

刪除連結串列中指定val的所有節點

示例:

輸入:1->2->6->3->4->5->6, val=6

輸出:1->2->3->4->5

考慮兩種思路:普通思路和遞迴思路。普通解法要設定一下新的頭節點,保持演算法一致性避免特殊處理。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
//遞迴解法,很是精妙!
class Solution {
    public ListNode removeElements(ListNode head, int val) {
        if(head == null) return head;
        head.next = removeElements(head.next, val);
        return head.val == val ? head.next: head;

    }
}

上述解法真的很巧妙。如果head為null,則直接返回head;其他呼叫遞迴。返回時,如果head.val為目標值,則返回head.next。

// 普通解法
public ListNode removeElements(ListNode head, int val) {
    // 先建立一個Node節點,避免影響首節點
    ListNode header = new ListNode(-1);
    header.next = head;
    ListNode cur = header;
    while(cur.next != null) {
        if (cur.next.val == val) {
        	// 直接跳過目標節點
            cur.next = cur.next.next;
        }
        else{
            cur = cur.next;
        }
    }
    return header.next;
}


數字

判斷是否是醜數

class Solution {
    public boolean isUgly(int num) {
        if(num == 0){
            return false;
        }
        while (num != 1){
            if(num % 2 == 0){
                num /= 2;
                //continue;
            }
            else if(num % 3 == 0){
                num /= 3;
                // continue;
            }
            else if(num % 5 == 0){
                num /= 5;
                //continue;
            }
            else
                return false;
        }
        return true;
    }
}

簡單寫法:

public boolean isUgly(int num){
    while(num%2 == 0) num = num/2;
    while(num%3 == 0) num = num/3;
    while(num%5 == 0) num = num/5;
    if (num == 1) return true;
    return false;
}

第n個醜數

【筆記】使用三指標法。一部分是醜數陣列,另一部分是權重2,3,5。下一個醜數,定義為醜數陣列中的數乘以權重,所得的最小值。

那麼,2該乘以誰?3該乘以誰?5該乘以誰?

class Solution:
    def nthUglyNumber(self, n: int) -> int:
        res = [1]
        idx2 = 0
        idx3 = 0
        idx5 = 0
        for i in range(n-1):
            res.append(min(res[idx2]*2, res[idx3]*3, res[idx5]*5))
            # 如果最後一個值來自某一個
            if res[-1] == res[idx2]*2:
                idx2 += 1
            if res[-1] == res[idx3]*3:
                idx3 += 1
            if res[-1] == res[idx5]*5:
                idx5 += 1
        return res[-1]
        

充分利用了2、3、5三個數值,因為醜數就是由這三部分組成的。注意:range(n-1),為0 到 n-2,加上原來已經有的1,一共有n-1個值。

棧的應用在於它是一個先進後出的資料結構,可以利用這個特性做一些事情。

有效的括號

給定一個只包括 '(',')','{','}','[',']' 的字串,判斷字串是否有效。

有效字串需滿足:

左括號必須用相同型別的右括號閉合。
左括號必須以正確的順序閉合。
注意空字串可被認為是有效字串。

class Solution {
    public boolean isValid(String s) {
        Stack<Character> stack = new Stack<>();
        for (char c : s.toCharArray()) {
            if (c == '(') {
                stack.push(')');
                continue;
            }
            if (c == '[') {
                stack.push(']');
                continue;
            }
            if (c == '{') {
                stack.push('}');
                continue;
            }

            if (stack.empty() || stack.pop() != c) {
                return false;
            }
        }

        return stack.empty();
    }
}

這道題目中考慮到java中棧這個資料結構的使用。Stack stack = new Stack<>(); 這種用法是java中非常常見的。

二叉樹

判斷二叉樹是否相同

二叉樹相同的條件:

  1. 根節點相同
  2. 左子樹相同
  3. 右子樹相同

第一時間的想法肯定是用遞迴思想。如果root相同,則比較左子樹和右子樹,全部相同則是相同的二叉樹。一次編譯通過。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public boolean isSameTree(TreeNode p, TreeNode q) {
        if (p == null || q == null) {
            if  (p == null && q == null) 
                return true;
            else{
                return false;
            }
        }

        if (p.val != q. val){
            return false;
        }
        else{
            return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
        }
    }
}

判斷是否是對稱二叉樹

給定一個二叉樹,檢查它是否是映象對稱的。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public boolean compareTree(TreeNode l, TreeNode r){
        if(l==null && r==null) return true;
        if(l==null || r==null) return false;
        if(l.val != r.val) return false;

        return compareTree(l.left, r.right) && compareTree(l.right, r.left);
    }
    public boolean isSymmetric(TreeNode root) {
        if(root == null) return true;
        return compareTree(root.left, root.right);
    }
}

定義了內部一個函式compareTree,傳入引數是兩個樹。

計算二叉樹的最大深度

使用遞迴的演算法,其實很簡單。遞迴可以解決很多看上去很複雜的問題。

class Solution {
    public int maxDepth(TreeNode root) {
        if(root == null) return 0;
        return Math.max(maxDepth(root.left)+1, maxDepth(root.right)+1);
    }
}

難以置信的簡單。

計算二叉樹的最小深度

給定一個二叉樹,找出其最小深度。

最小深度是從根節點到最近葉子節點的最短路徑上的節點數量。

說明: 葉子節點是指沒有子節點的節點。

示例:

給定二叉樹 [3,9,20,null,null,15,7],
返回它的最小深度 2.

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public int minDepth(TreeNode root) {
        // 特殊場景
        if (root == null) return 0;
        if(root.left == null && root.right==null) return 1;
        int min_depth = Integer.MAX_VALUE;
        if (root.left != null) {
            min_depth = Math.min(minDepth(root.left), min_depth);
        }
        if (root.right != null) {
            min_depth = Math.min(minDepth(root.right), min_depth);
        }
        return min_depth+1;
    }
}

翻轉二叉樹

如何翻轉二叉樹。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    //先序遍歷 --- 從頂向下交換
    public TreeNode invertTree(TreeNode root) {
        if (root==null)  return root;
        // 儲存右子樹
        TreeNode rightTree = root.right;
        // 交換左右子樹的位置
        root.right = invertTree(root.left);
        root.left = invertTree(rightTree);
        return root;
    }
}

計算二叉樹的高度

計算二叉樹的高度,考慮清楚遞迴邊界條件:

  1. Node本身為空。

  2. Node的左右子節點均為空。

public int dfs(TreeNode x){
    if(x == null) return 0;
    if(x.left == null && x.right == null) return 1;
    int leftH = dfs(x.left)+1;
    int rightH = dfs(x.right)+1;
    return Math.max(leftH, rightH);
}

先明確特殊情況:空樹,單節點樹。

然後遞迴呼叫函式本身,返回左右子樹的最大高度。

判斷二叉樹是否是平衡二叉樹

遞迴求解每個節點左右子樹的高度。

public boolean isBalanced(TreeNode root){
    if (root == null) return true;
    // 判斷左右子樹是否是平衡二叉樹
    if(!isBalanced(root.left)) return false;
    if(!isBalanced(root.right)) return false;
    //分別計算左右子樹的高度
    int leftH = dfs(root.left) + 1;
    int rightH = dfs(root.right) + 1;
    if(Math.abs(leftH - rightH) > 1) {
        return false;
    }
    else{
        return true;
    }
}

中序遍歷二叉樹

中序是指root根節點來說的,root節點是中間被加入的。

一般使用遞迴方法比較常見。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    List<Integer> list = new ArrayList<>();
    public List<Integer> inorderTraversal(TreeNode root) {
        if(root == null) return list;
        inorderTraversal(root.left);
        list.add(root.val);
        inorderTraversal(root.right);
        return list;
    }
}

python的實現:

以下兩種寫法都是正確的。

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
    def inorderTraversal(self, root: TreeNode) -> List[int]:
        res_list = []
        if root:
            res_list += self.inorderTraversal(root.left)
            res_list.append(root.val)
            res_list += self.inorderTraversal(root.right)
        return res_list
    
    def inorder_traversal(p):
        if not p:
            return []
        res = []
        res.extend(inorder_traversal(p.left))
        res.append(p.val)
        res.extend(inorder_traveral(p.right))
        return res
    
    

非遞迴的實現:

def inorder_traversal(root):
    stack = []
    res = []
    if not root:
        return []
    while root or stack:
        # 先把左子樹加入stack
        while root:
            stack.append(root)
            root = root.left
        if stack:
        	a = stack.pop()
            res.append(a.val)
            root = a.right
	return res

前序遍歷二叉樹

先訪問root根節點,再訪問左子樹和右子樹。

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
    
    def preorderTraversal(self, root: TreeNode) -> List[int]:
        res_list = []
        if not root:
            return res_list
        res_list.append(root.val)
        res_list += self.preorderTraversal(root.left)
        res_list += self.preorderTraversal(root.right)
        return res_list

           

java的實現:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    List<Integer> list = new ArrayList<>();
    public List<Integer> preorderTraversal(TreeNode root) {
        if(root == null) return list;
        list.add(root.val);
        preorderTraversal(root.left);
        preorderTraversal(root.right);
        return list;
    }
}

非遞迴的實現:

非遞迴的時候stack必須先加入右子樹節點再加入左子樹節點,因為stack的機制是先入後出。

def preOrderTravese(root):
    stack = [root]
    res = []
    while stack:
        res.append(root.val)
        if root.right:
            stack.append(root.right)
        if root.left:
            stack.append(root.left)
        root = stack.pop()
        

層次遍歷二叉樹

按層遍歷:從上到下、從左到右按層遍歷

利用佇列這個資料結構。

# 先進先出選用佇列結構
import queue
def layerTraverse(head):
    if not head:
        return None
    que = queue.Queue()     # 建立先進先出佇列
    que.put(head)
    while not que.empty():
        head = que.get()    # 彈出第一個元素並列印
        print(head.val)
        if head.left:       # 若該節點存在左子節點,則加入佇列(先push左節點)
            que.put(head.left)
        if head.right:      # 若該節點存在右子節點,則加入佇列(再push右節點)
            que.put(head.right)
            

二叉樹節點個數

# 求二叉樹節點個數
def tree_node_nums(node):
    if node is None:
        return 0
    nums = tree_node_nums(node.left)
    nums += tree_node_nums(node.right)
    return nums + 1

最後的加1是因為算上了節點本身。

雜湊表

判斷兩個字串是否互為變形詞

變形詞,出現的字元種類一樣,並且每種字元出現的次數也一樣。

// 時間複雜度為O(N), 空間複雜度為O(M)
public static boolean isDeformation(String str1, String str2) {
    if(str1 == null || str2 == null || str1.length() != str2.length()) {
        return false;
    }
    char[] chas1 = str1.toCharArray();
    char[] chas2 = str2.toCharArray();
    int[] map = new int[256];
    for(int i=0; i<chas1.length; i++) {
        map[chas1[i]] ++;
    }
    for(int j=0; j<chas2.length;j++) {
        //如果減少之前的值小於0,則說直接返回false
        //如果遍歷完str2,map中的值也沒有出現負值,則返回true
        if(map[chas2[j]] == 0) {
            return false;
        }
        map[chas2[j]]--;
    }
    return true;
}

巧妙地使用了陣列,因為字元的數字在256之內。

陣列

合併排序的陣列

給定兩個排序後的陣列 A 和 B,其中 A 的末端有足夠的緩衝空間容納 B。 編寫一個方法,將 B 合併入 A 並排序。初始化 A 和 B 的元素數量分別為 m 和 n。

class Solution {
    // 使用尾插法
    public void merge(int[] A, int m, int[] B, int n) {
        int i =m-1, j=n-1, idx = m+n-1;
        while(j>=0){
            if(i<0 || B[j]>=A[i]){
                A[idx--] = B[j--];
            }
            else{
                A[idx--] = A[i--];
            }
        }
    }
}


//另一個解法,也是尾插法
class Solution {
	public void merge(int[] A, int m, int[] B, int n) {
		int i = m-1, j = n-1, idx = m+n-1;
		while(i>=0 && j>=0){
			if (A[i]<B[j]){
				A[idx--] = B[j--];
			}
			else{
				A[idx--] = A[i--];
			}
		}
        //處理最後B剩下的,A不需要處理
        while(j>=0){
            A[idx--] = B[j--];
        }
	}
}

一個偷巧的方法,效率還很快。

class Solution:
    def merge(self, A: List[int], m: int, B: List[int], n: int) -> None:
        """
        Do not return anything, modify A in-place instead.
        """
        A[m:m+n+1] = B
        A.sort()

求眾數

給定一個大小為n的陣列,找到其中的多數元素。多數元素是指在陣列中出現次數大於n/2的元素。可以假設陣列是非空的,並且給定的陣列總是存在多數元素。

暴力法,時間為O(n^2), 空間複雜度O(1):

class Solution{
    public int majorityElement(int[] nums){
        int majorityCount = nums.length/2;
        for(int num : nums){
            int count = 0;
            for (int elem : nums){
                if (elem == num){
                    count += 1;
                }
            }
            if (count>majorityCount){
                return num;
            }
        }
        return -1;
    }
}

使用雜湊表,時間複雜度為O(n),空間複雜度為O(n),相當於以空間換時間:

class Solution:
    def majorityElement(self, nums: List[int]) -> int:
        # 使用雜湊表
        nums_dict = {}
        for i in nums:
            if i in nums_dict:
                nums_dict[i] += 1
            else:
                nums_dict[i] = 1
        for k, v in nums_dict.items():
            if v > len(nums)//2:
                return k
        return -1

將每個元素替換為右側最大元素

給你一個數組 arr ,請你將每個元素用它右邊最大的元素替換,如果是最後一個元素,用 -1 替換。

完成所有替換操作後,請你返回這個陣列。

// 1.從後往前排
// 2.定義一個變數跟蹤最大值
class Solution {
    public int[] replaceElements(int[] arr) {
        int max = -1;
        for (int i = arr.length - 1; i >= 0; i--) {
            int temp = arr[i];
            arr[i] = max;
            if (temp > max) {
                max = temp;
            }
        }
        return arr;
    }
}

python版本,注意倒序遍歷的寫法:

class Solution:
    def replaceElements(self, arr: List[int]) -> List[int]:
        max_arr = -1
        for i in range(len(arr)-1, -1, -1):
            tmp = arr[i]
            arr[i] = max_arr
            max_arr = max(max_arr, tmp)
        return arr

二分查詢

尋找比目標字母大的最小字母

使用二分查詢,python3中mid=(low+high)//2

class Solution:
    def nextGreatestLetter(self, letters: List[str], target: str) -> str:
        res = letters[0]
        low = 0
        high = len(letters)-1
        while low <= high:
            mid = (low+high)//2
            if letters[mid] > target:
                res = letters[mid]
                high = mid - 1
            else:
                low = mid + 1
        return res