是时候对自己做的编程题来一个总结了。剑指offer上的题目都很经典,注意体会出题者想要你应用的算法意图是什么,毕竟一道题可能有很多做法,使用一种复杂的数据结构(Map,ArrayList)能解决的算法问题可能并不是出题者考察算法的本意(二分法,插入排序法,递归法)。
1 链表的理解
通过亲自实践编程对链表理解更深了。返回链表头结点能够返回整个链表,这是因为链表是后向性的(单链表)。
对于链表的操作包括 查询和修改
查询操作是在原链表上移动“指针”寻找突破口。比如链表环的入口结点?主要应用的操作是后移操作:
Node = Node.next;
修改操作是对原有链表剪枝和拼接形成新的链表。如复杂链表的复制,链表的去重,链表的反转。主要应用的操作是指向操作:
Node.next = currentNode.next;
这里需要注意一点:默认单链表只有一个后向指针。对后向指针修改后则保留最新的配置信息。原来的后向指针失效。
指针赋值可以修改被赋值对象的后向指针。仍然满足单链表只有一个后向指针。
对于重构链表的操作:套路就是创建一个前置指针,对该创建指针赋值给一个工作指针,并使指向原链表表头。当工作指针更改完毕后,返回前置指针则返回了新链表的表头。即指针赋值操作修改被赋值对象的后向指针且满足单链表原则。
特别要注意链表尾端的空结点的处理。如果在循环中需要,应该明确空结点没有值,所以访问空结点的val会报错。java.lang.NullPointerException。
2 二叉树的理解
通过亲自实践递归对于二叉树的考点有了一定的掌握。对于二叉树的考察一般都为递归法。
少数题目可以用由顶到下的循环解决(如题目“中序遍历的下一节点”,因为该题目的输入并不是根节点,可能是任意一个节点,所以不存在递归的条件)。
对于递归的题目,简单的话一个函数可以完成(涉及二叉树的深度问题),该问题可以对问题函数进行递归,用max函数进行截断得到深度。
Math.max(root.left,root.right);
多数题目需要两函数法。(如BST中第K个节点。BST改写双向链表)
这类题目在公共定义域定义需要的值和数据结构。此处的公共内存变量仅仅为线性变化的变量如count++,对于在递归函数中不规则变化的变量,如
int left= Depth(root.left)
int right = Depth(root.right)
应定义在递归函数内,不能在公共内存区定义。
问题函数仅仅包含递归函数然后返回需要的数据结构。
主要的工作由递归函数iinorder();完成。或由判别函数完成。 问题函数中的数据结构在递归函数中体现改变。
3动态规划题目的总结
动态规划题目的本质是找规律。找到状态方程是关键。最简单的如青蛙跳台阶的问题,以及拼图问题。主要的注意点在于不重不漏:本次操作的结果和上一次操作紧接着一次操作,以及上上次操作之后紧接另一操作的关系是什么(求和)。
3.1最大利润问题
再有就是最大利润问题:借助Max函数实现了非线性截断,比较当前利润与过往最大利润的最大值。
3.2最值求解
矩阵问题是一类动态规划问题。包括最值求解问题和路径回溯问题两种。都是路径有多种可能选择,需要Max函数截断。单值求解不需要回溯和剪枝,起点和终点固定。方向为向右& 向下。解法为两重for循环,判断每个位置的最优解:其中左边界和上边界有唯一最优解。其余则需要比较该方格的左值和上值,
dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1])+board[i][j]; //取最值后与当前方格元素相加。右下角元素即为整个矩阵的最值。
dp[i] = Math.max(dp[j] + Math.max(dp[j] *(i-j),j*(i-j)); // form j = 1 to j = i;
3.3路径回溯
这是一类最难的深度优先搜索问题。需要判断条件(visited[boolean] )(这个是在每次回溯过程中显性或隐性判断的,对于树的搜索可不需要这个条件,因为树是有规律的节点排列,不会重复访问。坐标棋盘则必须要这个visit条件),同时判断横纵坐标的边界条件,以及题目的约束条件(树的根的值的和为某一个值,坐标和小于target,当前字符串和题给字符串吻合)。综上进行回溯和必要的剪枝可以解决这类问题。
机器人运动坐标点问题是简化版的回溯问题,也不需要visit判断条件。
4 数据结构总结
数据结构是基础,有些题有了思路但是一写就错,对于各种数据结构的使用不熟练。现在做一个总结。
数组和二维数组的使用时最基础的,
Java中数组的定义是对对象的引用,所以在new时一定要初始化,否则找不到在堆上创建的对象。
int[] a;//只定义不初始化
int[][] arrays = new int[m][n]; //二维数组,引用必须初始化,默认为null
int[] array = new int[length]; //一维数组,引用必须初始化。默认为null
定义字符数组以及字符串
char[] b;
String[] c;//只定义,不初始化
String[] str = new String[m]; //字符串数组的引用必须初始化。
char[]b = {'1','2','3'}; //直接赋值
String str = ""; //str赋空字符串
char[] char = new Char[m]; //字符数组的引用必须初始化。
StringBuffer strbuf = new StringBuffer(); //Strbuf数据结构的定义
char[] a = str.toCharArray(); //将str字符串按字符放入字符数组a的每一个位置中
char charbyte = str.charAt(i); //截取str字符串的第i个字符赋给charbyte
集合和泛型的使用
List<Integer> list = new ArrayList<>(); //ArrayList创建,主要用于get,set等查询操作
Stack<Integer> stack = new Stack<>(); //Stack栈数据结构的创建,可用于非递归的二叉树遍历实现
List<TreeNode> node = new LinkedList<>(); // LinkedList数据结构的创建,主要适用于add,remove操作
Map<Character,Integer> map = new HashMap<>(); //哈希表的创建
PriorityQueue<Integer> left = new PriorityQueue<>((o1, o2) -> o2 - o1); //存储整型数据的大顶堆,重载了方法
PriorityQueue<Integer> right = new PriorityQueue<>(); // 存储整型数据的小顶堆
5 位运算相关考察
位运算是一类编程题目。体现的技巧性较强。需要多总结。
位逻辑运算符包括4个:按位与(&) ,按位或(|),按位异或(^),按位取反(~)
位移运算符包括:右移位运算符(>>),左移位运算符(<<)
异或加移位可以解决数字二进制表示中1的个数问题。
6 排序问题
最重要的算法可能就是排序算法了,面试必须会的,上手就能写,包括快速排序,归并排序,插入排序等
6.1 快速排序
输入待排序数组和起止下标。通过quicksort算法将数组下标为0的元素排到数组的正确位置。并返回0号归位元素的下标。然后递归调用quicksort算法,对起点到返回下标处的子数组排序,以及返回下标+1到终点的子数组排序。
排序过程就是设计两个指针,分别指向头和尾。比较头尾指针元素和零号元素是否满足:头指针元素小于零号元素,尾指针元素大于零号元素,满足则向对应向中间位置移动。直到头尾指针都出现不满足情况,交换对应元素。继续移动指针。直至头指针大于尾指针。退出循环。交换零号元素和尾指针位置元素。完后对零号元素的排序。
//亲测有效的代码 5.27
public static void fastdg(int[] nums,int l,int r){
if(l>=r)
return;
int i = l;
int j = r;
while (i<j){
while(i<j && nums[j]>nums[l])
j--;
while(i<j && nums[i]<=nums[l])
i++;
swap(nums,i,j);
}
swap(nums,l,i);
fastdg(nums,l,i-1);
fastdg(nums,i+1,r);
}
public static void swap(int[] nums,int x,int y){
int tmp = nums[x];
nums[x] = nums[y];
nums[y] = tmp;
}
// 亲测有效的代码 6.8
//自己想思路写了一版快排的算法,反复测试无误,且比上一版(5.27)更容易记。5.27版要求先while j,再while i.否则出错。本版本不要求i,j的顺序,只在交换i,j时增加条件:i<=j.并且while循环可以取等号。
private static void fastsort(int[] nums,int i,int j){
if(i>=j)
return;
int tmp = nums[i];
int start = i,end = j;
while(start<=end){
while(start<=end && nums[start]<=tmp){
start++;
}
while(start<=end && nums[end]>tmp){
end--;
}
if(start<=end)
swap(start,end,nums);
}
swap(i,end,nums);
fastsort(nums,i,end-1);
fastsort(nums,end+1,j);
}
private static void swap(int i,int j,int[] nums){
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
6.2 归并排序
归并排序的思想是化整为零,积零为整。“对于一个大的问题无从下手,那么把它分解为小的问题,每一个小问题都是一个排序子问题。解决小问题,然后将其链接则可以解决大问题。”与快速排序一样,本算法也会改变原数组的值,
输入待排序数组并知道首尾位置后,归并排序以递归为解决方法:递归首尾->递归首位置到中间位置的子数组以及递归中间位置到末位置的子数组。最终首先对零号位置和一号位置元素Merge。Merge算法所做的就是对两个元素分别定义两个临时存储空间,将较小的元素排在temp[0],较大的元素排入temp[1],然后分别覆盖原数组的零号位和一号位。
对于所有的最后一级递归完成同样的排序后,进行Merge,这次是按元素下标顺序两两合并为四。比较和归位后对原数组相应位置进行覆盖。
对于该级完成递归后,再对上一级进行Merge,逐级Merge,即逐级归并。最后完成对整个数组的排序。
private void mergeSort(int[] nums, int l, int h) {
if (h - l < 1)
return;
int m = l + (h - l) / 2;
mergeSort(nums, l, m);
mergeSort(nums, m + 1, h);
merge(nums, l, m, h);
}
private void merge(int[] nums, int l, int m, int h) {
int i = l, j = m + 1, k = l;
while (i <= m || j <= h) {
if (i > m)
tmp[k] = nums[j++];
else if (j > h)
tmp[k] = nums[i++];
else if (nums[i] <= nums[j])
tmp[k] = nums[i++];
else {
tmp[k] = nums[j++];
this.cnt += m - i + 1; // nums[i] > nums[j],说明 nums[i...mid] 都大于 nums[j]
}
k++;
}
for (k = l; k <= h; k++)
nums[k] = tmp[k];
}
6.3 插入排序
插入排序同样适合数据量较小的数据排序,且由于没有空间消耗,自身数组的元素顺序在排序过程中被修改。排序主要借助while循环。相比递归算法对内存更友好。排序时将待排序数组分成已排序部分和未排序部分。逐个元素进入while循环。对于未排序部分的起始元素,分别于已经排好顺序部分的每个元素从大到小比较,这样做是为了当找到满足待排序元素小于已排序部分某一元素时,其后的所有元素均向后移动了一个位置。留出空位置可以给待排序的元素。做到多次移动,一次赋值。
public class InsertSort {
public static void insertSort(int[] a) {
int i, j, insertNote;// 要插入的数据
for (i = 1; i < a.length; i++) {// 从数组的第二个元素开始循环将数组中的元素插入
insertNote = a[i];// 设置数组中的第2个元素为第一次循环要插入的数据
j = i - 1;
while (j >= 0 && insertNote < a[j]) {
a[j + 1] = a[j];// 如果要插入的元素小于第j个元素,就将第j个元素向后移动
j--;
}
a[j + 1] = insertNote;// 直到要插入的元素不小于第j个元素,将insertNote插入到数组中
}
}
public static void main(String[] args) {
int a[] = { 38,65,97,76,13,27,49 };
insertSort(a);
System.out.println(Arrays.toString(a));
}
}