校招中遇到的常見算法題總結(持續更新)
主要是相關的題型,原題較少
1、最長公共子序列(leetcode-1143)
經典的二維動態規划問題之一
- 動態規划難點在於如何定義dp,此處為尋找兩個字符序列的最長公共子序列,即從頭到尾中去最長。故可將dp[i][j]定義為字符串s1,s2的長度為i和j的前綴的最長子序列長度
即dp[i][j]表示s1.substring(0,i)和s1.substring(0,j)的最長子序列長度
-
隨后是邊界問題
顯然,當i=0時,dp[0][j]都為0,同理j=0時,dp[i][j]=0,dp[0][0]=0 -
緊接着就是一般性分析(找i,j逐漸增大時的遞推式,顯然和最后一個字符相關)
-
當s1.charAt(i)==s2.charAt(j)時,則dp[i][j]等於dp[i-1][j-1]+1
eg: s1: acg; s2:dag dp[3][3]=dp[2][2]+1=1+1=2
-
當s1.chas1.charAt(i)!=s2.charAt(j)時,dp[i][j-1],dp[i-1][j]取其中的較大值
eg: s1: acf; s2:afgd dp[3][3]=Math.max(dp[2][3],dp[3][2])=max(1,2)=2
-
總結
- dp[i][j]=dp[i-1][j-1]+1 when s1.charAt(i)==s2.charAt(j);
- dp[i][j]=Math.max(dp[i][j-1],dp[i-1][j]) when s1.charAt(i)!=s2.charAt(j);
Java代碼如下:
public int longestCommonSubsequence(String s1, String s2) {
int n1=s1.length(),
n2=s2.length();
int[][] dp=new int[n1+1][n2+1];
int result=0;
dp[0][0]=0;//邊界值
for(int i=1;i<=n1;i++){//邊界值
dp[i][0]=0;
}
for(int j=1;j<=n2;j++){//邊界值
dp[0][j]=0;
}
for(int k1=1;k1<=n1;k1++){//一般性分析
char c=s1.charAt(k1-1);
for(int k2=1;k2<=n2;k2++){
if(s2.charAt(k2-1)==c){//最后一個字符相等時
dp[k1][k2]=dp[k1-1][k2-1]+1;
}else{//最后一個字符不等時
dp[k1][k2]=Math.max(dp[k1][k2-1],dp[k1-1][k2]);
}
}
}
return dp[n1][n2];
}
2、手撕快速排序(基准值隨機選取)
class Solution {
public int[] sortArray(int[] nums) {
quickSort(nums,0,nums.length-1);
return nums;
}
private static void quickSort(int[] nums,int l,int r){
if(l<r){
int privot=partion(nums,l,r);
quickSort(nums,l,privot-1);
quickSort(nums,privot+1,r);
}
}
private static int partion(int[] nums,int l,int r){
int i=new Random().nextInt(r-l+1)+l;//此處隨機選擇快排基准值的位置
swap(nums,r,i);//將其移動到末尾
return partionCount(nums,l,r);
}
private static int partionCount(int[] nums,int l,int r){
int privot=nums[r];
int i=l-1;
for(int j=l;j<r;j++){
if(nums[j]<=privot){//交換小於基准和大於基准的兩個數
i++;
swap(nums,i,j);
}
}
swap(nums,i+1,r);
return i+1;
}
private static void swap(int[] nums,int i,int j){
int tmp=nums[i];
nums[i]=nums[j];
nums[j]=tmp;
}
}
3、K個一組翻轉鏈表(leetcode-25)
主要問題可概括如下:
- 翻轉K個節點的鏈表(翻轉鏈表應該都做過,原理一樣)
- 反轉后的首尾鏈表和原鏈表首尾的拼接(保存前一個結點和后一個節點;返回節點為翻轉后的第一個和最后一個節點,即可拼接)
- 第一個節點無法保存第一個節點(要么特設,要么添加無實際作用的頭節點)
- 最后少於k個節點時不需要翻轉(因為新增頭節點的緣故,直接返回頭節點.next即可)
代碼如下:
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode headPre=new ListNode(0);//添加頭節點
headPre.next=head;
ListNode pre=headPre;
while(head!=null){
ListNode tail=pre;
for(int i=0;i<k;i++){
tail=tail.next;
if(tail==null){
return headPre.next;//最后少於k個節點可以直接返回當前鏈表
}
}
ListNode nxt=tail.next;
ListNode[] l=reverseKList(head,tail);
head=l[0];
tail=l[1];
pre.next=head;//拼接原鏈表
tail.next=nxt;//拼接原鏈表
head=nxt;
pre=tail;
}
return headPre.next;
}
public static ListNode[] reverseKList(ListNode head,ListNode tail){//翻轉鏈表操作(通常情況翻轉整條鏈表時tail.next為null)
ListNode prev=tail.next;
ListNode h=head;
ListNode t=tail.next;
while(h!=t){
ListNode nex=h.next;
h.next=prev;
prev=h;
h=nex;
}
return new ListNode[]{tail,head};
}
}