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-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[] 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(-1, null);
ListNode temp = head;
//第一個while,只要l1和l2都還有元素時,依據大小進行合併
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 / 2 - 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 = 2 * i + 1; k < length; k = 2 * k + 1) {
//如果該節點有右節點的話,並且右節點最大時,將k指向右節點
if (k + 1 < 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 = {-7, 11, 44, 7, -89, 34, 12, 67, 90, 34, 67, -89, 3, 1, 9, 0, 4, 2, -7, 95, 700, 21, 65, -900};
heapSort(a);
for (int temp : a) {
System.out.print(temp + " ");
}
}
}
15、最長迴文子串
要求:
給定一個字符串 s,找到 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 <= 2 || 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);
}