前言:无论是远程的视频面试,仍是现场的面试,都有可能会有手撕代码的环节,这也是不少童鞋包括我(虽然还没遇到过..)都很头疼的东西,多是由于 IDE 自动提示功能用惯了或是其余一些缘由,总之让我手写代码就是感受很奇怪..可是我想的话,这应该侧重考察的是一些细节或者是习惯方面的一些东西,因此仍是防患于未然吧,把一些可能手撕的代码给准备准备,分享分享,但愿能够获得各位的指正,而后能有一些讨论,因为我字太丑就不上传本身默写的代码了,但仍是但愿各位潦草写一遍加深一下印象吧,以上;java
这绝对是属于重点了,无论是考察对于该重要模型的理解仍是考察代码能力,这都是一道很好的考题,因此颇有必要的,咱们先来回顾一下什么是生产者-消费者问题;node
生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要做用是生成必定量的数据放到缓冲区中,而后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。(摘自维基百科:生产者消费者问题)git
注意: 生产者-消费者模式中的内存缓存区的主要功能是数据在多线程间的共享,此外,经过该缓冲区,能够缓解生产者和消费者的性能差;github
上面说到该问题的关键是:如何保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区空时消耗数据;解决思路能够简单归纳为:web
生产者持续生产,直到缓冲区满,满时阻塞;缓冲区不满后,继续生产;面试
消费者持续消费,直到缓冲区空,空时阻塞;缓冲区不空后,继续消费;算法
生产者和消费者均可以有多个;编程
那么在 Java 语言中,能达到上述要求的,天然而然的就会有以下的几种写法,可是问题的核心都是可以让消费者和生产者在各自知足条件须要阻塞时可以起到正确的做用:数组
wait()
/notify()
方式;缓存
await()
/signal()
方式;
BlockingQueue
阻塞队列方式;
PipedInputStream
/PipedOutputStream
方式;
手写代码,咱们着重掌握上面对应的第一种和第三种写法就足够了;
在手写代码以前,咱们须要如今 IDE 上实现一遍,理解其中的过程而且找到一些重点/细节,咱们先来代码走一遍,而后我把我理解的重点给圈儿出来:
public class Producer implements Runnable {
private volatile boolean isRunning = true;
private final Vector sharedQueue; // 内存缓冲区
private final int SIZE; // 缓冲区大小
private static AtomicInteger count = new AtomicInteger(); // 总数,原子操做
private static final int SLEEPTIME = 1000;
public Producer(Vector sharedQueue, int SIZE) {
this.sharedQueue = sharedQueue;
this.SIZE = SIZE;
}
@Override
public void run() {
int data;
Random r = new Random();
System.out.println("start producer id = " + Thread.currentThread().getId());
try {
while (isRunning) {
// 模拟延迟
Thread.sleep(r.nextInt(SLEEPTIME));
// 当队列满时阻塞等待
while (sharedQueue.size() == SIZE) {
synchronized (sharedQueue) {
System.out.println("Queue is full, producer " + Thread.currentThread().getId()
+ " is waiting, size:" + sharedQueue.size());
sharedQueue.wait();
}
}
// 队列不满时持续创造新元素
synchronized (sharedQueue) {
data = count.incrementAndGet(); // 构造任务数据
sharedQueue.add(data);
System.out.println("producer create data:" + data + ", size:" + sharedQueue.size());
sharedQueue.notifyAll();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupted();
}
}
public void stop() {
isRunning = false;
}
}
有了上面的提到的解决思路,应该很容易实现,可是这里主要提一下一些细节和重点:
创造数据:生产者-消费者解决的问题就是数据在多线程间的共享,因此咱们首要关心的问题就应该是数据,咱们这里采用的是使用一个AtomicInteger
类来为咱们创造数据,使用它的好处是该类是一个保证原子操做的类,咱们使用其中的incrementAndGet()
方法不只可以保证线程安全,还能够达到一个计数的效果,因此是一个既简单又实用的选择,固然也可使用其余的数据来代替,这里注意的是要保证该类在内存中只存在一份,使用`static`修饰;
内存缓冲区:要保证在多线程环境下内存缓冲区的安全,因此咱们考虑使用简单的Vector
类来做为咱们的内存缓冲区,而且使用final
修饰保证内存缓冲区的惟一,而后的话咱们须要判断队列是否满,须要手动添加一个标识缓冲区大小的变量SIZE
,注意也是final
修饰;
模拟延迟:这里主要模拟的是一个网络延迟,咱们首先定义了一个SLEEPTIME
的延迟范围,注意使用的是`static final`修饰,而后使用Random()
类的nextInt()
方法来随机选取一个该范围内的值来模拟网络环境中的延迟;
中止方法:首先须要知道在Thread
类中有一个弃用的stop()
方法,咱们本身增长一个标志位isRunning
来完成咱们本身的stop()
功能,须要注意的是使用`volatile`来修饰,保证该标志位的可见性;
错误处理:当捕获到错误时,咱们应该使用Thread
类中的interrupted()
方法来终止当前的进程;
消息提示:咱们主要是要在控制台输出该生产者的信息,包括当前队列的状态,大小,当前线程的生产者信息等,注意的是信息格式的统一(后面的消费者一样的);
public class Consumer implements Runnable {
private final Vector sharedQueue; // 内存缓冲区
private final int SIZE; // 缓冲区大小
private static final int SLEEPTIME = 1000;
public Consumer(Vector sharedQueue, int SIZE) {
this.sharedQueue = sharedQueue;
this.SIZE = SIZE;
}
@Override
public void run() {
Random r = new Random();
System.out.println("start consumer id = " + Thread.currentThread().getId());
try {
while (true) {
// 模拟延迟
Thread.sleep(r.nextInt(SLEEPTIME));
// 当队列空时阻塞等待
while (sharedQueue.isEmpty()) {
synchronized (sharedQueue) {
System.out.println("Queue is empty, consumer " + Thread.currentThread().getId()
+ " is waiting, size:" + sharedQueue.size());
sharedQueue.wait();
}
}
// 队列不空时持续消费元素
synchronized (sharedQueue) {
System.out.println("consumer consume data:" + sharedQueue.remove(0) + ", size:" + sharedQueue.size());
sharedQueue.notifyAll();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
}
}
跟生产者相同的,你须要注意内存缓冲区/ 模拟延迟/ 错误处理/ 消息提示这些方面的细节问题,整体来讲消费者就是持续不断的消费,也比较容易实现;
有了咱们的消费者和生产者代码,咱们须要来验证一下它们的正确性,照常理来讲咱们直接建立一些消费者和生产者的线程让它们执行就能够了啊,可是为了“加分”考虑呢,咱们仍是使用线程池吧..也不是特别复杂:
public static void main(String args[]) throws InterruptedException {
// 1.构建内存缓冲区
Vector sharedQueue = new Vector();
int size = 4;
// 2.创建线程池和线程
ExecutorService service = Executors.newCachedThreadPool();
Producer prodThread1 = new Producer(sharedQueue, size);
Producer prodThread2 = new Producer(sharedQueue, size);
Producer prodThread3 = new Producer(sharedQueue, size);
Consumer consThread1 = new Consumer(sharedQueue, size);
Consumer consThread2 = new Consumer(sharedQueue, size);
Consumer consThread3 = new Consumer(sharedQueue, size);
service.execute(prodThread1);
service.execute(prodThread2);
service.execute(prodThread3);
service.execute(consThread1);
service.execute(consThread2);
service.execute(consThread3);
// 3.睡一下子而后尝试中止生产者
Thread.sleep(10 * 1000);
prodThread1.stop();
prodThread2.stop();
prodThread3.stop();
// 4.再睡一下子关闭线程池
Thread.sleep(3000);
service.shutdown();
}
你们能够自行去看看运行的结果,是没有问题的,不会出现多生产或者多消费之类的多线程问题,运行一段时间等生产者都中止以后,咱们能够观察到控制台三个消费者都在等待数据的状况:
Queue is empty, consumer 17 is waiting, size:0
Queue is empty, consumer 15 is waiting, size:0
Queue is empty, consumer 16 is waiting, size:0
该方式对比起上面一种方式实现起来要简单一些,由于不须要手动的去通知其余线程了,生产者直接往队列中放数据直到队列满,消费者直接从队列中获取数据直到队列空,BlockingQueue会自动帮咱们完成阻塞这个动做,仍是先来看看代码
public class Producer implements Runnable {
private volatile boolean isRunning = true;
private BlockingQueue<Integer> queue; // 内存缓冲区
private static AtomicInteger count = new AtomicInteger(); // 总数,原子操做
private static final int SLEEPTIME = 1000;
public Producer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
int data;
Random r = new Random();
System.out.println("start producer id = " + Thread.currentThread().getId());
try {
while (isRunning) {
// 模拟延迟
Thread.sleep(r.nextInt(SLEEPTIME));
// 往阻塞队列中添加数据
data = count.incrementAndGet(); // 构造任务数据
System.out.println("producer " + Thread.currentThread().getId() + " create data:" + data
+ ", size:" + queue.size());
if (!queue.offer(data, 2, TimeUnit.SECONDS)) {
System.err.println("failed to put data:" + data);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupted();
}
}
public void stop() {
isRunning = false;
}
}
跟上面一种方式没有很大的差异,却是代码更加简单通透,不过须要注意的是对阻塞队列添加失败的错误处理;
public class Consumer implements Runnable {
private BlockingQueue<Integer> queue; // 内存缓冲区
private static final int SLEEPTIME = 1000;
public Consumer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
int data;
Random r = new Random();
System.out.println("start consumer id = " + Thread.currentThread().getId());
try {
while (true) {
// 模拟延迟
Thread.sleep(r.nextInt(SLEEPTIME));
// 从阻塞队列中获取数据
if (!queue.isEmpty()) {
data = queue.take();
System.out.println("consumer " + Thread.currentThread().getId() + " consume data:" + data
+ ", size:" + queue.size());
} else {
System.out.println("Queue is empty, consumer " + Thread.currentThread().getId()
+ " is waiting, size:" + queue.size());
}
}
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
}
}
public static void main(String args[]) throws InterruptedException {
// 1.构建内存缓冲区
BlockingQueue<Integer> queue = new LinkedBlockingDeque<>();
// 2.创建线程池和线程
ExecutorService service = Executors.newCachedThreadPool();
Producer prodThread1 = new Producer(queue);
Producer prodThread2 = new Producer(queue);
Producer prodThread3 = new Producer(queue);
Consumer consThread1 = new Consumer(queue);
Consumer consThread2 = new Consumer(queue);
Consumer consThread3 = new Consumer(queue);
service.execute(prodThread1);
service.execute(prodThread2);
service.execute(prodThread3);
service.execute(consThread1);
service.execute(consThread2);
service.execute(consThread3);
// 3.睡一下子而后尝试中止生产者
Thread.sleep(10 * 1000);
prodThread1.stop();
prodThread2.stop();
prodThread3.stop();
// 4.再睡一下子关闭线程池
Thread.sleep(3000);
service.shutdown();
}
由于队列中添加和删除的操做比较频繁,因此这里使用LinkedBlockingQueue
来做为阻塞队列,因此这里除了内存缓冲区有所不一样之外,其余的都差很少…固然你也能够指定一个队列的大小;
生产者-消费者模式很好地对生产者线程和消费者线程进行解耦,优化了系统总体的结构,同时因为缓冲区的做用,容许生产者线程和消费者线程存在执行上的性能差别,从必定程度上缓解了性能瓶颈对系统性能的影响;上面两种写法都是很是常规的写法,只能说能起码能在及格的基础上加个那么点儿分数,若是想要得高分能够去搜索搜搜 Disruptor 来实现一个无锁的生产者-消费者模型….这里就不说起了..
改进:上面的线程输出可能会有点儿不友好(不直观),由于咱们这里是直接使用的线程的 ID 来做为输出,咱们也能够给线程弄一个名字来做为输出,以上;
排序算法固然也算是重点考察的对象之一了,毕竟基础且偏算法,固然咱们有必要去了解常见的排序算法以及它们采起了怎样的思想又是如何实现的还有复杂度的问题,可是这里的话,主要就说起两种考的比较常见的排序算法:冒泡和快排,以及分别对它们进行的一些优化;
冒泡应该是比较基础的一种算法,咱们以从小到大排序为例,它的基础思想是:从第一个数开始直到数组倒数第二个数,每一轮都去比较数组中剩下的数,若是后面的数据更小则两数交换,这样一轮一轮的比较交换下来,最大的那个数也就“沉到”了数组的最后,最小的“冒”到了数组的最前面,这样就完成了排序工做;
很简单,直接上代码:
/**
* 冒泡排序
*
* @param nums 待排序的数组
*/
public void bubbleSort(int[] nums) {
// 正确性判断
if (null == nums || nums.length <= 1) {
return;
}
// 从小到大排序
for (int i = 0; i < nums.length - 1; i++) {
for (int j = i + 1; j < nums.length; j++) {
if (nums[i] > nums[j]) {
nums[i] = nums[i] + nums[j];
nums[j] = nums[i] - nums[j];
nums[i] = nums[i] - nums[j];
}
}
}
}
这里须要注意:加上正确性判断;(讨论:其实我看大多数地方都是没有这个的,不知道有没有加上的必要…求讨论)
另外光写完实现冒泡排序的算法是不算完的,还要养成良好的习惯去写测试单元用例,并且尽量要考虑到多的点,例如这里的负数、多个相同的数之类的特殊状况,我就大概写一个吧,也欢迎指正:
@Test
public void bubbleSortTester() {
// 测试用例1:验证负数是否知足要求
int[] nums = {1, 4, 2, -2, 5, 11, -7, 0};
bubbleSort(nums);
// 输出测试结果
for (int i = 0; i < nums.length; i++) {
System.out.print(nums[i] + ", ");
}
System.out.println();
// 测试用例2:验证多个相同的数是否知足要求
nums = new int[]{1, 1, 5, 7, 7, 3, 1};
bubbleSort(nums);
// 输出测试结果
for (int i = 0; i < nums.length; i++) {
System.out.print(nums[i] + ", ");
}
}
想象一个这样的场景:若是该数组基本有序,或者在数组的后半段基本有序,上面的算法就会浪费许多的时间开销,因此咱们再也不使用双重嵌套去比较每两个元素的值,而只是不断比较数组每先后两个数值,让大的那个数不断“冒”到数组的最后,而后缩小尾边界的范围,而且增长一个标志位,表示这一趟是否发生了交换,若是没有那么证实该数组已经有序则完成了排序了:
/**
* 改进的冒泡排序
*
* @param nums 待排序的数组
*/
public void bubbleSort2(int[] nums) {
// 正确性判断
if (null == nums || nums.length <= 1) {
return;
}
// 使用一个数来记录尾边界
int length = nums.length;
boolean flag = true;// 发生了交换就为true, 没发生就为false,第一次判断时必须标志位true。
while (flag) {
flag = false;// 每次开始排序前,都设置flag为未排序过
for (int i = 1; i < length; i++) {
if (nums[i - 1] > nums[i]) {// 前面的数字大于后面的数字就交换
int temp;
temp = nums[i - 1];
nums[i - 1] = nums[i];
nums[i] = temp;
// 表示交换过数据;
flag = true;
}
}
length--; // 减少一次排序的尾边界
}
}
一样的记得写单元测试函数;
顺着这个思路,咱们进一步想象一个场景:如今有一个包含 1000 个数的数组,仅有前面 100 个数无序,后面的 900 个数都比前面的 100 个数更大而且已经排好序,那么上面优化的方法又会形成必定的时间浪费,因此咱们进一步增长一个变量记录最后发生交换的元素的位置,也就是排序的尾边界了:
/**
* 冒泡算法最优解
*
* @param nums 待排序的数组
*/
public static void bubbleSort3(int[] nums) {
int j, k;
int flag = nums.length;// flag来记录最后交换的位置,也就是排序的尾边界
while (flag > 0) {// 排序未结束标志
k = flag;// k 来记录遍历的尾边界
flag = 0;
for (j = 1; j < k; j++) {
if (nums[j - 1] > nums[j]) {// 前面的数字大于后面的数字就交换
// 交换a[j-1]和a[j]
int temp;
temp = nums[j - 1];
nums[j - 1] = nums[j];
nums[j] = temp;
// 表示交换过数据;
flag = j;// 记录最新的尾边界.
}
}
}
}
这应该是最优的冒泡排序了,同时也别忘记了最后要写测试单元用例代码;
快排也是一种很经典的算法,它使用了一种分治的思想,基本思想是:经过一趟排序将待排序的数组分红两个部分,其中一部分记录的是比关键字更小的,另外一部分是比关键字更大的,而后再分别对着两部分继续进行排序,直到整个序列有序;
很是经典的代码,直接上吧:
public static void quickSort(int[] arr) {
qsort(arr, 0, arr.length - 1);
}
private static void qsort(int[] arr, int low, int high) {
if (low < high) {
int pivot = partition(arr, low, high); // 将数组分为两部分
qsort(arr, low, pivot - 1); // 递归排序左子数组
qsort(arr, pivot + 1, high); // 递归排序右子数组
}
}
private static int partition(int[] arr, int low, int high) {
int pivot = arr[low]; // 枢轴记录
while (low < high) {
while (low < high && arr[high] >= pivot) --high;
arr[low] = arr[high]; // 交换比枢轴小的记录到左端
while (low < high && arr[low] <= pivot) ++low;
arr[high] = arr[low]; // 交换比枢轴小的记录到右端
}
// 扫描完成,枢轴到位
arr[low] = pivot;
// 返回的是枢轴的位置
return low;
}
固然,在手撕的时候须要注意函数上的 Java Doc 格式的注释,这里省略掉是为了节省篇幅,另外别忘了测试单元用例代码;
上面的代码也很容易理解,其实就是一个“填坑”的过程,第一个“坑”挖在每次排序的第一个位置arr[low]
,从序列后面往前找第一个比pivot
小的数来把这个“坑”填上,这时候的“坑”就变成了当前的arr[high]
,而后再从序列前面日后用第一个比pivot
大的数把刚才的“坑”填上,如此往复,始终有一个“坑”须要咱们填上,直到最后一个“坑”出现,这个“坑”使用一开始的pivot
填上就能够了,而这个“坑”的位置也就是pivot
该填上的正确位置,咱们再把这个位置返回,就能够把当前序列分红两个部分再依次这样操做最终就达到排序的目的了,不得不说这样的思想挺神奇的;
上面这个快速排序算法能够说是最基本的快速排序,由于它并无考虑任何输入数据。可是,咱们很容易发现这个算法的缺陷:这就是在咱们输入数据基本有序甚至彻底有序的时候,这算法退化为冒泡排序,再也不是O(n㏒n),而是O(n^2)了。
究其根源,在于咱们的代码实现中,每次只从数组第一个开始取。若是咱们采用“三者取中”,即 arr[low], arr[high], arr[(low+high)/2] 三者的中值做为枢轴记录,则能够大大提升快速排序在最坏状况下的性能。可是,咱们仍然没法将它在数组有序情形下的性能提升到O(n)。还有一些方法能够不一样程度地提升快速排序在最坏状况下的时间性能。
此外,快速排序须要一个递归栈,一般状况下这个栈不会很深,为log(n)级别。可是,若是每次划分的两个数组长度严重失衡,则为最坏状况,栈的深度将增长到O(n)。此时,由栈空间带来的空间复杂度不可忽略。若是加上额外变量的开销,这里甚至可能达到恐怖的O(n^2)空间复杂度。因此,快速排序的最差空间复杂度不是一个定值,甚至可能不在一个级别。
为了解决这个问题,咱们能够在每次划分后比较两端的长度,并先对短的序列进行排序(目的是先结束这些栈以释放空间),能够将最大深度降回到O(㏒n)级别。
关于优化的话,了解一个大概的思路就能够了,那在这里稍微总结一下:
①三数取中做为枢轴记录;
②当待排序序列的长度分割到必定大小以后,使用插入排序;
③在一次分割结束后,能够把与pivot
相等的元素聚在一块儿,继续下次分割时,不用再对与pivot
相等的元素分割;
④优化递归操做;
参考文章:https://blog.51cto.com/flyingcat2013/1281614
想要了解的更多的童鞋能够戳这里:https://blog.csdn.net/insistGoGo/article/details/7785038
二叉树也是一个容易说起的概念和写算法的问题,特别是它的几种递归遍历和非递归遍历,都是基础且常考的点,那在这里就稍微整理整理吧…
前序、中序、后序遍历都是很是基础且容易的遍历方法,重点仍是在后面的中序和后续的非递归遍历上,固然还有层序遍历,因此这里很少说,直接给代码;
public void preOrderTraverse1(TreeNode root) {
if (root != null) {
System.out.print(root.val + " ");
preOrderTraverse1(root.left);
preOrderTraverse1(root.right);
}
}
public void inOrderTraverse1(TreeNode root) {
if (root != null) {
preOrderTraverse1(root.left);
System.out.print(root.val + " ");
preOrderTraverse1(root.right);
}
}
public void postOrderTraverse1(TreeNode root) {
if (root != null) {
preOrderTraverse1(root.left);
preOrderTraverse1(root.right);
System.out.print(root.val + " ");
}
}
前面三种遍历,也就是输出结点数据的位置不一样而已,因此很容易,可是若是手写,建议问清楚面试官要求,是在遍历时直接输出仍是须要函数返回一个List集合,而后注意写测试用例代码!
层序遍历咱们只须要增长使用一个队列便可,看代码很容易理解:
public void levelTraverse(TreeNode root) {
if (root == null) {
return;
}
LinkedList<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
System.out.print(node.val + " ");
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
}
public void preOrderTraverse2(TreeNode root) {
if (root == null) {
return;
}
LinkedList<TreeNode> stack = new LinkedList<>();
TreeNode pNode = root;
while (pNode != null || !stack.isEmpty()) {
if (pNode != null) {
System.out.print(pNode.val + " ");
stack.push(pNode);
pNode = pNode.left;
} else { //pNode == null && !stack.isEmpty()
TreeNode node = stack.pop();
pNode = node.right;
}
}
}
/**
* 非递归中序遍历二叉树
*
* @param root 二叉树根节点
* @return 中序遍历结果集
*/
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
ArrayDeque<TreeNode> stack = new ArrayDeque<>();
while (root != null || !stack.isEmpty()) {
while (root != null) {
stack.addFirst(root);
root = root.left;
}
root = stack.removeFirst();
list.add(root.val);
root = root.right;
}
return list;
}
/**
* 二叉树的后序遍历
*
* @param root 二叉树根节点
* @return 后序遍历结果集
*/
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode pre = null;
while (!stack.isEmpty() || root != null) {
while (root != null) {
stack.push(root);
root = root.left;
}
root = stack.peek();
// i :判断若是右子树不为空且不为
if (root.right != null && root.right != pre) {
root = root.right;
} else {
root = stack.pop();
list.add(root.val);
pre = root;
root = null;
}
}
return list;
}
若是比较难以理解的话,能够本身尝试着跟跟 Debug 而后看看过程;
另外的话还有一些比较常见的关于树的算法,在文章的末尾,这里就再也不赘述了:
连接:https://www.jianshu.com/p/4ef1f50d45b5
除了上面 3 Part 比较重要的点以外,还有一些其余的算法也是常常考到的,下面咱们来讲;
这是一道很经典的题,不只考你对链表的理解,并且还有一些细节(例如正确性判断/ 测试用例)须要你从代码层面去展示,下面咱们给出两段代码,读者能够自行去比较,我只是提供一个思路;
这是最经典的算法,也是须要咱们紧紧掌握的方法,最重要的仍是理解while()
循环中的过程:
公众号字数限制,详情戳:https://www.jianshu.com/p/3f0cd7af370d
这是一个很简单的思路,比上个思路要多遍历一遍链表,可是好处是简单,思路清晰,实现起来容易,emm,像这一类问题我以为另外一个比较重要的就是触类旁通的能力吧,在这里我只提供两个思路,其实还有不少种实现方法,固然也别忘了细节的东西~
公众号字数限制,详情戳:https://www.jianshu.com/p/3f0cd7af370d
问题描述:将两个有序链表合并为一个新的有序链表并返回。新链表是经过拼接给定的两个链表的全部节点组成的;
一样的经典算法,须要掌握:
公众号字数限制,详情戳:https://www.jianshu.com/p/3f0cd7af370d
这道题也是 LeetCode 上的一道题,我当时的作法是下面这样的,虽然看起来代码量多了很多并且看起来蠢蠢的..可是通过 LeetCode 测试,甚至比上面的实现要快上那么 2ms,特别提醒:下面的代码只是用做一个思路的参考,着重掌握上面的代码 :
公众号字数限制,详情戳:https://www.jianshu.com/p/3f0cd7af370d
题目描述:找出两个链表的第一个公共结点;
公众号字数限制,详情戳:https://www.jianshu.com/p/3f0cd7af370d
须要注意的细节是:①正确性判断;②判断链表是否本身成环;③注释;④注意要本身写测试用例啊…
另外还有一些相似的题目像是:①链表入环结点;②链表倒数第k个结点;之类的都是须要掌握的…
二分查找也是一类比较常考的题目,其实代码也比较容易理解,直接上吧,再再再提醒一下:注意正确性判断还有测试用例…
公众号字数限制,详情戳:https://www.jianshu.com/p/3f0cd7af370d
公众号字数限制,详情戳:https://www.jianshu.com/p/3f0cd7af370d
这也是一道很经典的题,一般是要要求 N 值的范围的,常规写法应该很简单,因此须要掌握的是优化以后的算法:
公众号字数限制,详情戳:https://www.jianshu.com/p/3f0cd7af370d
仍是注意正确性判断而后写测试用例…
若是用手写代码的话,确实是个挺麻烦的事儿,首先须要对代码有至关的熟悉程度,而后其次的话考察的都是一些细节的东西,例如:
编码规范:包括一些命名的规范/ 注释的规范等等;
缩进:这个我本身却是挺在乎的..关于这个能够去参考参考阿里出的那个规范手册;
注释:若是命名规范作得好的话实际上是能够达到代码即注释的,可是仍然有一些须要标注的地方例如函数头之类的,最好仍是作好注释;
代码的完整性:我以为这个包括对于错误的处理/ 正确性判断这样一类的东西;
测试用例:每一个函数都须要必定的测试来保证其正确性,因此这个仍是挺有必要的,特别是一些边界状况,null 值判断之类的;
想好再下笔:这一点其实也蛮重要的,无论是在纸上仍是在咱们平时的编程中,思路永远都是更重要的;
说来讲去仍是关于代码的事,我以为仍是理清思路最重要,因此咱们须要在一遍一遍熟悉代码的过程当中,熟知这些代码的思路,只有搞清楚这些代码背后的原理了,咱们才能正确且快速的写出咱们心中想要的代码,而不是简单的去背诵,这样是没有很大效果的,以上…
欢迎转载,转载请注明出处!
简书ID:@我没有三颗心脏
github:wmyskxz
欢迎关注公众微信号:wmyskxz_javaweb
分享本身的Java Web学习之路以及各类Java学习资料
想要交流的朋友也能够加qq群:3382693
我没有三颗心脏