【JAVA面試】JAVA常考點之數據結構與算法(1)

                            JAVA常考點之數據結構與算法(1)

JAVA常考點之數據結構與算法

目錄

1、二分查找 1

2、冒泡排序 3

3、層序遍歷二叉樹 4

4、選擇排序 5

5、反轉單鏈表 6

6、插入排序 7

7、判斷鏈表是否有環 8

8、快速排序 11

9、求二叉樹最大的深度(寬度) 12

10、爬樓梯 13

11、合併兩個有序鏈表 14

12、數組的最大子序和 16

13、求兩個數的最大公約數與最小公倍數 17

14、堆排序 18

15、最長迴文子串 20

 

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-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 >= && 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[] a = new int[n + 1];
    a[0] = 1;
    a[1] = 1;
    for (int i = 2; i <= n; i++) {
        a[i] = a[i - 1] + a[i - 2];
    }
    return a[n];
}

 

 

 

11、合併兩個有序鏈表

結點類

static class ListNode {
    int val;
    ListNode next;

    ListNode(int val, ListNode next) {
        this.val = val;
        this.next = next;
    }

 

(1)非遞歸解法

public static ListNode mergeList2(ListNode l1, ListNode l2) {

    ListNode head = new ListNode(-1null);
    ListNode temp = head;

    //第一個while,只要l1l2都還有元素時,依據大小進行合併
    while (l1 != null && l2 != null) {
        if (l1.val < l2.val) {
            head.next = l1;
            l1 = l1.next;
        } else {
            head.next = l2;
            l2 = l2.next;
        }
        head = head.next;
    }
    //第二個while,此時l1已經爲空,則將l2剩餘的結點合併到新鏈表中
    while (l2 != null) {
        head.next = l2;
        head = head.next;
        l2 = l2.next;
    }
    //第三個while,此時l2已經爲空,則將l1剩餘的結點合併到新鏈表中
    while (l1 != null) {
        head.next = l1;
        head = head.next;
        l1 = l1.next;
    }
    return temp.next;
}

 

 

(2)遞歸解法

public static ListNode mergeList(ListNode l1, ListNode l2) {
    //遞歸的最小規模解
    if (l1 == null) {
        return l2;
    }
    if (l2 == null) {
        return l1;
    }
    //遞歸的規模分解
    if (l1.val <= l2.val) {
        l1.next mergeList(l1.next, l2);
        return l1;
    } else {
        l2.next mergeList(l1, l2.next);
        return l2;
    }
}

 

 

 

12、數組的最大子序和

題目描述:給定一個整數數組 nums ,找到一個具有最大和的連續子數組(子數組最少包含一個元素),返回其最大和。

 

使用動態規劃法:

 

假設sum(i,j)表示nums數組中從i到j的元素之和,那麼我們必須遵循的條件是sum(i,j-1)>0,我們可以認爲sum(i,j)有可能是最大子序和,需要和其滿足這個條件的情況進行比較。如果sum(i,j-1)<0,我們就拋棄nums[i]到nums[j-1]之間的元素(兩端也拋棄,拋棄之前,先與最大值進行比較,防止出現數組全負數的情況)。再從nums[j]開始,即求sum(j,k)的最大值,如果sum(i,k-1)<0,我們就再從nums[k]開始,以此類推。

 

 

public static int getMaxSum(int[] a) {
    int max = a[0];
    int cur = a[0];
    for (int i = 1; i < a.length; i++) {
        if (cur > 0) {
            cur += a[i];
        } else {
            cur = a[i];
        }
        if (cur > max) {
            max = cur;
        }
    }
    return max;
}

 

 

 

 

 

 

 

13、求兩個數的最大公約數與最小公倍數

(1)最大公約數

使用相減法

/**
 * 相減法
 */
public static int getMaxCommonDivisor(int x, int y) {
    while (x != y) {
        if (x > y) {
            x = x - y;
        } else {
            y = y - x;
        }
    }
    return x;
}

 

(2)最小公倍數

基於定理 兩數乘積=最大公約數*最小公倍數

public static int getMinCommonMultiple(int x, int y) {
    return x * y / getMaxCommonDivisor(x, y);
}

 

 

 

 

 

 

 

 

 

 

 

 

 

14、堆排序

時間複雜度:O( N*logN )

空間複雜度:O(1)

爲不穩定排序

 

 

注意點:

(1)初始化大頂堆時 是從最後一個有子節點開始往上調整最大堆。

(2)而堆頂元素(最大數)與堆最後一個數交換後,需再次調整成大頂堆,此時是從上往下調整的。

(3)不管是初始大頂堆的從下往上調整,還是堆頂堆尾元素交換,每次調整都是從父節點、左孩子節點、右孩子節點三者中選擇最大者跟父節點進行交換,交換之後都可能造成被交換的孩子節點不滿足堆的性質,因此每次交換之後要重新對被交換的孩子節點進行調整。

 

package day1024;

/**
 * 堆排序
 */
public class Solution {
    public static void heapSort(int[] a) {
        //從第一個非葉子節點開始,從下往上,從右向左調整堆
        for (int i = a.length 1; i >= 0; i--) {
            adjust(a, i, a.length);
        }

        //構建完堆後,需要首先交換堆頂元素與尾元素,然後除堆尾元素,再次進行堆的調整
        for (int j = a.length 1; j > 0; j--) {
            //交換堆頂元素與尾元素
            swap(a, 0, j);
            //再次對堆進行排序,每次都需要排除有序的元素
            adjust(a, 0, j);

        }
    }


    /**
     * 調整節點a[i],其左節點a[2i+1],右節點a[2i+2],且左右子節點均在length範圍內
     */
    public static void adjust(int[] a, int i, int length) {
        //先是該節點的左節點b,然後是b的左節點,
        for (int k = * i + 1; k < length; k = * k + 1) {
            //如果該節點有右節點的話,並且右節點最大時,將k指向右節點
            if (k + < length && a[k] < a[k + 1]) {
                k++;
            }
            //此時a[k]已經是左右兩節點最大的節點
            if (a[k] > a[i]) {
                //交換兩元素
                swap(a, i, k);
                //由於交換節點與子節點,導致子節點構成的堆亂序,因此將元素下標改爲其子節點下標
                i = k;
            } else {
                break;
            }
        }

    }

    /**
     * 交換數組中的兩個元素
     */
    public static void swap(int[] a, int m, int n) {
        int temp = a[m];
        a[m] = a[n];
        a[n] = temp;
    }

    public static void main(String[] args) {
        int[] a = {-711447, -89341267903467, -89319042, -7957002165, -900};
        heapSort(a);
        for (int temp : a) {
            System.out.print(temp + " ");
        }
    }

}

15、最長迴文子串

要求:

給定一個字符串 s,找到 s 中最長的迴文子串。你可以假設 的最大長度爲1000。

示例 1:

輸入: "babad"

輸出: "bab"

 

 

這裏我們採用動態規劃。

 

主要思路:

 

(1)聲明一個布爾類型的二維數組dp,boolean[][] dp = new boolean[len][len],len爲字符串s的長度,那麼如果dp[i][j]爲true時,則表示字符串s從第i個位置開始到第j個位置結束之間的子字符串爲迴文子串。

 

如:s="abac",那麼此時dp聲明爲dp[4][4],那麼dp[0][0],dp[1][1],dp[2][2],dp[3][3],dp[0][2]都爲true,表示對應位置的字母都是迴文子串,那麼這裏最大長度的迴文子串爲aba。

 

(2)如果dp[i][j]爲true,那麼dp[i+1][j-1]也必定爲true,即“abba”爲迴文子串,那麼“bb”也爲迴文子串。因此我們的狀態轉移方程爲dp[i+1][j-1]=true&&s.charAt(i)==s.charAt(j)  =>  dp[i][j]爲true。

 

(3)如果j-i<=2時,即可能爲迴文的字符串最大長度爲3時,如果有s.charAt(i)==s.charAt(j),就直接斷定dp[i][j]=true,並不需要dp[i+1][j-1]=true。即當s="abad",i=0;j=2,s.charAt(0)==s.charAt(2),則直接判定aba爲迴文字符串,即dp[0][2]=true。

 

(4)使用兩層for循環,外層循環指示器i控制所求字符串的開始位置,內層循環指示器j控制所求字符串的結束位置。但不是簡單的使得i從0開始,i++,j=i,j++,而是需要i從最大值開始,即字符串的長度-1開始,i--,j=i,j++,先求出小範圍內下標大的dp情況,再求出大範圍內下標小的情況,因爲後者依賴前者。例如dp[0][3]依賴dp[1][2]的值。

 

 

public static String longestPalindrome(String str) {
    int len = str.length();
    boolean[][] dp = new boolean[len][len];
    //迴文串最大長度,開始下標,結束下標
    int max = 0;
    int start = 0;
    int stop = 0;

    //先從下標大的元素開始,因爲dp[0][8]依賴dp[2][6]
    for (int i = len - 1; i >= 0; i--) {
        for (int j = i; j < len; j++) {
            if (str.charAt(i) == str.charAt(j) && (j - i <= || dp[i + 1][j - 1])) {
                dp[i][j] = true;
                if (max < j - i + 1) {
                    max = j - i + 1;
                    start = i;
                    stop = j;
                }
            }
        }
    }
    return str.substring(start, stop + 1);
}

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章