目录

一、基本编程思维小总结

(一)二进制中1的个数

(二)调整数组顺序使奇数位于偶数前面

(三)旋转数组的最小数字

(四)和为S的两个数字

(五)和为S的连续正数序列

二、队与栈相关基本编程练习与思考

(一) 两个栈实现一个队列

(二)两个队列实现一个栈

(三)包含min函数的栈

(四)判断栈的压入对应弹出序列是否正确

三、链表相关基本编程练习与思考

(一)反转链表

(二)链表中倒数第k个结点

(三)两个链表的第一个公共结点

(四)合并两个排序的链表

四、二叉树相关基本编程练习与思考

(一)二叉树的深度

(二)判断平衡二叉树

五、其他思维算法题的小总结

(一)回文串

(二)字符串全排列

(三)字符串转数字

(四)最大公约数和最小公倍数

(五)最长公共字串

(六)字典序列化

(七)路径规划

(八)股票交易日 or 风口的猪

股票交易日

风口的猪

最多一次的情况

(九)统计文件中单词出现次数前50的单词(不考虑大文件情况)


一、基本编程思维小总结

(一)二进制中1的个数

输入一个整数,输出该数二进制表示中1的个数。例如把9表示成二进制是1001,有2位是1,故输入是9,输出是2。其中负数用补码表示。

分析这里涉及到一个技巧,把一个整数减1,在和原整数做与运算,会把该整数最右边一个1变成0,那么一个整数的二进制表示中有多少个1,就可以进行多少次这样的操作。代码如下:

public class Solution {public int  NumberOf1(int n) {int result=0;while (n!=0) {result++;n = n&(n-1);}return result;}
}

(二)调整数组顺序使奇数位于偶数前面

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。

分析要保证奇数和奇数,偶数和偶数之间的相对位置不变。就只能采用顺次移动或相邻交换了,故和下面做法一致。

==时间复杂度高==

/*** 1.要想保证原有次序,则只能顺次移动或相邻交换。* 2.i从左向右遍历,找到第一个偶数。* 3.j从i+1开始向后找,直到找到第一个奇数。* 4.将[i,...,j-1]的元素整体后移一位,最后将找到的奇数放入i位置,然后i++。* 5.終止條件:j向後遍歷查找失敗。*/
public class Solution {/*** 功能描述:将数组进行排序,使得所有奇数位于偶数前面,原位置相对不变** @param array* @author yanfengzhang* @date 2020-04-29 10:45*/public static void reOrderArray(int[] array) {if (null == array || array.length <= 0) {return;}int i = 0, j;while (i < array.length) {while (i < array.length && !isEven(array[i])) {i++;}j = i + 1;while (j < array.length && isEven(array[j])) {j++;}if (j < array.length) {int tmp = array[j];for (int j2 = j - 1; j2 >= i; j2--) {array[j2 + 1] = array[j2];}array[i++] = tmp;} else {// 查找失敗break;}}}/*** 功能描述:判断当前数是否为偶数** @param number* @return boolean* @author yanfengzhang* @date 2020-04-29 10:58*/private static boolean isEven(int number) {if ((number & 1) == 0) {return true;}return false;}
}

==降低时间复杂度==

public class Solution {public void reOrderArray(int [] array) {int evenIdx = -1;boolean firstEven = true;for(int i=0;i<array.length;i++) {if(array[i]%2==0) {if(firstEven==true) {evenIdx=i;firstEven = false;}}else {if(evenIdx!=-1) {int evenEnd = i-1;int tmpOdd = array[i];for(int j=evenEnd;j>=evenIdx;j--) {array[j+1]=array[j];}           array[evenIdx]=tmpOdd;evenIdx++;                    }}             }}
}

==利用空间换时间==

public class Solution {public void reOrderArray(int [] array) {if (null == array || array.length <= 0) {return;}List<Integer> evenList = new ArrayList<Integer>();List<Integer> oddList = new ArrayList<Integer>();for (int i = 0; i < array.length - 1; i++) {if (isEven(array[i])) {evenList.add(array[i]);} else {oddList.add(array[i]);}}int oddCount = oddList.size();for (int i = 0; i < oddList.size(); i++) {array[i] = oddList.get(i);}for (int i = 0; i < evenList.size(); i++) {array[oddCount + i] = evenList.get(i);}}/*** 功能描述:判断当前数是否为偶数** @param number* @return boolean* @author yanfengzhang* @date 2020-04-29 10:58*/private static boolean isEven(int number) {if ((number & 1) == 0) {return true;}return false;}
}

更多测试代码见:https://github.com/ZYFCodes/ProgrammingExercises/blob/master/src/main/java/org/zyf/programming/arrayexercises/EvenAndOdd.java

(三)旋转数组的最小数字

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。

分析:首先得明白什么是旋转数组,递增排序的数组的一个旋转:[1,2,3,4,5]→[3,4,5,1,2]。所以我们分析的是[3,4,5,1,2],找到其中的最小数。旋转后的数组实际上可以划分为两个排序的子数组。而且前面的子数组元素大于或等于后面子数组的元素,最小元素刚好为两个子数组的分界线。

按思路整理步骤:

  • 先设置两个指针,分别如图所指,前面的子数组元素大于或等于后面子数组的元素,所以有以上两种情况,分别讨论;
  • 情况一分析:第一个元素大于最后一个元素,按二分查找思路,先找到中间元素。如果中间元素位于前面的递增子数组中,那么它应该大于第一个指针所指元素。此时数组中最小元素在该中间元素的后面。把第一个指针指向中间元素,缩小查找范围。移动后的第一个指针仍然位于递增子数组里;如果中间元素位于后面的递增子数组中,那么它应该小于第二个指针所指元素。此时数组中最小元素在该中间元素的前面。把第二个指针指向中间元素,缩小查找范围。移动后的第二个指针仍然位于递增子数组里。更新两个指针,重新做一轮查找。最后当两个指针指向两个相邻元素时,第二个指针指向的就是最小元素。
  • 情况二分析:很直观,不得不采用顺序查找。

具体代码如下:https://github.com/ZYFCodes/ProgrammingExercises/blob/master/src/main/java/org/zyf/programming/arrayexercises/MinNumberInRotateArray.java

public class Solution {/*** 功能描述:注意情况的划分* @author yanfengzhang* @date 2020-04-29 14:52* @param array* @return int*/public static int minNumberInRotateArray(int[] array) {if (null == array || array.length <= 0) {return 0;}/*标注最小值的下标*/int minIndex = 0;int index1 = 0;int index2 = array.length - 1;while (array[index1] >= array[index2]) {if (index2 - index1 == 1) {minIndex = index2;break;}if (array[index1] == array[index2]) {int base = array[index1];for (int i = index1 + 1; i < index2; i++) {if (base > array[i]) {minIndex = i;}break;}}int middleIndex = (index1 + index2) >> 1;if (array[middleIndex] > array[index1]) {index1 = middleIndex;minIndex = middleIndex;} else if (array[middleIndex] <= array[index2]) {index2 = middleIndex;minIndex = middleIndex;}}return array[minIndex];}
}

(四)和为S的两个数字

输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出任意一对。

如果先从数组中固定一个数字,再一次判断数组中其余n-1个数字与它是不是等于S,则算法复杂度为O(n2),通常不是我们的最优代码。

不妨在数组中先选择两个数逐步趋近S,用夹逼思想。设置两个指针分别指向数组第一个和最后一个,如果他们的和等于S,我们就找到了要找的两个数。如果小于S,考虑到可能有负数,希望两个数的和再大一些,考虑选择较小的数字后面的数字。当两个数字的和大于输入的数字的时候,选择较大数字后面的数字。

以在数组[1 2 4 7 11 15]中查找和为15的数对为例:

编写代码如下:https://github.com/ZYFCodes/ProgrammingExercises/blob/master/src/main/java/org/zyf/programming/arrayexercises/SumIsS.java

public class Solution {/*** 功能描述:在递增的数组中找到和为S的两个数的组合,输出任意一对** @param array 排序数组* @param sum   具体和* @return ArrayList<Integer>* @author yanfengzhang* @date 2020-04-29 15:05*/public static ArrayList<Integer> FindNumbersWithSumInSortedArray(int[] array, int sum) {ArrayList<Integer> result = new ArrayList<Integer>();if (null == array || array.length < 2) {return result;}int index1 = 0;int index2 = array.length - 1;while (index1 < index2) {if (array[index1] + array[index2] == sum) {result.add(array[index1]);result.add(array[index2]);return result;} else if (array[index1] + array[index2] > sum) {index2--;} else {index1++;}}return result;}
}

如果是无序的话,代码将变为:

public class Solution {/*** 功能描述:在数组中找到和为S的两个数的组合,输出任意一对** @param array 排序数组* @param sum   具体和* @return ArrayList<Integer>* @author yanfengzhang* @date 2020-04-29 15:05*/public static ArrayList<Integer> FindNumbersWithSumInNoSortedArray(int[] array, int sum) {ArrayList<Integer> result = new ArrayList<Integer>();if (null == array || array.length <= 0) {return result;}Map tempMap = new HashMap();for (int i = 0; i < array.length; i++) {tempMap.put(array[i], "");}for (int i = 0; i < array.length; i++) {int otherNumber = sum - array[i];if (tempMap.containsKey(otherNumber)) {result.add(array[i]);result.add(otherNumber);return result;}}return result;}
}

(五)和为S的连续正数序列

小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck!

考虑用两个数 small 和 big 分别表示序列的最小值和最大值。首先把 small 初始化为 1,big 初始化为 2。如果从 small 到 big 的序列的和大于等于 s,我们可以从序列中去掉较小的值,也就是增大 small 的值。

如果从 small 到 big 的序列的和小于 s,我们可以增大 big,让这个序列包含更多的数字。 因为这个序列至少要有两个数字,我们一直增加 small 到(1+s)/2 为止 。

以求取和为9的连续序列过程为例:

编写代码如下:https://github.com/ZYFCodes/ProgrammingExercises/blob/master/src/main/java/org/zyf/programming/mathproblemsexercises/ContinuousSumIsS.java

public class Solution {/*** 功能描述:考虑用两个数 small 和 big 分别表示序列的最小值和最大值。* @param sum 具体的和值* @return ArrayList<ArrayList < Integer>> 所有符合要求的值* @author yanfengzhang* @date 2020-04-29 16:14*/public static ArrayList<ArrayList<Integer>> findContinuousSequence(int sum) {ArrayList<ArrayList<Integer>> result = new ArrayList<ArrayList<Integer>>();int small = 1;int big = 2;int middle = (1 + sum) >> 1;int currentSum = small + big;while (small < middle) {if (currentSum > sum) {currentSum -= small++;} else if (currentSum < sum) {currentSum += big++;} else {ArrayList<Integer> list = new ArrayList<Integer>();for (int i = small; i <= big; i++) {list.add(i);}result.add(list);currentSum -= small++;}}return result;}
}

二、队与栈相关基本编程练习与思考

(一) 两个栈实现一个队列

第一个栈只负责添加元素,第二个栈在弹出元素时,首先判断当前栈是否为空,若为空就直接将其第一个栈中的数据全部压入第二个栈中,然后输出栈顶元素,即可实现队列效果;若第二个栈中有数据,添加直接将其数据压入第一个栈中,输出时直接输出第二个栈顶的元素即可!

代码如下:https://github.com/ZYFCodes/ProgrammingExercises/blob/master/src/main/java/org/zyf/programming/stackexercises/TwoStackImplQueue.java

public class TwoStackImplQueue {Stack<Integer> stack1 = new Stack<Integer>();Stack<Integer> stack2 = new Stack<Integer>();/*** 功能描述:队列入队操作** @param element 入队元数据* @author yanfengzhang* @date 2020-04-29 16:30*/public void offer(Integer element) {stack1.push(element);}/*** 功能描述:队列出队操作** @return Integer 返回 当前对头元素* @throws* @author yanfengzhang* @date 2020-04-29 16:37*/public Integer poll() throws Exception {if (stack2.isEmpty()) {while (!stack1.isEmpty()) {stack2.push(stack1.pop());}}if (stack2.isEmpty()) {throw new Exception("Queue is empty!");}return stack2.pop();}public static void main(String[] args) throws Exception {TwoStackImplQueue twoStackImplQueue = new TwoStackImplQueue();System.out.println("进行入队操作,入队元素依次为:1,3,5,4,2");twoStackImplQueue.offer(1);twoStackImplQueue.offer(3);twoStackImplQueue.offer(5);twoStackImplQueue.offer(4);twoStackImplQueue.offer(2);System.out.println("进行出队操作,出队元素为:" + twoStackImplQueue.poll());System.out.println("再次进行入队操作,入队元素为:7");twoStackImplQueue.offer(7);System.out.println("再次进行出队队操作,出队元素为:" + twoStackImplQueue.poll());}
}

(二)两个队列实现一个栈

两个队列添加元素,哪个队列为空,由于在输出元素时,要进行相应元素的移动(除去尾部元素),所以要在对应不为空的队列进行元素的添加;在输出数据时,要进行两个队列的变相操作,不为空的队列要依次向为空的队列中添加元素,直到尾元素输出即可!

代码如下:https://github.com/ZYFCodes/ProgrammingExercises/blob/master/src/main/java/org/zyf/programming/queueexercises/TwoQueueImplStack.java

public class TwoQueueImplStack {Queue<Integer> queue1 = new ArrayDeque<Integer>();Queue<Integer> queue2 = new ArrayDeque<Integer>();/*** 功能描述:栈的入栈操作** @param element 入栈元素* @author yanfengzhang* @date 2020-04-29 16:58*/public void push(Integer element) {/*两个队列都为空时,优先考虑 queue1*/if (queue1.isEmpty() && queue2.isEmpty()) {queue1.offer(element);return;}/*如果queue1为空,queue2有数据,直接放入queue2*/if (queue1.isEmpty()) {queue2.offer(element);return;}/*如果queue2为空,queue1有数据,直接放入queue1中*/if (queue2.isEmpty()) {queue1.offer(element);}}/*** 功能描述:栈的出栈操作** @return Integer 栈顶元素* @throws Exception 栈为空* @author yanfengzhang* @date 2020-04-29 17:06*/public Integer pop() throws Exception {/*如果两个栈都为空,则没有元素可以弹出,异常*/if (queue1.isEmpty() && queue2.isEmpty()) {throw new Exception("Stack is empty!");}/*如果queue1中没有元素,queue2中有元素,将其queue2中的元素依次放入queue1中,直到最后一个元素,弹出即可*/if (queue1.isEmpty()) {while (queue2.size() > 1) {queue1.add(queue2.poll());}return queue2.poll();}/*如果queue2中没有元素,queue1中有元素,将其queue1中的元素依次放入queue2中,直到最后一个元素,弹出即可*/if (queue2.isEmpty()) {while (queue1.size() > 1) {queue2.add(queue1.poll());}return queue1.poll();}return (Integer)null;}public static void main(String[] args) throws Exception {TwoQueueImplStack twoQueueImplStack = new TwoQueueImplStack();System.out.println("进行入栈操作,入栈元素依次为:2,4,7,5");twoQueueImplStack.push(2);twoQueueImplStack.push(4);twoQueueImplStack.push(7);twoQueueImplStack.push(5);System.out.println("进行出栈操作,出栈元素为:" + twoQueueImplStack.pop());System.out.println("再次进行入栈操作,入栈元素为:1");twoQueueImplStack.push(1);System.out.println("再次进行出栈操作,出栈元素为:" + twoQueueImplStack.pop());}
}

(三)包含min函数的栈

理解得到栈最小元素的min函数这个功能,言外之意要求我们对当前栈弹出的应该是当前栈内的最小元素,而栈又是“先进后出”的数据结构,所以要确保通过每次压入比较得到最小元素,并将最小元素放到栈顶保证弹出时正好是当前栈的最小值,但这样就打破了栈的定义,所以我们需要添加一个成员变量来存放最小值同时需要新建一个辅助栈来实现每次弹出都保证弹出最小值。画个表分析如下:

这样一来如果每次都把最小元素压入辅助栈,那么就能保证辅助栈的栈顶一直是最小值。当最小元素从数据栈内被弹出之后,同时弹出辅助栈的栈顶元素,此时辅助栈的新栈顶元素就是下一个最小值。从而编写代码如下:https://github.com/ZYFCodes/ProgrammingExercises/blob/master/src/main/java/org/zyf/programming/stackexercises/HaveMinElementStack.java

public class Solution {Stack<Integer> dataStack = new Stack<Integer>();Stack<Integer> minStack = new Stack<Integer>();/*** 功能描述:栈的入栈操作** @param element 入栈元素* @author yanfengzhang* @date 2020-04-29 17:30*/public void push(Integer element) {dataStack.push(element);if (minStack.isEmpty()) {minStack.push(element);} else if (element < minStack.peek()) {minStack.push(element);}}/*** 功能描述:栈的出栈操作** @return Integer 出栈元素* @author yanfengzhang* @date 2020-04-29 17:33*/public Integer pop() {if (dataStack.peek().equals(minStack.peek())) {minStack.pop();}return dataStack.pop();}/*** 功能描述:栈的最小元素** @return Integer 栈最小元素* @author yanfengzhang* @date 2020-04-29 17:35*/public Integer min() {return minStack.peek();}public static void main(String[] args) {HaveMinElementStack haveMinElementStack = new HaveMinElementStack();System.out.println("进行入栈操作,入栈元素依次为:2,4,7,5, 1, 4, 5, 7, 9, 0, 5");haveMinElementStack.push(2);haveMinElementStack.push(4);haveMinElementStack.push(7);haveMinElementStack.push(5);haveMinElementStack.push(1);haveMinElementStack.push(4);haveMinElementStack.push(5);haveMinElementStack.push(7);haveMinElementStack.push(9);haveMinElementStack.push(0);haveMinElementStack.push(5);System.out.println("当前栈中最小元素为:" + haveMinElementStack.min());System.out.println("进行出栈操作,出栈元素为:" + haveMinElementStack.pop());System.out.println("再次进行入栈操作,入栈元素为:7");haveMinElementStack.push(7);System.out.println("再次进行出栈操作,出栈元素为:" + haveMinElementStack.pop());System.out.println("当前栈中最小元素为:" + haveMinElementStack.min());System.out.println("再次进行出栈操作,出栈元素为:" + haveMinElementStack.pop());System.out.println("当前栈中最小元素为:" + haveMinElementStack.min());System.out.println("再次进行出栈操作,出栈元素为:" + haveMinElementStack.pop());System.out.println("当前栈中最小元素为:" + haveMinElementStack.min());System.out.println("再次进行出栈操作,出栈元素为:" + haveMinElementStack.pop());System.out.println("当前栈中最小元素为:" + haveMinElementStack.min());System.out.println("再次进行出栈操作,出栈元素为:" + haveMinElementStack.pop());System.out.println("当前栈中最小元素为:" + haveMinElementStack.min());System.out.println("再次进行出栈操作,出栈元素为:" + haveMinElementStack.pop());System.out.println("当前栈中最小元素为:" + haveMinElementStack.min());System.out.println("再次进行出栈操作,出栈元素为:" + haveMinElementStack.pop());System.out.println("当前栈中最小元素为:" + haveMinElementStack.min());System.out.println("再次进行出栈操作,出栈元素为:" + haveMinElementStack.pop());System.out.println("当前栈中最小元素为:" + haveMinElementStack.min());}
}

(四)判断栈的压入对应弹出序列是否正确

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。不妨直接根据举例看能不能分析下它的规律,就以上述两种情况分析,如下表:

总结:判断一个序列是不是栈的弹出序列的规律:

  • 如果下一个弹出的数字刚好为栈顶元素,那么直接弹出;
  • 如果下一个弹出的数字不在栈顶,,我们把压栈序列中还没有入栈的数字压入到一个辅助栈,直到把下一个需要弹出的数字压入栈顶为止;
  • 如果所有的数字都压入栈了,仍然没有找到下一个弹出的数字,那么该序列不可能是一个弹出序列。

根据规律编写代码如下:借用一个辅助的栈,遍历压栈顺序,先讲第一个放入栈中,这里是1,然后判断栈顶元素是不是出栈顺序的第一个元素,这里是4,很显然1≠4,所以我们继续压栈,直到相等以后开始出栈,出栈一个元素,则将出栈顺序向后移动一位,直到不相等,这样循环等压栈顺序遍历完成,如果辅助栈还不为空,说明弹出序列不是该栈的弹出顺序。

具体代码见:https://github.com/ZYFCodes/ProgrammingExercises/blob/master/src/main/java/org/zyf/programming/stackexercises/IsPopOrderOfStack.java

举例:
入栈1,2,3,4,5
出栈4,5,3,2,1
首先1入辅助栈,此时栈顶1≠4,继续入栈2
此时栈顶2≠4,继续入栈3
此时栈顶3≠4,继续入栈4
此时栈顶4=4,出栈4,弹出序列向后一位,此时为5,,辅助栈里面是1,2,3
此时栈顶3≠5,继续入栈5
此时栈顶5=5,出栈5,弹出序列向后一位,此时为3,,辅助栈里面是1,2,3
….
依次执行,最后辅助栈为空。如果不为空说明弹出序列不是该栈的弹出顺序。
*/
import java.util.ArrayList;
import java.util.Stack;
public class Solution {/*** 功能描述:判断栈的压入对应弹出序列是否正确** @param pushArray 入栈顺序* @param popArray  出栈顺序* @return boolean 判断栈的压入对应弹出序列是否正确* @author yanfengzhang* @date 2020-04-29 18:55*/public static boolean isPopOrderOfStack(int[] pushArray, int[] popArray) {if (pushArray == null || popArray == null) {return false;}Stack<Integer> stack = new Stack<Integer>();/*用于标识弹出序列的位置*/int popIndex = 0;for (int i = 0; i < pushArray.length; i++) {stack.push(pushArray[i]);while (!stack.isEmpty() && stack.peek() == popArray[popIndex]) {stack.pop();popIndex++;}}return stack.isEmpty();}public static void main(String[] args) {int[] pushArray = {1, 2, 3, 4, 5};int[] popArray1 = {4, 5, 3, 2, 1};int[] popArray2 = {4, 3, 5, 1, 2};System.out.println("当前入栈顺序为12345,出栈顺序为45321,栈的压入对应弹出序列是否正确:" + isPopOrderOfStack(pushArray, popArray1));System.out.println("当前入栈顺序为12345,出栈顺序为43512,栈的压入对应弹出序列是否正确:" + isPopOrderOfStack(pushArray, popArray2));}
}

三、链表相关基本编程练习与思考

(一)反转链表

输入一个链表,反转链表后,输出链表的所有元素。

具体代码见:https://github.com/ZYFCodes/ProgrammingExercises/blob/master/src/main/java/org/zyf/programming/listexercises/ReverseList.java

/*
public class ListNode {int value;ListNode next = null;ListNode(int value) {this.value = value;}
}*/
public class Solution {/*** 功能描述:将链表进行反转* @author yanfengzhang* @date 2020-04-29 22:39* @param head 当前链表* @return ListNode 反转以后的链表*/public ListNode reverseList(ListNode head) {/*head为当前节点,如果当前节点为空的话,那就什么也不做,直接返回null;*/if (null == head) {return null;}/*当前节点是head,pre为当前节点的前一节点,next为当前节点的下一节点需要pre和next的目的是让当前节点从pre->head->next1->next2变成pre<-head next1->next2即pre让节点可以反转所指方向,但反转之后如果不用next节点保存next1节点的话,此单链表就此断开了。所以需要用到pre和next两个节点1->2->3->4->5  //1<-2<-3 4->5*/ListNode pre = null;ListNode next = null;while (head != null) {/*做循环,如果当前节点不为空的话,始终执行此循环,此循环的目的就是让当前节点从指向next到指向pre如此就可以做到反转链表的效果先用next保存head的下一个节点的信息,保证单链表不会因为失去head节点的原next节点而就此断裂*/next = head.next;/*保存完next,就可以让head从指向next变成指向pre了*/head.next = pre;/*head指向pre后,就继续依次反转下一个节点让pre,head,next依次向后移动一个节点,继续下一次的指针反转*/pre = head;head = next;}return pre;}public String printListNode(ListNode head) {StringBuilder stringBuilder = new StringBuilder();while (head != null) {stringBuilder.append(head.value);head = head.next;}return stringBuilder.toString();}public static void main(String[] args) {ReverseList reverseList = new ReverseList();ListNode listNode1 = new ListNode(1);ListNode listNode2 = new ListNode(2);ListNode listNode3 = new ListNode(3);ListNode listNode4 = new ListNode(4);ListNode listNode5 = new ListNode(5);listNode1.next = listNode2;listNode2.next = listNode3;listNode3.next = listNode4;listNode4.next = listNode5;System.out.println("原链表为:" + reverseList.printListNode(listNode1));System.out.println("反转链表为:" + reverseList.printListNode(reverseList.reverseList(listNode1)));}
}

(二)链表中倒数第k个结点

输入一个链表(单链表),输出该链表中倒数第k个结点。为了符合大多数人的习惯,本题从1开始计算,即链表的尾结点是倒数第一个结点。例如一个链表有6个结点,从头结点开始他们的值依次是1、2、3、4、5、6。这个链表的倒数第3个结点是值为4的结点。

我们先考虑边界问题和可能有哪些问题:①如果整个链表是空链表,头指针为空,这个时候我们求解的问题无意义,但这个问题有可能存在,所以此时返回null;②在考虑输入的k值,当k值小于等于0时,同样求解无意直接返回null;③输入的k值也有可能比我们的链表长度要大很多,如果不想通过全部遍历在遍历的方式进行就必须在通过一定的判断条件来分析了。

现在不考虑边界条件,正常思路分析,输出链表中倒数第k个结点,可能直接想到的就是先遍历链表走到链表的尾端在回溯k步,但链表定义为单链表,并没有返回指针,这条思路行不通。那就两次遍历,第一次统计链表结点的个数,第二次就能找到倒数第k个结点。考虑能不能优化,即只需要一次遍历就能输出倒数第k个结点。引入指针,和我们之前做的大多数题一样,利用指针将问题简化。

定义两个指针,先指针初始化,然后让第一个先指向遍历的第k-1个,接着第二个指针再次初始化的时候就指头指针,然后两个指针同时移动,直到第一个指针指向尾结点,此时第二个指向的就是倒数第k个结点,返回该结点。

综合考虑,将代码编写如下:https://github.com/ZYFCodes/ProgrammingExercises/blob/master/src/main/java/org/zyf/programming/listexercises/FindKthToTailInListNode.java

/*
public class ListNode {int value;ListNode next = null;ListNode(int value) {this.value = value;}
}*/
public class Solution {/*** 功能描述:输入一个链表(单链表),输出该链表中倒数第k个结点。** @param head 当前链表* @param k    倒数第k个* @return ListNode 倒数第k个节点信息* @author yanfengzhang* @date 2020-04-29 23:15*/public ListNode findKthToTail(ListNode head, int k) {if (head == null || k <= 0) {return null;}ListNode index1 = head;ListNode index2 = head;while (k != 1) {if (index1.next != null) {index1 = index1.next;k--;} else {return null;}}while (index1.next != null) {index1 = index1.next;index2 = index2.next;}return index2;}public static void main(String[] args) {FindKthToTailInListNode findKthToTailInListNode = new FindKthToTailInListNode();ListNode listNode1 = new ListNode(1);ListNode listNode2 = new ListNode(2);ListNode listNode3 = new ListNode(3);ListNode listNode4 = new ListNode(4);ListNode listNode5 = new ListNode(5);ListNode listNode6 = new ListNode(6);ListNode listNode7 = new ListNode(7);ListNode listNode8 = new ListNode(8);ListNode listNode9 = new ListNode(9);listNode1.next = listNode2;listNode2.next = listNode3;listNode3.next = listNode4;listNode4.next = listNode5;listNode5.next = listNode6;listNode6.next = listNode7;listNode7.next = listNode8;listNode8.next = listNode9;System.out.println("当前链表情况为:" + findKthToTailInListNode.printListNode(listNode1));System.out.println("当前链表的倒数第4个节点信息为:" + findKthToTailInListNode.findKthToTail(listNode1, 4).value);//System.out.println("当前链表的中间节点信息为:" + findKthToTailInListNode.findMiddleNode(listNode1).value);}
}

相关问题:

  • 求链表的中间结点。如果中间结点总数为奇数则返回中间结点,如果是偶数,返回中间两个结点的任意两个。----解决思路:定义两个指针,同时从链表头出发,一个指针一次走一步,另一个指针一次走两步。当走的快的指针走到链表末尾时,走的慢的指针正好在链表的中间。见https://github.com/ZYFCodes/ProgrammingExercises/blob/master/src/main/java/org/zyf/programming/listexercises/FindKthToTailInListNode.java
  • 判断一个单向链表是否形成了环形结构。----解决思路:定义两个指针,同时从链表头出发,一个指针一次走一步,另一个指针一次走两步。当走的快的指针追上了走得慢的指针时则链表为环形结构;当走的快的指针到了链表的末尾都没追上第一个指针,那么链表就不是环形的。见https://github.com/ZYFCodes/ProgrammingExercises/blob/master/src/main/java/org/zyf/programming/listexercises/CycleInListNode.java

(三)两个链表的第一个公共结点

输入两个链表(单链表),找出它们的第一个公共结点。

看到该题,起码应该有三种思路来分析:(实际中,还是追求效率!)

  • 思路一:蛮力法。在第一个链表上顺序遍历每个结点,每遍历到一个结点的时候,在第二个链表上顺序遍历每个结点。如果在第二个链表上有一个结点和第一个链表上的结点一样,说明两个链表在这个结点上重合,于是找到了他们的公共结点。但时间复杂度为O(mn)。(m为第一个链表的长度,n为第二个链表的长度)
  • 思路二:进一步思考,如果两个链表有公共的结点,那么这两个链表从某一结点开始,他们的next指针都指向同一个结点。由于是单链表的结点,每个结点只有一个next指针,因此从第一个公共结点开始,之后他们的结点都是重合的,不可能再出现分叉。也就说,如果两个链表有公共结点,那么公共结点出现在链表的尾部。如果我们从两个链表的尾部开始往前比较,最后一个相同的结点就是我们要找的结点。我们分别把两个链表放入两个栈里,这样两个链表的尾结点就位于两个栈的栈顶,接下来比较两个栈顶元素的结点是否相同。如果相同,则把栈顶元素弹出接着比较下一个栈顶,直到找到最后一个相同的结点。但是我们加入了空间复杂度O(m+n),通过空间换时间,此时我们的时间复杂度也由O(mn)降到了O(m+n)。
  • 思路三:思路二中之所以需要用到栈,是因为我们想同时遍历到达两个栈的尾结点。当两个链表的长度不相同时,如果我们从头开始遍历到达尾结点的时间就不一致。所以可以换一种思路不用辅助栈。首先遍历两个链表得到他们的长度,就能知道那个链表长,以及长的链表比短的链表多几个结点。在第二次遍历的时候,在较长的链表上先走若干步,接着同时在两个链表上遍历,找到的第一个相同的结点就是他们的第一个公共结点。

综上第三种方法效率最高,故编写如下:https://github.com/ZYFCodes/ProgrammingExercises/blob/master/src/main/java/org/zyf/programming/listexercises/CommonNodeInListNode.java

/*
public class ListNode {int value;ListNode next = null;ListNode(int value) {this.value = value;}
}*/
public class Solution {/*** 功能描述:找出两个链表(单链表)第一个公共结点** @param pHead1 当前链表1* @param pHead2 当前链表2* @return ListNode 第一个公共结点* @author yanfengzhang* @date 2020-04-30 11:12*/public ListNode findFirstCommonNode(ListNode pHead1, ListNode pHead2) {if (pHead1 == null || pHead2 == null) {return null;}int length1 = getLengthOfListNode(pHead1);int length2 = getLengthOfListNode(pHead2);int different = 0;if (length1 >= length2) {/*如果链表1的长度大于链表2的长度,先遍历链表1,遍历的长度就是两链表的长度差*/different = length1 - length2;while (different != 0) {pHead1 = pHead1.next;different--;}} else {/*如果链表2的长度大于链表1的长度,先遍历链表1,遍历的长度就是两链表的长度差*/different = length2 - length1;while (different != 0) {pHead2 = pHead2.next;different--;}}while (pHead1 != pHead2) {pHead1 = pHead1.next;pHead2 = pHead2.next;}return pHead1;}/*** 功能描述:获取链表长度** @param head 当前链表* @return int 链表长度* @author yanfengzhang* @date 2020-04-30 11:11*/public int getLengthOfListNode(ListNode head) {int length = 0;if (head == null) {return length;}while (head.next != null) {head = head.next;length++;}return length + 1;}/*** 功能描述:打印当前链表** @param head 当前链表* @return String 返回链表信息* @author yanfengzhang* @date 2020-04-29 23:09*/public String printListNode(ListNode head) {StringBuilder stringBuilder = new StringBuilder();while (head != null) {stringBuilder.append(head.value);head = head.next;}return stringBuilder.toString();}public static void main(String[] args) {CommonNodeInListNode commonNodeInListNode = new CommonNodeInListNode();/*链表1*/ListNode listNode1 = new ListNode(1);ListNode listNode2 = new ListNode(2);ListNode listNode3 = new ListNode(3);ListNode listNode4 = new ListNode(4);ListNode listNode5 = new ListNode(5);ListNode listNode6 = new ListNode(6);ListNode listNode7 = new ListNode(7);ListNode listNode8 = new ListNode(8);ListNode listNode9 = new ListNode(9);listNode1.next = listNode2;listNode2.next = listNode3;listNode3.next = listNode4;listNode4.next = listNode5;listNode5.next = listNode6;listNode6.next = listNode7;listNode7.next = listNode8;listNode8.next = listNode9;/*链表2*/ListNode listNode11 = new ListNode(6);ListNode listNode21 = new ListNode(0);ListNode listNode31 = new ListNode(7);listNode11.next = listNode21;listNode21.next = listNode31;listNode31.next = listNode5;System.out.println("当前链表1的相关信息为:" + commonNodeInListNode.printListNode(listNode1));System.out.println("当前链表2的相关信息为:" + commonNodeInListNode.printListNode(listNode11));System.out.println("两个链表的第一个公共节点为:" + commonNodeInListNode.findFirstCommonNode(listNode1, listNode11).value);}
}

当然,可以利用运用HasnMap的特性,编写如下:

public class Solution {public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {ListNode current1 = pHead1;ListNode current2 = pHead2;HashMap<ListNode, Integer> hashMap = new HashMap<ListNode, Integer>();while (current1 != null) {hashMap.put(current1, null);current1 = current1.next;}while (current2 != null) {if (hashMap.containsKey(current2))return current2;current2 = current2.next;}return null;}
}

(四)合并两个排序的链表

输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。例如如图要求将链表1与链表2合并后依旧为升序链表如链表3。

同样先考虑边界条件,两个链表万一有空链表就会有空指针,而每当代码试图访问空指针指向的内存时程序就会崩溃,这就会带来鲁棒性的问题。考虑空链表时,显然就这三种情况:1链表1是空链表(它的头结点是一个空指针),与链表2合并,结果就是链表2;2链表2是空链表(它的头结点是一个空指针),与链表1合并,结果就是链表1;3链表1和链表2都是空链表,合并的结果为一个空链表。

现在分析若两个链表不为空链表,则合并他们的时候我们怎样做比较合适?我们可以从两个链表的头结点开始,当链表1头结点的值小于链表2的头结点的值,就让链表1的头结点为合并后的链表3的头结点。继续按这个方式合并剩余的结点,当我们得到两个链表中值较小的头结点并把它链接到已经合并的链表之后,两个链表剩余的结点依旧是排序的所以合并的步骤和之前是一样的,这显然就是一个典型的递归过程。过程如图所示:

代码如下:https://github.com/ZYFCodes/ProgrammingExercises/blob/master/src/main/java/org/zyf/programming/listexercises/MergeTwoSortedLists.java

/*
public class ListNode {int value;ListNode next = null;ListNode(int value) {this.value = value;}
}*/
public class Solution {/*** 功能描述:两个有序链表合成后的新有序链表** @param l1 当前链表1* @param l2 当前链表2* @return ListNode 合成后的新有序链表* @author yanfengzhang* @date 2020-04-30 11:42*/public ListNode mergeTwoLists(ListNode l1, ListNode l2) {if (l1 == null || l2 == null) {return null;}ListNode head = new ListNode(0);ListNode mergedListNode = head;while (l1 != null && l2 != null) {if (l1.value > l2.value) {mergedListNode.next = l2;l2 = l2.next;} else {mergedListNode.next = l1;l1 = l1.next;}mergedListNode = mergedListNode.next;}mergedListNode.next = (l1 == null ? l2 : l1);return head.next;}/*** 功能描述:打印当前链表** @param head 当前链表* @return String 返回链表信息* @author yanfengzhang* @date 2020-04-29 23:09*/public String printListNode(ListNode head) {StringBuilder stringBuilder = new StringBuilder();while (head != null) {stringBuilder.append(head.value);head = head.next;}return stringBuilder.toString();}public static void main(String[] args) {MergeTwoSortedLists mergeTwoSortedLists = new MergeTwoSortedLists();/*链表1*/ListNode listNode1 = new ListNode(1);ListNode listNode2 = new ListNode(2);ListNode listNode3 = new ListNode(3);listNode1.next = listNode2;listNode2.next = listNode3;/*链表2*/ListNode listNode11 = new ListNode(7);ListNode listNode21 = new ListNode(8);ListNode listNode31 = new ListNode(9);listNode11.next = listNode21;listNode21.next = listNode31;System.out.println("当前链表1的相关信息为:" + mergeTwoSortedLists.printListNode(listNode1));System.out.println("当前链表2的相关信息为:" + mergeTwoSortedLists.printListNode(listNode11));ListNode mergedListNode = mergeTwoSortedLists.mergeTwoLists(listNode1, listNode11);System.out.println("两个链表合并后的新链表为:" + mergeTwoSortedLists.printListNode(mergedListNode));}
}

四、二叉树相关基本编程练习与思考

(一)二叉树的深度

输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。

换简单思路想问题时会减轻代码量,用这个角度来理解树的深度:

  • 如果一棵树只有一个结点,它的深度为1;
  • 如果根结点只有左子树而没有右子树,那么树的深度应该是左子树的深度加1;
  • 如果根结点只有右子树而没有左子树,那么树的深度应该是右子树的深度加1;如果既有右子树又有左子树,那么该树的深度是左、右子树深度较大值加1。

按照此思路简单编码如下:

https://github.com/ZYFCodes/ProgrammingExercises/blob/master/src/main/java/org/zyf/programming/treeexercises/TreeDepth.java

/*
public class TreeNode {int val = 0;TreeNode left = null;TreeNode right = null;public TreeNode(int val) {this.val = val;}
};*/
public class Solution {/*** 功能描述:求该树的深度* @author yanfengzhang* @date 2020-04-30 13:50* @param pRoot 当前树信息* @return int 树的深度*/public int getTreeDepth(TreeNode pRoot) {if (pRoot == null) {return 0;}int left = getTreeDepth(pRoot.left);int right = getTreeDepth(pRoot.right);return left > right ? left + 1 : right + 1;}public static void main(String[] args) {TreeDepth treeDepth = new TreeDepth();LinkedList<Integer> inputList = new LinkedList<Integer>(Arrays.asList(3, 2, 9, null, null, 10, null, null, 8, null, 4));TreeNode pRoot = createBinaryTree(inputList);System.out.println("当前树的深度为:" + treeDepth.getTreeDepth(pRoot));}/*** 功能描述:根据链表构建一颗二叉树** @param inputList 输入具体的二叉树信息* @return TreeNode 具体的二叉树* @author yanfengzhang* @date 2020-04-30 12:11*/public static TreeNode createBinaryTree(LinkedList<Integer> inputList) {if (inputList == null || inputList.isEmpty()) {return null;}TreeNode node = null;Integer data = inputList.removeFirst();if (data != null) {node = new TreeNode(data);node.left = createBinaryTree(inputList);node.right = createBinaryTree(inputList);}return node;}
}

(二)判断平衡二叉树

输入一棵二叉树,判断该二叉树是否是平衡二叉树。如果某二叉树中任意结点的左右子树的深度相差不超过1,那么它就是一颗平衡二叉树。

判断一颗二叉树是否是平衡二叉树,首先得得到任意结点的左右子树的深度,当两深度相差不超过1时,我们才能判断它是否为平衡二叉树。

现在先处理如何有效计算树的深度。换一个采用递归思想的简单思路:如果一棵树只有一个结点,它的深度为1;如果根结点只有左子树而没有右子树,那么树的深度应该是左子树的深度加1;如果根结点只有右子树而没有左子树,那么树的深度应该是右子树的深度加1;如果既有右子树又有左子树,那么该树的深度是左、右子树深度较大值加1。按照此思路简单编码如下:

    /*** 功能描述:求该树的深度* @author yanfengzhang* @date 2020-04-30 13:50* @param pRoot 当前树信息* @return int 树的深度*/public int getTreeDepth(TreeNode pRoot) {if (pRoot == null) {return 0;}int left = getTreeDepth(pRoot.left);int right = getTreeDepth(pRoot.right);return left > right ? left + 1 : right + 1;}

按照直接定义,我们可以在遍历树的每一个结点的时候,调用函数TreeDepth得到它的左右子树的深度。如果它的左右子树的深度相差不超过1,那么它就是一颗平衡二叉树。这样编码如下:https://github.com/ZYFCodes/ProgrammingExercises/blob/master/src/main/java/org/zyf/programming/treeexercises/IsBalancedTree.java

public class Solution {/*** 功能描述:判断该二叉树是否是平衡二叉树** @param root 当前树信息* @return boolean 判断该二叉树是否是平衡二叉树* @author yanfengzhang* @date 2020-04-30 14:04*/public boolean isBalanced(TreeNode root) {if (root == null) {return false;}int leftDepth = getTreeDepth(root.left);int rightDepyh = getTreeDepth(root.right);int difference = leftDepth - rightDepyh;if (difference > 1 || difference < -1) {return false;}return true;}/*** 功能描述:求该树的深度** @param root 当前树信息* @return int 树的深度* @author yanfengzhang* @date 2020-04-30 13:50*/public int getTreeDepth(TreeNode root) {if (root == null) {return 0;}int left = getTreeDepth(root.left);int right = getTreeDepth(root.right);return left > right ? left + 1 : right + 1;}/*** 功能描述:根据链表构建一颗二叉树** @param inputList 输入具体的二叉树信息* @return TreeNode 具体的二叉树* @author yanfengzhang* @date 2020-04-30 12:11*/public static TreeNode createBinaryTree(LinkedList<Integer> inputList) {if (inputList == null || inputList.isEmpty()) {return null;}TreeNode node = null;Integer data = inputList.removeFirst();if (data != null) {node = new TreeNode(data);node.left = createBinaryTree(inputList);node.right = createBinaryTree(inputList);}return node;}public static void main(String[] args) {IsBalancedTree isBalancedTree = new IsBalancedTree();LinkedList<Integer> inputList = new LinkedList<Integer>(Arrays.asList(3, 2, 9, null, null, 10, null, null, 8, null, 4));TreeNode pRoot = createBinaryTree(inputList);System.out.println("当前树:判断该二叉树是否是平衡二叉树:" + isBalancedTree.isBalanced(pRoot));}
}

但是我们又发现一个问题,就是每个结点会被重复遍历多遍,这种思路的效率不是很高,重复遍历同一个结点会影响性能。考虑不需要重复遍历的算法。

如果我们用后序遍历的方式遍历二叉树的每一个结点,在遍历到一个结点之前就已经遍历了它的左右子树,只要在遍历每个结点的时候记录它的深度(某一结点的深度等于它到叶结点的路径长度),我们就可以一边遍历一边判断每个结点是不是平衡的。这样一来,优化代码如下:

public class Solution {
//后续遍历时,遍历到一个节点,其左右子树已经遍历  依次自底向上判断,每个节点只需要遍历一次private boolean isBalanced=true;public boolean IsBalanced_Solution(TreeNode root) {getDepth(root);return isBalanced;}public int getDepth(TreeNode root){if(root==null)return 0;int left=getDepth(root.left);int right=getDepth(root.right);if(Math.abs(left-right)>1){isBalanced=false;}return right>left ?right+1:left+1;  }
}

五、其他思维算法题的小总结

(一)回文串

给定一个字符串,问是否能通过添加一个字母将其变为回文串。

输入:coco     返回:true

相关代码见:(以及判断一个字符串是否为回文串)https://github.com/ZYFCodes/ProgrammingExercises/blob/master/src/main/java/org/zyf/programming/stringexercises/PharseString.java

public class PharseString {/*** 功能描述:给定一个字符串,问是否能通过添加一个字母将其变为回文串。** @param str 具体字符串* @return boolean 是否能通过添加一个字母将其变为回文串* @author yanfengzhang* @date 2020-04-30 14:24*/public boolean isPharseStringWithChar(String str) {if (null == str) {return false;}if (str.length() == 1 || str.length() == 2) {return true;}for (int i = 1; i < str.length() >> 1; i++) {if (str.toCharArray()[i] != str.toCharArray()[str.length() - i]) {return false;}}return true;}/*** 功能描述:给定一个字符串,问是否为回文串。** @param str 具体字符串* @return boolean 是否回文串* @author yanfengzhang* @date 2020-04-30 15:22*/public boolean isPharseString(String str) {if (null == str) {return false;}if (str.length() == 1) {return true;}Stack<Character> stack = new Stack<Character>();char[] strArray = str.toCharArray();for (int i = 0; i < str.length(); i++) {stack.push(strArray[i]);}for (int i = 0; i < str.length(); i++) {if (stack.pop() != strArray[i]) {return false;}}return true;}public static void main(String[] args) {PharseString pharseString = new PharseString();String str1 = "coco";String str2 = "4h3211123h4";System.out.println("当前字符串:" + str1 + " 通过添加一个字母将其变为回文串的可能为:" + pharseString.isPharseStringWithChar(str1));System.out.println("当前字符串:" + str2 + " 通过添加一个字母将其变为回文串的可能为:" + pharseString.isPharseString(str2));}
}

(二)字符串全排列

public class Test {public static void main(String[] args) {String str = "abc";allArrange(str.toCharArray(), 0);}public static void allArrange(char[] chars, int n) {if (n == chars.length - 1) {System.out.println(String.valueOf(chars));}for (int i = n; i < chars.length; i++) {char tmp = chars[i];chars[i] = chars[n];chars[n] = tmp;allArrange(chars, n + 1);}}
}

(三)字符串转数字

public class Test {public static void main(String[] args) {String str = "123";System.out.print(strToInt(str));}public static int strToInt(String str) {int result = 0; // 空字符串返回0if (str.length() == 0) {return result;}char[] chars = str.toCharArray();char base = '0';for (int i = chars.length; i > 0; i--) {result += (chars[chars.length - i] - base) * Math.pow(10, i - 1);}return result;}
}

(四)最大公约数和最小公倍数

public class MaxCommonDivisorAndMinCommonMultiple {/*** 功能描述:最大公约数** @param m n 具体常数* @return int 最大公约数* @author yanfengzhang* @date 2020-04-30 16:57*/public static int maxCommonDivisor(int m, int n) {if (m == n) {return m;}if ((m & 1) == 0 && (n & 1) == 0) {return maxCommonDivisor(m >> 1, n >> 1) << 1;} else if ((m & 1) == 0 && (n & 1) != 0) {return maxCommonDivisor(m >> 1, n);} else if ((m & 1) != 0 && (n & 1) == 0) {return maxCommonDivisor(m, n >> 1);} else {int big = m > n ? m : n;int small = m < n ? m : n;return maxCommonDivisor(big - small, small);}}/*** 功能描述:最小公倍数** @param m n 具体常数* @return int 最小公倍数* @author yanfengzhang* @date 2020-04-30 16:58*/public static int minCommonMultiple(int m, int n) {return m * n / maxCommonDivisor(m, n);}public static void main(String[] args) {int m = 8;int n = 12;System.out.println("当前有两个整数,分别为8和12。");System.out.println("这两个数的最大公约数为" + maxCommonDivisor(m, n));System.out.println("这两个数的最小公倍数为" + minCommonMultiple(m, n));}
}

(五)最长公共字串

求两个字符串的最长公共子串,如“abcdefg”和“adefgwgeweg”的最长公共子串为“defg”(子串必须是连续的)

public class MaxSubString {/*** 功能描述:两个字符串的最长公共子串** @param str1 字符串1* @param str2 字符串2* @return List<String> 最长公共子串* @author yanfengzhang* @date 2020-04-30 17:11*/public static List<String> getMaxSubString(String str1, String str2) {if (StringUtils.isEmpty(str1) || StringUtils.isEmpty(str2)) {return null;}String max;String min;if (str1.length() > str2.length()) {max = str1;min = str2;} else {max = str2;min = str1;}List<String> subStrings = Lists.newArrayList();String maxSubString = "";for (int i = 0; i < min.length(); i++) {for (int begin = 0, end = min.length() - i; begin < end; begin++) {String tmp = min.substring(begin, end);if (max.contains(tmp) && tmp.length() >= maxSubString.length()) {maxSubString = tmp;subStrings.add(maxSubString);}}}return subStrings;}public static void main(String[] args) {String str1 = "abcdefg";String str2 = "adefgwgeweg";System.out.println("当前存在两个字符串分别为:abcdefg,adefgwgeweg");System.out.println("两个字符串的最长公共子串为:" + getMaxSubString(str1, str2));}
}

(六)字典序列化

我们程序中用到了一个数组 a ,数组的每个元素都是一个字典(map/dict)。

字典的 key/value 都是字符串,字符串中可包含任意字符。

示例:

a[0]["k1"] = "v1"

a[0]["k2"] = "v2"

a[1]["A"] = "XXX"

...

实际使用过程中,我们自定义了一个基于字符串的存储结构,数组元素之间用“换行”分割,

字典元素之间使用“分号”分割, key/value 之间用“等号”分割。

上述数据序列化之后,应该得到一个字符串:

"k1=v1;k2=v2\nA=XXX"

请实现一个“保存”函数、一个“加载”函数。

text = store(a); //把数组保存到一个字符串中

a = load(text);  //把字符串中的内容读取为字典数组

请考虑所有边界情况,不要出现bug。在满足上述需求的前提下,可自行增加一些规则和约定。

// 约定:每个字典的key/value不能为 换行、分割、等号
public class Test {public static void main(String[] args) {
//        List<Map<String, String>> mapList = Lists.newArrayList();
//        Map<String, String> map1 = Maps.newHashMap();
//        map1.put("k1", "v1");
//        map1.put("k2", "v2");
//        mapList.add(map1);
//        Map<String, String> map2 = Maps.newHashMap();
//        map2.put("A", "XXX");
//        map2.put("B", " ");
//        map2.put(" ", "C");
//        map2.put(" ", " ");
//        mapList.add(map2);
//        Object[] strings = mapList.toArray();
//        String result = store(strings);
//        System.out.println(result);String str = "k1=v1;?=、";Object[] objects = load(str);System.out.println(objects);}private static String store(Object[] strings) {StringBuilder str = new StringBuilder();for (int i = 0; i < strings.length; i++) {Map<String, String> map = (Map<String, String>) strings[i];int j = 0;for (String key : map.keySet()) {str.append(key).append("=").append(map.get(key));if (j < map.size() - 1) {str.append(";");}j++;}if (i < strings.length - 1) {str.append("\\n");}}return str.toString();}private static Object[] load(String str) {List<Map<String, String>> mapList = Lists.newLinkedList();String[] maps = str.split("\n");for (int i = 0; i < maps.length; i++) {Map map = Maps.newLinkedHashMap();String[] keyValues = maps[i].split(";");for (int j = 0; j < keyValues.length; j++) {String[] keyValue = keyValues[j].split("=");if (keyValue.length == 2) {map.put(keyValue[0], keyValue[1]);}}mapList.add(map);}return mapList.toArray();}
}

(七)路径规划

我们有一个有向无环图,权重在节点上。

需求:从一个起点开始,找到一条节点权重之和最大的最优路径。

输入: n个节点,m个路径,起点

输出: 最优路径的权重值之和

举例:

3个节点与权重: A=1, B=2, C=2

3条路径: A->B, B->C, A->C

起点: A

输出: 5 (最优路径是 A->B->C , 权重之和是 1+2+2=5)

请考虑算法效率优化,考虑异常情况(比如输入的图有环路)要避免死循环或者崩溃。

public class Travel {public static void main(String[] args) {String[] vertex = {"a", "b", "c", "d", "e"};int[] weight = {0, 1, 1, 3, 6};double[][] matrix = {{0, 1, 0, 1, 0},{0, 0, 1, 0, 0},{0, 0, 0, 0, 1},{0, 0, 0, 0, 1},{0, 0, 0, 0, 0}};Graph<String> graph = new Graph<>(matrix, vertex, weight);System.out.println(graph.getMinWeight(graph.startSearch()));}public static class Graph<T> {// 邻接矩阵private double[][] matrix;// 顶点数组private String[] vertex;// 顶点数组对应权重值private int[] weight;// 顶点的数目private int vertexNum;// 当前结点是否还有下一个结点,判断递归是否结束的标志private boolean noNext = false;// 所有路径的结果集private List<List<String>> result = Lists.newArrayList();public Graph(double[][] matrix, String[] vertex, int[] weight) {if (matrix.length != matrix[0].length) {throw new IllegalArgumentException("该邻接矩阵不是方阵");}if (matrix.length != vertex.length) {throw new IllegalArgumentException("结点数量和邻接矩阵大小不一致");}if (vertex.length != weight.length) {throw new IllegalArgumentException("邻接矩阵大小和权重值数量不一致");}this.matrix = matrix;this.vertex = vertex;this.weight = weight;vertexNum = matrix.length;}/*** 深度遍历的递归*/private void DFS(int begin, List<String> path) {// 将当前结点加入记录队列path.add(vertex[begin]);// 标记回滚位置int rollBackNum = -1;// 遍历相邻的结点for (int i = 0; i < vertexNum; i++) {if ((matrix[begin][i] > 0)) {// 临时加入相邻结点,试探新的路径是否已遍历过path.add(vertex[i]);if (containBranch(result, path)) {// 路径已存在,将相邻结点再移出记录队伍path.remove(vertex[i]);// 记录相邻点位置,用于循环结束发现仅有当前一个相邻结点时回滚事件rollBackNum = i;// 寻找下一相邻结点continue;} else {// 路径为新路径,准备进入递归,将相邻结点移出记录队伍,递归中会再加入,防止重复添加path.remove(vertex[i]);// 递归DFS(i, path);}}// 终止递归if (noNext) {return;}}if (rollBackNum > -1) {// 循环结束仅有一个相邻结点,从这个相邻结点往下递归DFS(rollBackNum, path);} else {// 当前结点没有相邻结点,设置flag以结束递归noNext = true;}}/*** 开始深度优先遍历*/public List<List<String>> startSearch() {for (int i = 0; i < countPathNumber(); i++) {// 用于存储遍历过的点List<String> path = new LinkedList<>();noNext = false;// 开始遍历DFS(0, path);// 保存结果result.add(path);}return result;}/*** 获取权重值最大的路径*/public MaxWeight getMaxWeight(List<List<String>> lists) {Map<String, Integer> weightMap = Maps.newHashMap();for (int i = 0; i < vertex.length; i++) {weightMap.put(vertex[i], weight[i]);}int max = 0;int index = 0;for (int i = 0; i < lists.size(); i++) {int w = 0;for (String str : lists.get(i)) {w += weightMap.get(str);}if (w > max) {max = w;index = i;}}return new MaxWeight(lists.get(index), max);}/*** 获取权重值最小的路径*/public MaxWeight getMinWeight(List<List<String>> lists) {Map<String, Integer> weightMap = Maps.newHashMap();for (int i = 0; i < vertex.length; i++) {weightMap.put(vertex[i], weight[i]);}int min = 0;int index = 0;for (int i = 0; i < lists.size(); i++) {int w = 0;for (String str : lists.get(i)) {w += weightMap.get(str);}if(min == 0){min = w;index = i;}if (w < min) {min = w;index = i;}}return new MaxWeight(lists.get(index), min);}class MaxWeight {private List<String> path;private int weight;public List<String> getPath() {return path;}public void setPath(List<String> path) {this.path = path;}public int getWeight() {return weight;}public void setWeight(int weight) {this.weight = weight;}public MaxWeight(List<String> path, int weight) {this.path = path;this.weight = weight;}@Overridepublic String toString() {return "MaxWeight{" +"path=" + path +", weight=" + weight +'}';}}/*** 计算路径的分支数量*/private int countPathNumber() {int[] numberArray = new int[vertexNum];for (int i = 0; i < vertexNum; i++) {for (int j = 0; j < vertexNum; j++) {if (matrix[j][i] > 0) {numberArray[j]++;}}}int number = 1;for (int k = 0; k < vertexNum; k++) {if (numberArray[k] > 1) {number++;}}return number;}/*** 判断当前路径是否被已有路径的结果集合所包含*/private boolean containBranch(List<List<String>> nodeLists, List<String> edges) {for (List<String> list : nodeLists) {if (list.containsAll(edges)) {return true;}}return false;}}
}

(八)股票交易日 or 风口的猪

股票交易日

在股市的交易日中,假设最多可进行两次买卖(即买和卖的次数均小于等于2),规则是必须一笔成交后进行另一笔(即买-卖-买-卖的顺序进行)。给出一天中的股票变化序列,请写一个程序计算一天可以获得的最大收益。请采用实践复杂度低的方法实现。

给定价格序列prices及它的长度n,请返回最大收益。保证长度小于等于500。

如下:

[10, 22, 5, 75, 65, 80], 6

返回:

87

风口的猪

风口之下,猪都能飞。当今中国股市牛市,真可谓“错过等七年”。 给你一个回顾历史的机会,已知一支股票连续n天的价格走势,以长度为n的整数数组表示,数组中第i个元素(prices[i])代表该股票第i天的股价。

假设你一开始没有股票,但有至多两次买入1股而后卖出1股的机会,并且买入前一定要先保证手上没有股票。若两次交易机会都放弃,收益为0。 设法计算你能获得的最大收益。 输入数值范围:2<=n<=100,0<=prices[i]<=100

输入:

[3, 8, 5, 1, 7, 8] 6

返回:

12

public class Stock {public static void main(String[] args) {
//        int[] dataSet = {10, 22, 5, 75, 65, 80};int[] dataSet = {3, 8, 5, 1, 7, 8};int result = maxProfit(dataSet, dataSet.length);System.out.println(result);}public static int maxProfit(int[] dataSet, int n) {int sum = 0;for (int i = 0; i < dataSet.length; i++) {int tmp = getMax(dataSet, 0, i - 1) + getMax(dataSet, i, dataSet.length-1);if (tmp > sum) {sum = tmp;}}return sum;}private static int getMax(int[] dataSet, int left, int right) {if (left >= right) {return 0;}int min = dataSet[left];int max = 0;for (int i = left; i <= right; i++) {if (dataSet[i] - min > max) {max = dataSet[i] - min;}if (dataSet[i] < min) {min = dataSet[i];}}return max;}
}

最多一次的情况

public class Solution {public int maxProfit(int[] prices) {if(prices==null||prices.length==0){return 0;}int max=0;int min=prices[0];for(int i=0;i<prices.length;i++){min = Math.min(min,prices[i]);max=Math.max(max,prices[i]-min);}return max;}
}

(九)统计文件中单词出现次数前50的单词(不考虑大文件情况)

import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;public class CountWordsOfArticle
{public static List countWordsOfArticle(String[] args) throws Exception{ArrayList<Entry<String, Integer>> resultList = null;try{// 读取文件BufferedReader br = new BufferedReader(new FileReader("C:/words.txt"));// 建一个存放读入数据的集合HashMap<String, Integer> CountWordsMap = new HashMap<String, Integer>();String line = br.readLine();// 每次读取一行while (null != line){// 循环读入并进行切分String[] split = line.split(" ");for (String word : split){// 遍历字符串数组if (0 != word.length()){// 判断对象不为空if (CountWordsMap.containsKey(word)){// 判断集合中是否包括目标单词// 如果包括,value值+1,如果不包括,将新单词放入集合中CountWordsMap.put(word, CountWordsMap.get(word) + 1);}else{CountWordsMap.put(word, 1);}}}}// 集合中的元素以k,v形式取出Set<Entry<String, Integer>> entrySet = CountWordsMap.entrySet();// 放入List集合中resultList = new ArrayList<Entry<String, Integer>>(entrySet);Collections.sort(resultList, new Comparator<Entry<String, Integer>>(){// 进行降序排序@Overridepublic int compare(Entry<String, Integer> o1, Entry<String, Integer> o2){// 重写比较器return o2.getValue().compareTo(o1.getValue());}});// 返回列表的前50项if (resultList.size() >= 50){return resultList.subList(0, 50);}return resultList;}catch (Exception e){return null;}}
}

常见基本编程练习与思考相关推荐

  1. 使用MVVM尝试开发Github客户端及对编程的一些思考

    本文已授权 微信公众号 玉刚说 (@任玉刚)独家发布. 本文中我将尝试分享我个人 搭建个人MVVM项目 的过程中的一些心得和踩坑经历,以及在这过程中目前对 编程本质 的一些个人理解和感悟,特此分享以期 ...

  2. 安装python爬虫scrapy踩过的那些坑和编程外的思考

    '转载地址:http://www.cnblogs.com/rwxwsblog/p/4557123.html' 这些天应朋友的要求抓取某个论坛帖子的信息,网上搜索了一下开源的爬虫资料,看了许多对于开源爬 ...

  3. 电脑编程python老是出现错误_python常见的编程错误

    常见的编程错误2.1 试图访问一个未赋值的变量,会产生运行时错误. 常见的编程错误2.2 ==,!=, >=和<=这几个运算符的两个符号之间出现空格,会造成语法错误. 常见的编程错误2.3 ...

  4. 常见Shell编程脚本

    常见Shell编程脚本 一.Linux运维监控相关 1.创建 Linux 系统账户及密码 #!/bin/bash # 通过位置变量创建 Linux 系统账户及密码 #$1 是执行脚本的第一个参数,$2 ...

  5. java常见笔试编程题(一)

    java常见笔试编程题(一) 编写一个截取字符串的函数,输入为一个字符串和字节数,输出为按字节截取的字符串.但是要保证汉字不被截半个,例如"人abc",应该截为"人ab& ...

  6. 学python编程_少儿学Python编程的一些思考

    自从孩子上了初中,孩子妈就开始盯着各种真假难辨的中考.高考新政传言.当她从铺天盖地的少儿编程广告里获悉,编程将纳入中考,高考范围,并且2018年高考,多个省份的数学卷甚至都出现了编程题时,就变得异常兴 ...

  7. 面向对象编程(OOP)和函数式编程(FP)的思考

    最近看过不少 JavaScript 的类(实际是嵌套 function),自己也写了一些,发现一个值得思考的问题. 有的作者可能为了提高一点性能,喜欢有事没事把方法里面的某个变量做成类的字段(attr ...

  8. 常见软件项目开发模式思考

    一.软件项目 在2000左右 程序员还是一种比较罕见的工作,那是的个人台式机还是当时富裕家庭的高级娱乐用品,一开始网络程序员部分前后端,PHP.JSP.ASP这些技术形成了最早的网络程序.BS 系统 ...

  9. python二级编程题百度文库_Python自动化面试常见的编程题及答案

    前言 随着行业的发展,编程能力逐渐成为软件测试从业人员的一项基本能力.因此在笔试和面试中常常会有一定量的编码题,主要考察以下几点. 基本编码能力及思维逻辑 基本数据结构(顺序表.链表.队列.栈.二叉树 ...

最新文章

  1. 域控制器服务器端和客户端设置
  2. 面试问:Kafka为什么速度那么快?
  3. 2019年容器安全最新现状研究报告解读
  4. java程序员_Java和Python的区别 好程序员帮大家解读
  5. 机器学习入门一 ------- 什么是机器学习,机器学习的在实际中的用处
  6. mysql连表查询最大值_SQL 两个表联合查询记录中取最大值
  7. 谈软件测试人员定位---三年软件测试总结
  8. php四则运算器,php实现简单四则运算器
  9. WPS2019政府版本
  10. STM32实现Airplay音乐播放器
  11. 算法时间复杂度分析(一)
  12. 蓝桥杯 历年试题 矩阵翻硬币
  13. 方舟服务器id哪里显示,方舟怎么看自己的ID | 手游网游页游攻略大全
  14. Python+Excel数据分析实战:军事体能考核成绩评定(一)项目概况
  15. 你怎么看欧阳娜娜空降阿里p8?
  16. 蒸馏 (distill_Distill-BERT:使用BERT进行更智能的文本生成
  17. 威尔士和英格兰同属英国,但为啥还要在世界杯上进行PK?
  18. 【毕业设计】答 辩 技 巧 一(以一个过来人的身份,祝各位答辩 过 过 过)
  19. 基于ssm的药房药店药品管理系统
  20. 微信实名认证是成年的,但游戏是未成年的,怎么改

热门文章

  1. 青岛海尔供应商java面试_青岛某企业面试题(2019-11)
  2. BUUCTF Pwn warmup
  3. visio自己画的图怎么填充_VISIO自定义图形填充
  4. 大数据生态系统 修仙之道 Hadoop Blog
  5. 关于video标签,禁止点击播放时自动全屏,和video出现诡异窗口重叠
  6. 调音台使用基础-增益结构与推子位置
  7. system information
  8. 学习游戏服务器开发必看,C++游戏服务器开发常用工具介绍
  9. 利用cocoapods创建基于git的私有库Spec Repo
  10. 腾讯T3大牛亲自教你!2021年你与字节跳动只差这份笔记,分享PDF高清版