1. 程式人生 > >【JAVA面試】JAVA常考點之資料結構與演算法(1)

【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]) {

        return -1;    }     while (low <= high) {         int middle = (low + high) / 2;         if (key == arr[middle]) {             return middle;        } else if (key < arr[middle]) {            high = middle - 1;        } else {            low = middle + 1;        }    }     return -1;}

(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-1for (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 >= && 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 + : 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 == || n == 1) {         return n;    }     if (n == 2) {         return 2;    }     return climbStairs(n - 1) + climbStairs(n - 2);}

(2)動態規劃

public int climbStairs2(int n) {     if (n == || n == 1) {         return 1;    }     int