【JAVA面試】JAVA常考點之資料結構與演算法(1)
JAVA常考點之資料結構與演算法(1)
JAVA常考點之資料結構與演算法
目錄
1、二分查詢
優點是比較次數少,查詢速度快,平均效能好;
其缺點是要求待查表為有序表,且插入刪除困難。
因此,折半查詢方法適用於不經常變動而查詢頻繁的有序列表。
使用條件:查詢序列是順序結構,有序。
時間複雜度
採用的是分治策略
最壞的情況下兩種方式時間複雜度一樣:O(log2 N)
最好情況下為O(1)
空間複雜度
演算法的空間複雜度並不是計算實際佔用的空間,而是計算整個演算法的輔助空間單元的個數
非遞迴方式:
由於輔助空間是常數級別的所以:
空間複雜度是O(1);
遞迴方式:
遞迴的次數和深度都是log2 N,每次所需要的輔助空間都是常數級別的:
空間複雜度:O(log2N )
(1)非遞迴實現
public static int binarySearch(int[] arr, int key) {
int low = 0;
int high = arr.length - 1;
//當要查詢的數小於最小值或大於最大值時,直接返回,不需要進入while迴圈內部if (key < arr[low] || key > arr[high]) {
(2)遞迴實現
public static int recursionBinarySearch(int[] arr, int key, int low, int high) { int middle = (low + high) / 2; if (key < arr[low] || key > arr[high]) { return -1; } if (key == arr[middle]) { return middle; } else if (key < arr[middle]) { return recursionBinarySearch(arr, key, low, middle - 1); } else { return recursionBinarySearch(arr, key, middle + 1, high); }}
2、氣泡排序
比較兩個相鄰的元素,若第二個元素比第一個元素小,則交換兩者的位置,第一輪比較完成後,最大的數會浮到最頂端。排除此最大數,繼續下一輪的比較。
時間複雜度:O(N^2)
空間複雜度:O(1)
為穩定排序
可以為氣泡排序進行優化,當某一趟未發生交換時,則說明陣列已經有序了,無需再進行排序。
public static void bubbleSort(int[] arr) { //一共需要進行n-1趟迴圈for (int i = 0; i < arr.length - 1; i++) { //假設本次迴圈中,沒有發生交換boolean flag = false; //本次迴圈一共需要比較n-i-1次for (int j = 0; j < arr.length - i - 1; j++) { if (arr[j + 1] < arr[j]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; //本次迴圈發生交換了flag = true; } } //如果本次迴圈後,未發生交換,則表明陣列有序,退出排序if (!flag) { break; } }}
3、層序遍歷二叉樹
結點類
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(int val, TreeNode left, TreeNode right) { this.val = val; this.left = left; this.right = right; }}
層序遍歷(非遞迴)
public void layerOrder(TreeNode root) { LinkedList<TreeNode> list = new LinkedList<>(); TreeNode t; if (root != null) { list.push(root); } while (!list.isEmpty()) { t = list.removeFirst(); System.out.print(t.getValue()); if (t.getLeft() != null) { list.addLast(t.getLeft()); } if (t.getRight() != null) { list.addLast(t.getRight()); } }}
4、選擇排序
第一輪從整個陣列中選擇最小的數,與第一個數交換。
第二輪排除第一個數,從剩下來的數中選擇最小的數,與第二個數交換。
以此類推。
時間複雜度:O(N^2)
空間複雜度:O(1)
為穩定排序
public static void selectSort(int[] a) { for (int i = 0; i < a.length - 1; i++) { //假設當前下標代表最小的數int min = i; for (int j = i + 1; j < a.length; j++) { if (a[j] < a[min]) { min = j; } } if (min != i) { int temp = a[i]; a[i] = a[min]; a[min] = temp; } }}
5、反轉單鏈表
結點類
public static class ListNode { int val; ListNode next; ListNode(int val) { this.val = val; }}
這裡我們採用兩種方法,分別是迭代與遞迴。
(1)迭代
從連結串列頭部開始處理,我們需要定義三個連續的結點pPre,當前需要反轉的結點pCur,下一個需要反轉的結點pFuture和一個永遠指向頭結點的pFirst。每次我們只需要將pPre指向pFuture,pCur指向pFirst,調整頭結點pFirst,調整當前需要反轉的結點為下一個結點即pFuture,最後調整pFuture為pCur的next結點。
/** * 迭代方式* * @param head 翻轉前連結串列的頭結點* @return 翻轉後連結串列的頭結點*/public ListNode reverseList(ListNode head) { if (head == null) { return null; } //始終指向連結串列的頭結點ListNode pFirst = head; //三個結點中的第一個結點ListNode pPre = pFirst; //當前需要反轉的結點ListNode pCur = head.next; //下一次即將需要反轉的結點ListNode pFuture = null; while (pCur != null) { pFuture = pCur.next; pPre.next = pFuture; pCur.next = pFirst; pFirst = pCur; pCur = pFuture; } return pFirst;}
(2)遞迴
遞迴與迭代不同,遞迴是從連結串列的尾結點開始,反轉尾結點與前一個結點的指向。
/** * 遞迴方式* * @param head 翻轉前連結串列的頭結點* @return 翻轉後連結串列的頭結點*/public ListNode reverseList2(ListNode head) { if (head == null || head.next == null) { return head; } ListNode pNext = head.next; head.next = null; ListNode reverseListNode = reverseList2(pNext); pNext.next = head; return reverseListNode;}
6、插入排序
(1)兩層for迴圈
public static void insertSort(int[] a) { for (int i = 1; i < a.length; i++) { int cur = i; int t = a[i]; for (int j = i - 1; j >= 0; j--) { if (t < a[j]) { a[j + 1] = a[j]; cur = j; } } //cur位置是最後空出來的,將本次待插入的數t放進去即可a[cur] = t; }}
(2)內層使用while迴圈(不太好理解)
public static void insertSort2(int[] a) { for (int i = 1; i < a.length; i++) { int temp = a[i]; int j = i - 1; while (j >= 0 && temp < a[j]) { a[j + 1] = a[j]; j--; } a[j + 1] = temp; }}
7、判斷連結串列是否有環
節點類:
static class ListNode { public int val; public ListNode next; ListNode(int val) { this.val = val; this.next = null; }}
(1)使用額外空間來判斷連結串列中是否有環
思路:遍歷整個連結串列,將每一次遍歷的節點存入Set中,利用Set存入相同元素返回false的特性,判斷連結串列中是否有環。
public boolean hasCycle(ListNode head) { Set<ListNode> set = new HashSet<>(); while (head != null) { boolean result = set.add(head); if (!result) { return true; } head = head.next; } return false;}
由於遍歷,導致時間複雜度為O(n),由於使用了Set集合,空間複雜度為O(n)。
(2)使用快慢指標。
思路:快慢指標都從頭節點開始,快指標一次走兩步,慢指標一次,如果慢指標能夠追趕上快指標,則證明連結串列中有環。
public boolean hasCylce2(ListNode head) { ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { fast = fast.next.next; slow = slow.next; //如果慢指標追趕上快指標的話,則說明有環if (fast == slow) { return true; } } return false;}
拓展問題1:
如果連結串列有環,找出環的入口節點。
思路:快慢指標的相遇點到環入口的距離等於頭節點到環入口的距離,那麼在頭節點和相遇點各設一個相同步伐的指標,他們相遇的那個節點就是環入口。
public ListNode getEntrance(ListNode head) { ListNode slow = head; ListNode fast = head; boolean isCycle = false; while (fast != null && fast.next != null) { fast = fast.next.next; slow = slow.next; //如果慢指標追趕上快指標的話,則說明有環if (fast == slow) { isCycle = true; break; } } if (isCycle) { slow = head; while (slow != fast) { slow = slow.next; fast = fast.next; } return slow; } return null;}
拓展問題2:
若連結串列有環,求出環的長度。
思路:若連結串列有環,得到環入口,然後讓指標指向環入口,指標遍歷完重新回到環入口的路程即環的長度。
public int getCylceLength(ListNode head) { int length = 0; ListNode cycleNode = getEntrance(head); if (cycleNode != null) { ListNode temp = cycleNode; while (true) { temp = temp.next; length++; if (temp == cycleNode) { break; } } } return length;}
8、快速排序
在陣列中選定一個基準數,通過一次排序後,基準數左邊的元素都比基準數小,基準數右邊的元素都比基準數大。
最差時間複雜度O(N^2)
平均時間複雜度O(N*log2N)
為不穩定排序
public static void quickSort(int[] a, int left, int right) { if (left > right) { return; } //以陣列第一個元素為基準點int key = a[left]; int i = left; int j = right; while (i < j) { //j位於最右邊,向左邊進行遍歷,直到找到一個小於基準數的元素,取其下標while (i < j && a[j] >= key) { j--; } //i位於最左邊,向右邊進行遍歷,直到找到一個大於基準數的元素,取其下標while (i < j && a[i] <= key) { i++; } //若找到以上兩個數,則交換他們if (i < j) { int temp = a[i]; a[i] = a[j]; a[j] = temp; } } //此時離開while迴圈,說明i==j,將a[i]與基準數進行交換a[left] = a[i]; a[i] = key; //對左邊進行遞迴排序quickSort(a, left, i - 1); //對右邊進行遞迴排序quickSort(a, i + 1, right);}
9、求二叉樹最大的深度(寬度)
(1)遞迴實現
/** * 求二叉樹的深度(使用遞迴)* * @param root* @return*/public int getHeight(TreeNode root) { if (root == null) { return 0; } int leftHeight = getHeight(root.getLeft()); int rightHeight = getHeight(root.getRight()); return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;}
(2)非遞迴實現
按照層序遍歷的思想,記錄某一層的元素總數與本層中已經遍歷過的元素個數,當兩者相等時,深度自增。
也可用於求二叉樹的最大寬度,遍歷的同時取每層元素總數的最大值即可。
/** * 求二叉樹的深度(不使用遞迴)* * @param root* @return*/public int getHeight2(TreeNode root) { if (root == null) { return 0; } LinkedList<TreeNode> list = new LinkedList<>(); list.offer(root); //最大寬度留備用int max=0; //二叉樹的深度int level = 0; //本層中元素總數int size = 0; //本層已經訪問過的元素個數int cur = 0; while (!list.isEmpty()) { size = list.size(); max=Math.max(max,size); cur = 0; while (cur < size) { TreeNode node = list.poll(); cur++; if (node.getLeft() != null) { list.offer(node.getLeft()); } if (node.getRight() != null) { list.offer(node.getRight()); } } level++; } System.out.println("二叉樹最大寬度為:"+max); return level;}
10、爬樓梯
題目描述:
假設你正在爬樓梯。需要 n 階你才能到達樓頂。
每次你可以爬 1 或 2 個臺階。你有多少種不同的方法可以爬到樓頂呢?
注意:給定 n 是一個正整數。
示例:輸入3,輸出3
走法:
(1)1,1,1
(2)1,2
(3)2,1
思考:
第n階樓梯的爬法 =(第n-1階樓梯的爬法+第n-2階樓梯的爬法)
(1)遞迴(可能會出現超時情況)
public int climbStairs(int n) { if (n == 0 || n == 1) { return n; } if (n == 2) { return 2; } return climbStairs(n - 1) + climbStairs(n - 2);}
(2)動態規劃
public int climbStairs2(int n) { if (n == 0 || n == 1) { return 1; } int