最近開始刷LeetCode,回顧了一下被我遺忘在角落裏的數據結構和算法,包括java的基礎語法。爲了避免刷完了題又忘,所以在這裏總結一下做過的題目,文中出現的所有代碼均是用java編寫,有不對的地方歡迎指正。
“業精於勤,荒於嬉;行成於思,毀於隨。” 每天保持思考、學無止境、持續更新……
(以下內容均爲本人總結內容,僅供學習參考)
LeetCode經典算法題目二(樹、排序、查找、動態規劃、回溯、貪心)
LeetCode之字符串、數組、鏈表、棧、隊列、哈希
一、整數和字符串
1. 迴文數
迴文數是指:正序和倒序都是一樣的數。
算法: 初始化y(倒序)爲0,循環計算: y=y*10+rem(原數除以10的餘數)
2. 整數反轉輸出
與迴文數算法思想相同,但需要判斷32位整數是否溢出,因爲int類型佔4個字節,取值範圍爲:-2147483648~2147483647,所以有可能出現原數爲1999999999,如果反轉就溢出了。
解決辦法: 對結果變量定義爲long類型,int 32位 long 64位,判斷if((int)y == y)
3. 羅馬數字轉整數
用switch case語句將羅馬數字與整數一一對應即可,需要考慮一下兩個羅馬數字組合的特殊情形,細節不再描述。
記一些有關字符串、數組和列表的常用方法:
1.int length(); //注意:字符串中是length()方法,數組中是length屬性
2.char charAt(int index); //取字符串中索引爲index的字符元素
3.String indexOf(String str2); //該方法在str1中檢索str2,返回其在str1中第一次出現的位置索引,若找不到則返回-1。
4.String substring(int beginIndex, int endIndex); //截取索引爲 [beginIndex,endIndex) 的字符串
5.char[] toCharArray(); //把字符串轉化爲字符數組
6.System.arraycopy(原數組,原數組的開始位置,目標數組,目標數組的開始位置,拷貝的元素個數); //(二/6)屬於java.lang.System包中。
7.Arrays.sort(nums); //(二/6)關於Arrays類中封裝的排序方法(採用歸併排序)。
//我在java api中整理了一些常用的,這裏出現的數組類型都以int作爲示例,當然其他的基本數組類型都是通用的。(見下圖)
8.String replace(char oldChar, char newChar); //(一/5)替換字符串中字符或者字符串,char也可以是String
4. 最長公共前綴
題目描述:
編寫一個函數來查找字符串數組中的最長公共前綴。
如果不存在公共前綴,返回空字符串 “”。
算法一: 橫向+縱向掃描,for循環嵌套。第一個for循環縱向掃描首個字符串的每一位字符,第二個for循環橫向掃描數組中剩餘字符串的對應位,若掃描到任意字符串與第一個字符串對應位的字符不同,則跳出兩層for循環,用標記符號:break outter;
實現。由於兩個for循環,時間複雜度較高,性能較差。
將相同的字符添加到字符串str結尾。
StringBuilder sb = new StringBuilder(str); //創建一個StringBuilder對象
sb.append( c); //再添加字符到尾部
str = sb.toString(); //轉化成字符串
考慮數組越界: 因爲輸入的字符串數組的元素可能爲0,即啥也不輸入。所以要有判斷if(strs.length==0)
的語句(strs是字符串數組,String[] strs)
算法二: 橫向掃描。鎖定第一個字符串str,把它作爲比較對象與剩餘的字符串進行對比,每一次對比的是整個str ,若任意字符串不能檢索到它,則讓str長度減一,減去尾部字符,如此從後往前逐次去掉尾部不相同的字符,最終找到一個與剩餘字符串相同的最長前綴。
String str = strs[0];
for(i=1;i<strs.length;i++){
while(strs[i].indexOf(str) != 0){ //由於要找的是公共前綴,因此這裏需要判斷第一次出現的索引是否爲0
str = str.substring(0,str.length()-1); // 截取減掉尾字符後剩餘的字符串
}
}
5. x的平方根
如果使用for循環,在小於等於x的範圍內逐個找值的話,雖然是一種解決辦法,但是運行會超出時間限制。
計算平方根最好的辦法是牛頓迭代法
class Solution {
public int mySqrt(int x) {
double x0,x1;
x0=x;
x1=(x0+x/x0)/2;
while(Math.abs(x0-x1)>=1){
x0=x1;
x1=(x0+x/x0)/2;
}
return (int)x1;
}
}
6. 實現strStr();(★)
我沒自己寫算法實現,直接用的java封裝好的方法str1.indexOf(str2)在str1中檢索str2,個人覺得沒有太大必要費工夫來寫這個,因爲indexOf()太好用且常用了。
7. 最後一個單詞的長度
算法: 將原字符串倒序循環依次取每個字符,遇到不爲空格的字符則長度計數m加1;遇到空格字符但是m等於0(m初始化爲0),則繼續循環;遇到空格字符且m大於0,則退出while循環,返回m的值。
8. 反轉字符串(★★)
算法: 雙指針。將left和right指向的元素互換,不斷往中間走。
(我用的也是首尾元素互換的思想,但是運行要比雙指針慢一點。可能因爲循環條件裏面我寫的i<len/2
每次循環都要計算這個所以慢一點吧。但原理是一樣的)
class Solution {
public void reverseString(char[] s) {
int len=s.length;
int left=0,right=len-1;
char tem;
while(left<right){ //當左右指針走到中間就結束(奇偶都成立)
tem = s[left];
s[left] = s[right];
s[right] = tem;
left++;
right--;
}
}
}
9. 替換空格(★)
題目描述:請實現一個函數,把字符串 s 中的每個空格替換成"%20"。
方法一: 使用String類的方法:replace(String old,String new)
;
class Solution {
public String replaceSpace(String s) {
return s.replace(" ","%20");
}
}
方法二: 使用StringBuilder
在尾部逐個添加字符。
class Solution {
public String replaceSpace(String s) {
StringBuilder sb = new StringBuilder();
for(int i=0;i<s.length();i++){
if(s.charAt(i) == ' ')
sb.append("%20");
else
sb.append(s.charAt(i));
}
return sb.toString();
}
}
10. 字符串轉換整數 (atoi)(★)
題目描述:請你來實現一個 atoi 函數,使其能將字符串轉換成整數。
首先,該函數會根據需要丟棄無用的開頭空格字符,直到尋找到第一個非空格的字符爲止。接下來的轉化規則如下:
1、如果第一個非空字符爲正或者負號時,則將該符號與之後面儘可能多的連續數字字符組合起來,形成一個有符號整數。
2、假如第一個非空字符是數字,則直接將其與之後連續的數字字符組合起來,形成一個整數。
3、該字符串在有效的整數部分之後也可能會存在多餘的字符,那麼這些字符可以被忽略,它們對函數不應該造成影響。
*注意:假如該字符串中的第一個非空格字符不是一個有效整數字符、字符串爲空或字符串僅包含空白字符時,則你的函數不需要進行轉換,即無法進行有效轉換。在任何情況下,若函數不能進行有效的轉換時,請返回 0 。
*提示:
本題中的空白字符只包括空格字符 ’ ’ 。
假設我們的環境只能存儲 32 位大小的有符號整數,那麼其數值範圍爲 [−231, 231 − 1]。如果數值超過這個範圍,請返回 INT_MAX (231 − 1) 或 INT_MIN (−231) 。
調用Character
類的靜態方法:static boolean isDigit(char c)
可以判斷當前字符是否爲數字,如果爲數字,再使用:int digit = c - '0';
來實現字符轉換爲數字,從而可以使用:num = num * 10 + digit;
來實現將字符串轉換爲整數。
int的最大值:Integer.MAX_VALUE
int的最小值:Integer.MIN_VALUE
class Solution {
public int myAtoi(String str) {
int len = str.length();
int index=0;
while(index<len){ //先處理前面的空格字符
if(str.charAt(index)!=' ')
break;
index++;
}
boolean negative = false; //負號標識符
if(index<len && str.charAt(index)=='-'){ //負數
negative = true;
index++;
}
else if(index<len && str.charAt(index)=='+'){ //正數
index++;
}
int num=0;
while(index<len && Character.isDigit(str.charAt(index))){ //若當前字符爲數字
int digit = str.charAt(index) - '0'; //字符轉數字
if(num > (Integer.MAX_VALUE-digit)/10){ //若加上這個數字後超出int的範圍
return negative?Integer.MIN_VALUE:Integer.MAX_VALUE; //返回最大或最小
}
num = num * 10 + digit;//字符串轉整數
index++;
}
return negative?-num:num;
}
}
11. 用 Rand7() 實現 Rand10()(★)
題目描述:已有方法 rand7 可生成 1 到 7 範圍內的均勻隨機整數,試寫一個方法 rand10 生成 1 到 10 範圍內的均勻隨機整數。
不要使用系統的 Math.random() 方法。
/**
* The rand7() API is already defined in the parent class SolBase.
* public int rand7();
* @return a random integer in the range 1 to 7
*/
class Solution extends SolBase {
public int rand10() {
int num = (rand7()-1) * 7 + rand7();
while(num > 40){
num = (rand7()-1) * 7 + rand7();
}
return num%10 + 1;
}
}
二、數組
1. 刪除排序數組中的重複項(★★)
題目描述:給定一個排序數組,你需要在原地刪除重複出現的元素,使得每個元素只出現一次,返回移除後數組的新長度。不要使用額外的數組空間,你必須在原地修改輸入數組並在使用 O(1) 額外空間的條件下完成。
算法: 利用雙指針:慢指針i和快指針j。i指向的元素纔是最終的新數組的元素。
時間複雜度:O(n). 假設數組的長度是 n,那麼 i 和 j 分別最多遍歷 n 步。
空間複雜度:O(1)
class Solution {
public int removeDuplicates(int[] nums) {
if(nums.length==0)
return 0;
int i=0;
for(int j=1;j<nums.length;j++){
if(nums[i]!=nums[j]) //若num[i]!=nums[j],則把j指向的值賦給i的下一個元素
nums[++i]=nums[j];
}
return i+1;
}
}
注意:for(j;j<nums.length;j++) 寫法錯誤!java語法在for循環中必須先對循環變量賦值,對j初始化:int j=1或者外部有定義,直接j=1也可以,不能只出現單獨的 j,會報語法錯誤。
2. 移除元素
跟第7題是同樣的算法。利用雙指針,不創建新數組,指針i指向的元素是最終新數組的結果,返回 i+1即新數組的長度。
3. 找出數組中重複的數字(★)
題目描述:在一個長度爲 n 的數組 nums 裏的所有數字都在 0~n-1 的範圍內。數組中某些數字是重複的,但不知道有幾個數字重複了,也不知道每個數字重複了幾次。請找出數組中任意一個重複的數字。
算法思想: 使用哈希集合HashSet去重,利用add()
方法來判斷要添加的元素是否重複,若返回值爲true則說明沒有重複,可以添加;若返回值爲false,則說明已存在,無法繼續添加,那麼就是我們找的重複數字。
時間複雜度:O(n). 遍歷數組一遍。使用哈希集合添加元素的時間複雜度爲 O(1),故總的時間複雜度是 O(n)。
空間複雜度:O(n)。不重複的每個元素都可能存入集合,因此佔用O(n)額外空間。
class Solution {
public int findRepeatNumber(int[] nums) {
HashSet<Integer> hs = new HashSet<>();
int repeat=nums[0];
for(int i=0;i<nums.length;i++){
if(!hs.add(nums[i])){ //若無法往HashSet中添加,說明該數字重複,返回它即可。
repeat = nums[i];
break;
}
}
return repeat;
}
}
4. 搜索插入位置(★)
題目描述:在一個升序排序的數組中找到給的目標值,並返回其索引,沒有找到則返回它應該插入的位置。
算法: 從頭掃描數組,如果target > nums[i]
則不用管,繼續往後掃描,如果target <= nums[i]
則返回 i,若循環結束則返回數組長度(即目標值應該添加在末尾)。
class Solution {
public int searchInsert(int[] nums, int target) {
int i;
for(i=0;i<nums.length;i++){
if(target <= nums[i])
return i;
}
return i;
}
}
5. 加一
題目描述:給定一個整型數組,代表一個非負的整數。結果返回這個整數值加一的數(由數組表示)。
算法: 用一個while循環解決,從後往前 掃描原數組:(3種情況)
1.如果當前位的數字小於9,則只對當前位的值加一,返回數組。
2.否則讓該位數字爲0,繼續往前取下一位數字,小於9則……否則……,直到遇到情況1執行return語句。
3.如果while循環結束都還沒執行return,則說明原數組每一位都爲9,那麼每一位數值都加一後,數組的長度也應該加一,並且數組首元素應該爲"0",即變爲:10000….
class Solution {
public int[] plusOne(int[] digits) {
int n = digits.length;
int x;
while(n>0){
x = digits[n-1];
if(x < 9){
digits[n-1] = x +1;
return digits;
}
digits[n-1] = 0;
n--;
}
int[] nums = new int[digits.length + 1];
nums[0] = 1;
return nums;
}
}
6. 合併兩個有序數組(★★★★)
題目描述:給定兩個有序整數數組 nums1 和 nums2,將 nums2 合併到 nums1 中,使得 num1 成爲一個有序數組。
說明:
1.初始化 nums1 和 nums2 的元素數量分別爲 m 和 n。
2.你可以假設 nums1 有足夠的空間(空間大小大於或等於 m + n)來保存 nums2 中的元素。
算法一: 雙指針。指針 i 依次掃描數組nums1,指針 j 掃描數組nums2,在while循環中每次都把nums[i++]與nums[j++]的值進行比較,較小的那個給nums[k++] (新建的數組,用來存儲合併後的結果),當 i 掃描完或者 j 掃描完後,退出循環,讓沒掃描完的那個數組剩餘的元素直接循環添加到nums[k]的後面,得到最終合併後的有序數組,再把nums拷貝給nums1即可。
注意: java中拷貝數組不能直接用"=",直觀的方法是可以使用for循環將原數組的每個元素給到目標數組。但是比較便捷的是使用本地方法:
System.arraycopy(原數組,原數組的開始位置,目標數組,目標數組的開始位置,拷貝的元素個數);
該方法可以將原數組的某些元素直接拷貝到目標數組的某個位置,使用該方法的前提必須是目標數組是一個已經分配內存單元
的數組。
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int i=0,j=0,k=0;
int[] nums=new int[m+n];
while(i<m && j<n){ //任意一個數組掃描完後都結束循環
if(nums1[i]<nums2[j]){
nums[k]=nums1[i]; //把較小的元素給nums[k]
i++;}
else{
nums[k]=nums2[j]; //把較小的元素給nums[k]
j++;}
k++;
}
while(j<n) //讓沒掃描完的數組nums2剩餘的元素直接循環添加到nums[k]的後面
nums[k++]=nums2[j++];
while(i<m) //讓沒掃描完的數組nums1剩餘的元素直接循環添加到nums[k]的後面
nums[k++]=nums1[i++];
System.arraycopy(nums,0,nums1,0,m+n); //將排序後的元素拷貝到nums1目標數組中
}
}
算法二:
- 先使用數組拷貝方法,將num2合併到num1數組的尾部
- 再使用java的排序方法:
Arrays.sort(nums1);
直接對num1數組進行升序排序。
算法二代碼只有兩行,非常簡潔,但實際上這個方法並沒有有效地利用到兩個原數組均有序這一特點,所以時間複雜度較差,爲O((m+n)log(m+n))
7. 撲克牌中的順子(★★★)
題目描述:從撲克牌中隨機抽5張牌,判斷是不是一個順子,即這5張牌是不是連續的。2~10爲數字本身,A爲1,J爲11,Q爲12,K爲13,而大、小王爲 0 ,可以看成任意數字。A 不能視爲 14。
算法一: 排序
class Solution {
public boolean isStraight(int[] nums) {
Arrays.sort(nums); //java的排序方法
int i=0,j=0;
while(nums[i]==0){ //計算數組中‘0’的個數
j++;
i++;
}
i++;
while(i<nums.length){
if(nums[i]==nums[i-1]) //有重複的直接返回false
return false;
if(nums[i]>(nums[i-1]+1)) //計算不連續兩數的差值,看‘0’夠不夠填充
j = j - nums[i] + nums[i-1] +1;
i++;
}
if(j>=0) //如果‘0’的剩餘個數大於等於0,說明夠填充了,返回真值
return true;
else
return false;
}
}
算法二: 不排序
class Solution {
public boolean isStraight(int[] nums) {
int max,min,i=0;
max=nums[i];
while(nums[i]==0){ //找到第一個不爲0的元素,將它賦給min
i++;
}
min=nums[i];
for(i=0;i<nums.length;i++){
if(nums[i]!=0){ //在不爲0的前提下進行查找最大值和最小值
max = Math.max(max,nums[i]);
min = Math.min(min,nums[i]);
for(int j=i+1;j<nums.length;j++){ //for循環嵌套,檢查是否有重複元素
if(nums[i]==nums[j])
return false;
}
}
}
if(max - min <= 4) //在無重複元素的情況下,最大值和最小值差值若等於4,說明剛好是順子;若小於4,說明0的個數肯定足夠填充
return true;
else
return false;
}
}
8. 買賣股票的最佳時機
題目描述:給定一個數組,它的第 i 個元素是一支給定股票第 i 天的價格。
如果你最多隻允許完成一筆交易(即買入和賣出一支股票),設計一個算法來計算你所能獲取的最大利潤。
注意:你不能在買入股票前賣出股票。
算法一: 暴力法。兩個for循環遍歷,依次對數組中的每一個元素都去求它後面的每個元素與它的差值,總是保留差值最大的那個。很耗時,時間複雜度爲O(n*n)
class Solution {
public int maxProfit(int[] prices) {
int m=0;
for(int i=0;i<prices.length;i++){
for(int j=i+1;j<prices.length;j++){
m = Math.max(m,prices[j]-prices[i]); //保留最大差值
}
}
return m;
}
}
算法二: 峯谷法。僅一次遍歷,時間複雜度O(n)
兩個變量,一個存波谷值,一個存最大利潤。遍歷過程中如果數組元素值比現存波谷值更小,就更新波谷值,並且每次都要更新最大利潤值。
class Solution {
public int maxProfit(int[] prices) {
if(prices.length==0)
return 0;
int low=prices[0], profit=0; //初始化波谷爲數組第一個元素,最大利潤爲0
for(int i=1;i<prices.length;i++){
if(prices[i]<low)
low = prices[i];
profit = Math.max(profit,prices[i]-low); //更新最大利潤變量的值
}
return profit;
}
}
三、鏈表(遞歸)
1. 合併兩個有序鏈表(★)
鏈表節點有兩部分信息:
第一部分是節點保存的值,第二部分是指向的下一個節點的地址。
public class ListNode{
Object data; //每個節點的數據
ListNode next; //每個節點指向下一個節點的連接
ListNode(Object data){
this.data = data;
}
}
算法: 遞歸。
每次遞歸方法都找出值最小的結點。
如果鏈表1或者鏈表2爲空,則直接返回非空鏈表(已經是有序的)。若都有值的話就判斷誰的頭結點更小,讓更小的頭結點的next等於下一次遞歸返回的結點。(因爲下一次遞歸返回的一定又是最小結點),如此每一次遞歸都找出當下最小的結點,依次排序鏈接。
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) { //每次使用該方法,都能返回當前最小結點
if(l1 == null) //l1爲空則返回l2結點
return l2;
else if(l2 == null) //l2爲空則返回l1結點
return l1;
else if(l1.val < l2.val){ //若l1當前結點更小,則把l1提出來,再使它的下一個結點指向遞歸該方法返回的最小結點。
l1.next = mergeTwoLists(l1.next,l2);
return l1;
}
else{
l2.next = mergeTwoLists(l2.next,l1);
return l2;
}
}
}
2. 刪除排序鏈表中的重複元素
算法: 比較當前結點值與下一個結點值是否相同,如果相同則將下一個結點的next賦給當前結點的next,否則繼續比較下一個結點值與下下個結點值……直到head.next==null
結束。返回head結點(由於循環中head一直在變化,所以可以提前先備份一個headnew)
注意: 鏈表作爲參數輸入時一定要判空,否則會出現空指針報錯。
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if(head == null)
return null;
if(head.next == null)
return head;
ListNode headnew = head;
ListNode son = headnew.next;
while(son!=null){
if(headnew.val == son.val){
headnew.next = son.next;
son = headnew.next;
}
else{
headnew=headnew.next;
son = headnew.next;
}
}
return head;
}
}
3. 環形鏈表(★★★★★)
題目描述:給定一個鏈表,判斷鏈表中是否有環。
算法: 雙指針。快指針每次走兩個結點,即:fast = fast.next.next;
,慢指針每次走一個結點,即:slow = slow.next;
,可分爲:
- 鏈表無環。那麼fast一定比slow先走到結尾;
- 鏈表有環。fast一定可以在某一時刻等於slow。
但要注意: 避免指針報錯。結點null.next是無意義的指針,運行時會報錯,所以每次使用fast.next.next
時要先判斷if(fast == null || fast.next == null )
public class Solution {
public boolean hasCycle(ListNode head) {
if(head == null || head.next==null) //頭結點爲空或者指向爲空都說明無環
return false;
ListNode fast = head.next.next;
ListNode slow = head;
while(fast != slow){
if(fast == null || fast.next == null ) //快指針爲空或者它的下一個爲空說明無環
return false;
fast = fast.next.next; //快指針一次走兩個
slow = slow.next; //慢指針一次走一個
}
return true;
}
}
4. 鏈表中倒數第k個節點(★)
題目描述:輸入一個鏈表,輸出該鏈表中倒數第k個節點。爲了符合大多數人的習慣,本題從1開始計數,即鏈表的尾節點是倒數第1個節點。
例如,一個鏈表有6個節點,從頭節點開始,它們的值依次是1、2、3、4、5、6。這個鏈表的倒數第3個節點是值爲4的節點。
算法一: 嵌套循環。外循環
是對鏈表中的結點依次遍歷;內循環
是讓當前結點往前走 k 個單位,看是否走到空(即是否走到底),若是則該結點爲倒數第k個結點,若不是則繼執行外循環。
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode head2;
int k2;
while(head!=null){ //外循環,依次對鏈表中的結點遍歷
head2 = head; //每次外循環都需要對當前結點進行備份(因爲內循環要改變它的值)
k2 = k; //每次外循環都需要用最初的k值
while(k2>0){ //內循環,該結點往前走k個單位
head2=head2.next;
k2--;
}
if(head2 == null) //若走到空,則退出循環
break;
head = head.next; //否則繼續遍歷下一個結點
}
return head;
}
}
算法二: 雙指針(時間複雜度更小)。設置兩個指針:before
(前指針)和 after
(後指針),剛開始使這兩個指針之間相差 k 個元素,然後兩個指針同時往前移動,當前指針before
移動到爲null
時,此時的after
一定指向倒數第 k 個元素 。
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode before=head, after=head;
while(k>0){ //循環 k 次,使前指針before與後指針after相差 k 個元素
before = before.next;
k--;
}
while(before!=null){ //兩個指針同時往前移動,直到前指針before爲null結束
before = before.next;
after = after.next;
}
return after; //返回後指針after
}
}
5. 鏈表相交(★)
題目描述:給定兩個(單向)鏈表,判定它們是否相交併返回交點。請注意相交的定義基於節點的引用,而不是基於節點的值。換句話說,如果一個鏈表的第k個節點與另一個鏈表的第j個節點是同一節點(引用完全相同),則這兩個鏈表相交。
算法: 雙指針。讓nodeA和nodeB分別從它們的頭結點出發,一直沿着鏈表往下走,直到走到null
時,讓nodeA從B的頭結點
開始繼續走,而nodeB從A的頭結點
開始往下走。這樣一來,nodeA指針和nodeB指針都會把鏈表A和鏈表B全部走完,而走完的結束位置就在鏈表的相交位置處。可以看圖片說明:
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode nodeA=headA;
ListNode nodeB=headB;
while(nodeA != nodeB){ //循環直到兩結點相同爲止
nodeA = nodeA==null ? headB : nodeA.next; //若nodeA爲null則把B的頭指針給它,否則把它指向的下一個指針給它
nodeB = nodeB==null ? headA : nodeB.next; ////若nodeB爲null則把A的頭指針給它,否則把它指向的下一個指針給它
}
return nodeA; //返回nodeA或者nodeB都是一樣滴
}
}
6. 反轉鏈表
題目描述:定義一個函數,輸入一個鏈表的頭節點,反轉該鏈表並輸出反轉後鏈表的頭節點。
示例:
輸入: 1->2->3->4->5->NULL
輸出: 5->4->3->2->1->NULL
算法一:遞歸
遞歸的核心是:讓每個小問題都以同樣的方式來得到解,因此可以調用自身。
class Solution {
public ListNode reverseList(ListNode head) {
if(head==null || head.next==null)
return head;
ListNode tail = reverseList(head.next); //讓head的子鏈表再去進行反轉
head.next.next = head; //實現鏈表指針指向反轉,添加反向指針
head.next = null; //讓頭結點的下一個指向null,斷開原指向指針
return tail; //返回原尾結點
}
}
算法二:迭代
迭代的核心是:在每次循環中更新變量的值。
class Solution {
public ListNode reverseList(ListNode head) {
if(head==null || head.next==null)
return head;
ListNode pre = null;
while(head!=null){
ListNode tmp = head.next; //備份當前結點的子鏈表
head.next = pre; //實現鏈表指向轉向
pre = head; //更新pre的值
head = tmp; //更新head的值
}
return pre; //返回原尾結點
}
}
Q: 迭代、遞歸、動態規劃?
-
迭代 是
顯式
的循環。是利用變量的原值推算出變量的一個新值。
從程序結構上來講,迭代就是與普通循環。但它與普通循環的區別是,迭代是在循環代碼中不斷使用變量的原值遞推出變量的新值,當前變量的新值又作爲下次循環的初始值。也就是說,迭代時,循環代碼在不同循環輪次中始終是對同一組變量做修正。 -
遞歸 是
隱式
的循環。是程序調用自身,從頂部
開始分解問題,通過解決掉所有分解出來的小問題,來解決整個問題。
從程序結構上來講,遞歸是指函數重複調用自身而實現的循環。這種隱式的循環結束的方式是,當程序滿足終止條件時逐層返回。在循環次數較大的時候,遞歸的效率明顯低於迭代
。遞歸的特點是把多階段問題轉化爲一系列單階段問題,利用各階段之間的關係,逐個求解。即把原問題劃分爲一層層的子問題,然後逐層求解。 -
動態規劃 通常與遞歸相反,其從
底部
開始解決問題,將所有小問題解決掉,進而解決的整個問題。
動態規劃說白了就是記憶化
的遞歸,它把子問題的解臨時存儲在堆棧中,省去了重複計算的步驟,從而提高了算法效率。遞歸是自頂而下的,動態規劃是自底而上的。所謂動態規劃,可以簡單理解爲先用遞歸找出算法的本質並給出初步解然後等效的轉化爲迭代的形式(因爲問題規模較大時,遞歸的效率比迭代低,所以一般採用迭代)。
四、棧和隊列
1. 用兩個棧實現一個隊列
題目描述:用兩個棧實現一個隊列。隊列的聲明如下,請實現它的兩個函數 appendTail 和 deleteHead ,分別完成在隊列尾部插入整數和在隊列頭部刪除整數的功能。(若隊列中沒有元素,deleteHead 操作返回 -1 )
示例:
輸入:
[“CQueue” , “appendTail” , “deleteHead” , “deleteHead”]
[ [] , [3] , [] , [] ] // 這一行作爲上一行函數的參數
輸出:[ null , null , 3 , -1]
棧 是隻能在一端
進行插入或刪除操作的線性表。“後進先出
”
隊列 是隻能在表的一端
進行插入,另一端
進行刪除。“先進先出
”
思想: 創建兩個棧,stack1用於存儲數據
,stack2用於每次執行插入操作的暫存器
(過渡區),
- 入隊方法實現:由於隊列插入元素是在末尾插入,因此先將stack1中存儲的所有元素按序暫存到stack2中,直到stack1空爲止,這時可以調用
stack1.push(value);
將待插入的元素入棧,再把stack2中的元素按序回到stack1中,就實現了隊列在末尾插入
元素。 - 出隊方法實現:若stack1不爲空,直接調用
stack1.pop()
方法刪除棧頂元素,對應刪除了隊列的隊首元素
。
java api 中對 Stack 類的描述
方法和構造器:
class CQueue {
Stack<Integer> stack1;
Stack<Integer> stack2;
public CQueue() {
stack1 = new Stack<>();
stack2 = new Stack<>();
}
//在末尾添加元素
public void appendTail(int value) {
while(!stack1.empty()){ //將棧1中的所有元素暫存到棧2中
stack2.push(stack1.pop());
}
stack1.push(value); //待插入元素入棧,置於棧底位置
while(!stack2.empty()){ //將棧2中的元素還原到棧1中
stack1.push(stack2.pop());
}
}
//刪除隊首元素
public int deleteHead() {
if(stack1.empty())
return -1;
return stack1.pop(); //刪除棧頂元素(即隊首)
}
}
Java集合類型的默認容量以及擴容機制:
-
ArrayList
默認容量是10
最大容量Integer.MAX_VALUE - 8
(Integer.MAX_VALUE = 231-1 )
ArrayList擴容機制,按原數組長度的1.5倍
擴容。如果擴容後的大小小於實際需要的大小,將數組擴大到實際需要的大小。 -
Vector
是線程安全
版的ArrayList,內部實現都是用數組實現的。Vector通過在方法前用synchronized
修飾實現了線程同步功能。
默認容量是10
最大容量Integer.MAX_VALUE - 8
Vector擴容機制,如果用戶沒有指定擴容步長,按原數組長度的2倍
擴容,否則按用戶指定的擴容步長擴容。如果擴容後的大小小於實際需要的大小,將數組擴大到實際需要的大小。 -
Stack
繼承自Vector。添加了同步的push(E e)
、pop()
、peek()
方法,默認容量和擴容機制同Vector。 -
DelayQueue、PriorityQueue
非線程安全的無界
隊列。 -
HashMap
是基於數組和鏈表實現的。HashMap的容量必須是2的冪次方
默認容量是16
最大容量2的30次方
HashMap擴容機制,擴容到原數組的2倍
-
Hashtable
默認容量是11
最大容量Integer.MAX_VALUE - 8
Hashtable擴容機制,擴容到原數組的2倍+1
2. 有效的括號
算法一: 暴力消除。使用replace()
方法將成對 的"()""[]""{}"替換成 “” 空字符串,最終判斷剩餘的字符串是否爲空,爲空則說明所有括號有效。(該題不用考慮括號優先級問題)
String replace(char oldChar, char newChar); //char也可以是String
class Solution {
public boolean isValid(String s) {
if(s.length()==0)
return true;
char c=' ';
int len=0;
while(len != s.length()){
len = s.length();
s=s.replace("()",""); //將字符串中的()替換成空字符
s=s.replace("[]",""); //將字符串中的[]替換成空字符
s=s.replace("{}",""); //將字符串中的{}替換成空字符
}
return (s=="");
}
}
算法二: 利用棧。(好方法)
class Solution {
public boolean isValid(String s) {
if(s.length()==0)
return true;
Stack<Character> st = new Stack<>();
st.push(s.charAt(0)); //先把第一個元素入棧
for(int i=1;i<s.length();i++){ //遍歷字符串的每一個元素
if(!st.empty()){ //如果棧不爲空,可將棧頂元素與當前元素進行匹配
if(s.charAt(i) == ')' && st.peek()=='(')
st.pop();
else if(s.charAt(i) == ']' && st.peek()=='[')
st.pop();
else if(s.charAt(i) == '}' && st.peek()=='{')
st.pop();
else
st.push(s.charAt(i));
}
else //如果棧爲空,就把當前元素入棧
st.push(s.charAt(i));
}
return st.empty(); //如果棧爲空則返回true,否則返回false
}
}
3. 二叉樹的層平均值(★)
題目描述:給定一個非空二叉樹, 返回一個由每層節點平均值組成的數組。
Java集合主要由2大體系構成,分別是Collection
體系和Map
體系,其中Collection
和Map
分別是2大體系中的頂層接口
。
-
Collection主要有三個子接口,分別爲
List(列表)
、Set(集)
、Queue(隊列)
。其中,List、Queue中的元素有序可重複,而Set中的元素無序不可重複。(有關Set集在本節的最後補充) -
Map同屬於java.util包中,是集合的一部分,但與Collection是相互獨立的,沒有任何關係。Map中都是以
key-value
的形式存在,其中key必須唯一,主要有HashMap、HashTable、TreeMap三個實現類。
那麼下面就來看看在java api
中如何對這些接口和類進行描述的:
List(列表)接口:
- 實現類——ArrayList:底層通過數組實現,隨着元素的增加而
動態擴容
。
我們在使用數組時有一些很不好的體驗,比如在數組的兩個數據間插入數據是很麻煩的,而且在聲明數組的時候,必須同時指明數組的長度,數組的長度過長,會造成內存浪費,數組和長度過短,會造成數據溢出的錯誤。爲了克服數組的缺點,ArrayList
出現了,它是用於數據存儲
和檢索
的專用類,它的大小是按照其中存儲的數據來動態擴充與收縮
的。所以,我們在聲明ArrayList對象時並不需要指定它的長度。它可以很方便的進行數據的添加,插入和移除。
- 實現類——LinkedList:底層通過鏈表來實現,隨着元素的增加不斷向鏈表的後端增加節點。
Queue(隊列)接口:
Q1: 應該用接口類型
來引用對象還是實現類的類型
來引用對象?
結論:優先使用接口而不是類來引用對象
。
但是,當你用接口類型
來引用對象時,如果某些方法僅
存在於實現類中,那麼你是不能直接調用的,否則會報錯。
也就是說,要使用接口
來引用對象是有條件的——你即將要使用的方法全部是接口中的方法,不能單獨使用實現類獨有的方法。當然,如果你想使用實現類本身的方法時,可以選擇用實現類的類型來引用對象。
Q2: double 和 Double(int 和 Interger、float 和 Float、string 和 String)?
本質區別:double
是基本數據類型,Double
是封裝的類。
- double是基本的數據類型,初始化:
double i = 2.45;
- Double是double的封裝類,初始化:
Double di = new Double(2.45);
- Double和double都可以表示某一個數值;
- Double和double不能夠互用,因爲他們兩種不同的類型;比如:list是一個已經實例化的列表,那麼:
不可以!list.add(i);
list.add(di);
可以!
在瞭解了這些接口和類之後,我們就可以開始解題了。
算法: 層次遍歷的廣度優先搜索。
class Solution {
public List<Double> averageOfLevels(TreeNode root) {
List<Double> average = new ArrayList<>(); //實例化一個底層爲數組的列表,用來存放各層平均值
Queue<TreeNode> queue = new LinkedList<>(); //實例化一個LinkedList來實現隊列接口
double sum;
queue.add(root);
while(!queue.isEmpty()){ //當隊列不爲空時循環
int m = queue.size(); //記錄每次循環時隊列的大小(該層的結點數量)
sum = 0;
for(int i=1;i<=m;i++){ //只從隊列中取該層的所有結點(因爲每一層的結點數量就是剛開始隊列的大小m)
TreeNode node = queue.poll(); //隊首元素出隊
sum += node.val;
if(node.left!=null) //使左結點入隊
queue.add(node.left);
if(node.right!=null) //使右結點入隊
queue.add(node.right);
}
average.add( sum/m ); //計算平均值並放入數組列表中
}
return average;
}
}
補充一下Set:
實際上,在看過源碼後會發現,Set的實體類主要就是以map爲基礎,相對應的使用環境和意義也和對應的map相同。Set主要包含三種存放數據類型的變量,分別是HashSet
、LinkedHashSet
、TreeSet
.
其中,HashSet、LinkedHashSet無序且不可重複。TreeSet是以TreeMap作爲存儲結構的,有序不可重複。
來看看在 java api
中如何對 Set 集進行描述的:
Set 常用的方法:
注意1:若對對象進行重複添加,是沒有任何作用的,重複添加多個相同對象時,Set中只保留一個,另外,添加null
空指針也是可以的。
注意2:Set中元素因爲其無序性,所以不能用 get() 方法來查找,只能通過foreach()
或者iterator()
方法遍歷,並且每次遍歷輸出的結果順序是不一樣的。
看一下Iterator
接口的描述:
Iterator 常用的方法:
Q3: 爲什麼會構造Set這個集合呢?
實際上就是利用Map
的key-value鍵值對
的方式,通過key的唯一的特性,主要將Set構建的對象放入key中,以這樣的方式來使用集合的一些特性,從而可以直接用Set來進行調用。
4. 字符串的排列
題目描述:輸入一個字符串,打印出該字符串中字符的所有排列。你可以以任意順序返回這個字符串數組,但裏面不能有重複元素。
示例:
輸入:s = “abc”
輸出:[“abc”,“acb”,“bac”,“bca”,“cab”,“cba”]
算法: 回溯法。
回溯算法實際上一個類似枚舉的搜索嘗試過程,主要是在搜索嘗試過程中尋找問題的解,當發現已不滿足求解條件時,就“回溯”返回,嘗試別的路徑。回溯法是一種選優搜索法,按選優條件向前搜索,以達到目標。但當探索到某一步時,發現原先選擇並不優或達不到目標,就退回一步重新選擇,這種走不通就退回再走的技術爲回溯法,而滿足回溯條件的某個狀態的點稱爲“回溯點”。許多複雜的,規模較大的問題都可以使用回溯法,有“通用解題方法”的美稱。
思想: 首先把字符串轉爲字符數組,尋找排列方案的思路是:依次固定第0位、第1位、……、第n位字符。比如,我們都知道第0位有n種情況,若已經固定了第0位,那麼第1位有n-1種情況,若已經固定了第1位,則第2位有n-2種情況,……,最後,第n位只有1種情況。所以,關鍵在於:固定當前位字符,對剩餘的位置進行依次固定尋找排列方案,這樣相當於深度優先搜索,完成後再對當前位進行循環固定,也就是說選擇其他的字符來作爲當前位。
執行一次dfs()
目的是固定當前第x位,進行深度優先搜索來對剩餘位找排列方案。所以初始參數爲0,因爲首先要固定第0位,根據第0位的情況往下找,而每一個字符都要依次作爲第0位,可以使用交換法來實現,把每個字符依次放在第0個位置,完了後要交換回來。由於給的字符串中有可能含有重複字符,那麼排列組合就會有重複的排列組合,所以需要固定每一位、去重,如果當前位已經有重複的元素了,那麼就不用算兩遍。
一定要想清楚x和i分別的作用:
x
:代表固定的第x位,因此只能是它作爲dfs()
的參數。
i
:遍歷數組的索引,它的作用是把數組的每個字符都拿出來,作爲固定的第x個位,實現方法即: swap(i,x);
交換x和i指向的元素。
時間複雜度:O(n!)
空間複雜度 :O(N2)。全排列的遞歸深度爲 N ,系統累計使用棧空間大小爲 O(N);遞歸中輔助 Set 累計存儲的字符數量最多爲 N + (N-1) + … + 2 + 1 = (N+1)N/2N + (N−1) + … + 2 + 1 = (N+1)N/2 ,即佔用 O(N2)的額外空間。
class Solution {
List<String> list = new ArrayList<>();
char[] c;
public String[] permutation(String s) {
c = s.toCharArray();
dfs(0);
return list.toArray(new String[list.size()]); //將list轉爲數組。參考下方第一點
}
void dfs(int x){ //固定當前第x位,進行深度優先搜索來排列剩餘的位。
if(x == (c.length - 1)){
list.add(String.valueOf(c)); //把當前字符串數組添加到列表中,作爲一種排列方案。參考下方第二點
return;
}
HashSet<Character> hs = new HashSet<>(); //每一次dfs都創建一個HashSet,實現去重。
for(int i=x;i<c.length;i++){ //廣度遍歷,依次把數組中的每一個字符都當作當前第x位(交換位置實現)
if(hs.contains(c[i])){ //如果HashSet中包含了字符c[i],說明這是重複字符,當前位已經固定過了,直接跳過。
continue;
}
hs.add(c[i]);
swap(i,x); //交換,相當於選擇第i個字符來作爲當前固定的第x位
dfs(x+1); //深度搜索,遞歸,開始固定下一位。
swap(i,x); //還原數組
}
}
void swap(int i,int j){ //交換,索引值是不變的,即x和i不變,變的是它們指向的元素值。
char tmp = c[i];
c[i] = c[j];
c[j] = tmp;
}
}
1、toArray() 和 toArray(T[] a)
List和Set接口都提供了一個轉數組的非常方便的方法toArray()。toArray()有兩個重載的方法:
Object[] toArray();
是將list或者set直接轉爲Object[] 數組
。但是如果你這樣寫的話:String[] array= (String[])list.toArray();
運行會報錯。因爲java中的強制類型轉換隻是針對單個對象的,想要偷懶將整個數組轉換成另外一種類型的數組是不行的!因此不能直接將Object[] 轉化爲String[],轉化的話只能是取出每一個元素再轉化。T[] toArray(T[] a);
是將list或者set直接轉化爲你所需要類型的數組
。非常好用,且常用!一般寫法:String[] list_array = list.toArray(new String[list.size()]);
2、static String valueOf(char[] data)
String類的靜態方法,因此可以直接通過類名調用:String.valueOf();
作用是將 字符串數組/int整型/char字符 等等轉爲字符串。
3、HashSet
HashSet一般常用於去重,即:去除重複元素。
五、哈希表
1. 第一個只出現一次的字符(★)
題目描述:在字符串 s 中找出第一個只出現一次的字符。如果沒有,返回一個單空格。
算法一: 雙指針嵌套遍歷。相當於把原字符串複製一份,對兩個完全相同的字符串進行比較,看是否有相同字符。(當然,索引相同的情況要排除掉。)
class Solution {
public char firstUniqChar(String s) {
if(s.length()==0)
return ' ';
int i,j=0;
char c1,c2;
for(i=0;i<s.length();i++){ //第一次對字符串進行遍歷
c1 = s.charAt(i);
for(j=0;j<s.length();j++){ //第二次對字符串進行遍歷
c2 = s.charAt(j);
if((c1 == c2) && (i != j)) //若當前字符相同並且索引不同的話,說明在原字符串中它是重複字符,則退出內循環。
break;
}
if(j==s.length()) //如果第二次遍歷完了,都沒有找到相同元素,說明我們就get到了第一個只出現一次的字符。退出外循環。
break;
}
if(i<s.length() && j==s.length()) //如果第一次遍歷沒有超過尾字符,而第二次超過了尾字符,說明找到了第一個只出現一次的字符,返回它即可。
return s.charAt(i);
else //否則返回空字符。
return ' ';
}
}
算法二: 哈希表
class Solution {
public char firstUniqChar(String s) {
if(s.length()==0)
return ' ';
char c=' ';
HashMap<Character, Integer> hm= new HashMap<>(); //初始化一個哈希表
for(int i=0;i<s.length();i++){
c = s.charAt(i);
if(hm.containsKey(c)) //如果哈希表中存在此鍵,就把它的值加1
hm.put(c,hm.get(c)+1);
else //若不存在此鍵,就把它的值置爲1
hm.put(c,1);
}
for(int j=0;j<s.length();j++){ //遍歷字符串,找到第一個鍵所對的值爲1的元素,直接返回它
c = s.charAt(j);
if(hm.get(c)==1)
return c;
}
return ' '; //若遍歷結束都沒有return,則返回空格字符
}
}
Q: 什麼是哈希表?
哈希表 又稱散列表,其基本思路是,設要存儲的元素個數爲n,設置一個長度爲m(m>=n)的連續內存單元,以每個元素的關鍵字爲自變量,通過一個稱爲哈希函數的函數,把關鍵字映射爲內存單元的地址,並把該元素存儲在這個單元中。該映射的地址也叫哈希地址,由此構造的線性表存儲結構稱爲哈希表。
哈希表 是數組 + 鏈表
的數據結構,數組中存的是鍵值對
,鏈表的存在是爲了解決哈希衝突。如下圖所示,當對鍵 key12 進行哈希函數映射後,得到內存單元索引爲3,然後發現索引3並不爲空,那麼這時候就需要添加鏈表來存這個鍵值對。
哈希表的常用方法總結:
HashMap<Integer, String> hm = new HashMap<>();
//Integer是鍵的類型,String是值的類型。當然可以換成其他的,比如字符類型Character等
- 初始化時,HashMap只初始化了負載因子(使用默認值0.75),並沒有初始化table數組。(負載因子,當 已使用容量 > 總容量 * 負載因子 時,會啓動擴容。)
- 其實HashMap使用的是
延遲初始化
策略,當第一次put的時候,才初始化table(此時table是null)。當第一次put的時候,HashMap會判斷當前table是否爲空,如果是空,會調用resize()
方法進行初始化。resize()
方法會初始化一個容量大小爲 16 的數組,並賦值給table。
public V put(K key, V value) //插入鍵值對數據
public V get(Object key) //根據鍵值獲取鍵值對值數據
public int size() //獲取Map中鍵值對的個數
public boolean containsKey(Object key) //判斷Map集合中是否包含鍵爲key的鍵值對
boolean containsValue(Object value) //判斷Map集合中是否包含值爲value的鍵值對
public boolean isEmpty() //判斷Map集合中是否沒有任何鍵值對
public void clear() //清空Map集合中所有的鍵值對
public V remove(Object key) //根據鍵值刪除Map中鍵值對
2. 兩數之和
算法一:暴力法
兩次for循環嵌套,對每一個元素都去查找剩餘的其他元素,看是否相加之和爲target。
時間複雜度:O(n2)
class Solution {
public int[] twoSum(int[] nums, int target) {
int n,m;
int[] a=new int[2];
for(n=0;n<nums.length;n++){
for(m=n+1;m<nums.length;m++){
if(nums[n]+nums[m]==target){
a[0]=n;
a[1]=m;
break;
}
}
}
return a;
}
}
算法二:哈希表
記住,HashMap
查找的時間複雜度爲O(1)
(因爲它是靠計算hashcode索引地址來查找的,並非遍歷)
思想:創建一個哈希表,鍵
存數組元素的值,值
存數組元素的索引。遍歷數組,對其每一個元素都來查找哈希表中是否有元素與其相加和爲target,若有則拿出來,若無則把當前元素再放入哈希表。因此,我們只需要花O(n)
時間來遍歷長度爲n的數組,而每次哈希表查找的時間複雜度僅爲O(1)
。
時間複雜度:O(n)
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] a = new int[2];
HashMap<Integer, Integer> hm = new HashMap<>();
for(int i=0;i<nums.length;i++){ //遍歷數組
int x = target - nums[i];
if(hm.containsKey(x)){ //查找哈希表中是否存在目標元素
a[0] = i;
a[1] = hm.get(x);
break;
}
hm.put(nums[i], i); //最後再把當前元素放入哈希表
}
return a;
}
}
注意:一定是在每次循環的最後才能把當前元素放入哈希表。如果剛開始就把它放進去了,那麼會出現這種情況:哈希表中已經存在該key了(因爲數組中的元素很有可能有重複的),再放一個相同的進去的話,原key就會被覆蓋,而相應地value也會被覆蓋。那麼就找不到例如3 + 3 = 6
的情況了。因此必須是在沒有加入哈希表的前提下來進行查找纔行。
記一記:
- 對於HashMap而言,在執行
put(key, value)
方法時,若存在相同關鍵字key,則會直接替換原鍵值對。 - Map是
接口
,不能用new出對象;HashMap是Map接口的實現類
,可以new出對象。