前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Java 多线程(3)---- 线程的同步(上)

Java 多线程(3)---- 线程的同步(上)

作者头像
指点
发布于 2019-01-18 03:04:59
发布于 2019-01-18 03:04:59
73600
代码可运行
举报
文章被收录于专栏:指点的专栏指点的专栏
运行总次数:0
代码可运行

前言

我们在前面两篇文章中分别看了一下 Java 线程的一些概念、用法和对于线程控制(开始、暂停、停止)等,并对其中的一些易错点进行了总结,如果你是对这些概念还是还不是太熟悉,建议先看一下前面的文章:Java 多线程(1)— 初识线程Java 多线程(2) — 线程的控制。这篇文章我们来继续讨论 Java 多线程 — 线程的同步。

Java 内存模型

在开始介绍线程同步之前,我们必须要对 Java 中的内存模型(这里是针对线程的角度上来看)有一个大概的理解。这个是理解后面的内容的基础。 我们先从计算机的角度来看这个问题:我们都知道,计算机的 CPU 是整个计算机的 “心脏”,也是衡量一台计算机性能的一个重要指标,其 CPU 运算速度越快,相对来说这台计算机性能就越好。也正是因为计算机 CPU 的运算速度非常快,而相对来说主内存(可以理解成计算机的内存条)的读取和写入速度就很慢了,那么如果不另外采取手段弥补两者的速度差距,那么 CPU 再好的计算机的性能也会被内存的速度所影响。为了解决这个问题,人们在计算机 CPU 和内存之间又加了一个高速缓存 器件,其相比计算机主内存的特点是:读写速度比内存快 10 以上,接近于 CPU 的速度,但是其储存空间很小。我们可以用一张图来看一下 CPU、高速缓存和内存之间的关系:

我们再从 Java 线程角度上来看 Java 的内存模型: 从 Java 线程角度,我们把 Java 内存模型分为主内存和每条线程私有的工作内存。也就是说,从这个角度上看,Java 内存模型就只剩下两个类型:主内存、线程工作内存。和计算机的内存模型类似,我们也可以通过一张图来理解下 Java 线程工作内存和主内存之间的关系:

从上图中我们可以看到: 1、Java 线程只能直接对其的私有工作内存进行读取和写入数据操作,而不能对主内存直接进行读取和写入操作。 2、主内存对所有的 Java 线程都可见,即所有的 Java 线程都可以通过其工作内存来间接的修改主内存中的数据。 3、线程的工作内存只对其对应的 Java 线程可见,不同的 Java 线程不共享其工作内存。

而在图中,线程私有工作内存和主内存之间又可以进行互相的读取和写入操作,然而这里的 “读取/写入” 操作的描述其实并不严谨,因为 Java 线程工作内存和主内存之间的交互需要遵循 Java 规定的交互协议,这个交互协议定义了 8 种原子性的操作来完成线程工作内存和主内存的交互,但是在这里我们并不需要去深入的了解这 8 中操作的原理,我们只需要知道这些概念并且知道线程私有的工作内存可以通过某些 Java 底层已经实现的操作来和主内存进行数据的交互就可以了。

现在我们知道,如果一个 Java 线程要修改主内存中的某个数据,它必须经过下面几个步骤: 1、这个线程的私有工作内存读取在主内存中要修改的那个数据值并且拷贝一份副本留在该线程的工作内存中; 2、线程执行相关代码在其工作内存中修改这个从主内存拷贝过来的副本值; 3、该线程的工作内存将修改后的值写入到主内存中。

假设现在在主内存中有一个 int 类型的变量 x 值为 10,如果我想通过线程将这个变量 x 得值改为 1,根据上面的描述,会经过哪些过程?来看一张图:

原子性

了解了线程角度上的 Java 的内存模型之后,我们再来看一下原子性的概念,我们很多情况下都可能听到 原子性 这个词。 在自然界中,原子是构成物质的基本单位(当然电子等暂且不论),所以原子的意思代表着——“不可分的最小单位”。 在操作系统中的定义是:对于一个操作来说,如果执行它,那么在执行过程中不会被其他因素打断直到完成这个操作,否则这个操作就不执行。我们称这个操作具有原子性。 我们在 Java 中常用的 a = 1; 操作通常是具有原子性的,而类似于 a += 1;a++; 等操作就不具有原子性。为什么会有这个结论呢?要深入理解这个问题,我们需要从它们的字节码入手,我们可以创建一个 Java 类 Test.java

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Test {
	static int a;
	
	public static void decrease() {
		a--;
	}
	
	public static void setA(int t) {
		a = t;
	}
	
	public static int getA() {
		return a;
	}
}

我们用 javac 指令来编译这个类文件(注意:在使用这些 Java 指令之前,必须保证你的计算机已经将 JDK 中 bin 目录加入了环境变量,否则需要使用指令的绝对路径),具体格式为 javac 类文件的绝对路径 ,编译完成后我们在源文件的相同路径下会得到一个同名的 class 文件 test.class 。接下来我们再用 javap -v class文件的绝对路径 来得到对应的字节码,结果如下:

我们可以看到类中的方法中有 getstaticiconst_1isubputstaticireturnreturn 指令,有点类似于汇编指令。我们来看一下它们大概的意思:

getstatic 指令为从静态储存区取出变量的值并且压入操作栈顶 iconst_1 指令为将整形常量 1 压入操作栈顶 isub 指令为从栈中取出两个整形变量将相减的结果压入操作栈顶 putstatic 指令为从操作栈顶中取出变量的值并将变量值写入主内存中 iload_0 指令为将局部变量(这里即为 setA 方法的第一个参数)压入到操作栈顶 ireturn 指令为方法结束并返回从操作栈顶取出的 int 类型值 return 即为方法的结束返回指令

指令中提到的栈存在于 Java 线程私有的工作内存中。 我们可以看到在 setA 方法的字节码中对变量 a 进行改变的字节码只有 putstatic ,所以我们可以把它理解成原子性的。同样的,在 getA 方法中取出变量 a 的字节码也只有 ‘getstatic’ ,因此我们也可以把它理解成原子性的。但是对于 a--; 我们可以看到其操作的字节码是这么一段:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
getstatic
iconst_1
isub
putstatic

很明显 a--; 转化成字节码后要进行多步操作,所以其在没有另加同步措施干预的情况下不具有原子性。对于 a += 1;a++; 等操作也是同样的道理,相信你也可以通过字节码来分析这些操作。

线程并发带来的问题

有了上面的知识之后,我们再来看一下我们平常经常遇到的多线程并发的奇怪问题: 1、卖车票问题:假设有 10 张火车票,现在有 5 个线程模拟 5 个窗口卖票,我们用 Java 代码模拟这个过程:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 售卖火车票的测试类
 */
public static class SellTickets {
	static int tickets = 10; // 10 张火车票
	
	protected static void sell() {
		System.out.println(Thread.currentThread().getName() + "卖出了第 " + tickets-- + " 张票");
	}
	
	public static void startSell() {
		// 开启 5 个线程售票
		for (int i = 0; i < 5; i++) {
			new Thread("窗口" + (i+1)) {
				@Override
				public void run() {
					while (tickets > 0) {
						sell();
						try {
							Thread.sleep(1000);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
			}.start();
		}
	}
}

public static void main(String[] args) {
	SellTickets.startSell();
}

这个代码应该不用解释了,就是开启了 5 个线程来模拟 5 个窗口一起卖票,为了更好的观察结果,我们每卖一张票就让卖票的线程休眠一秒让出 CPU。我们来看看结果:

我们从图中可以看到这个卖票的结果显然不正确,我们知道 sell 方法中改变 tickets 变量的代码就是 tickets--; 操作 。我们已经知道这个操作不具有原子性。因此对于上图结果中两个窗口卖出了同一张车票的一种可能的情况是: 一开始先是线程 1 得到 CPU 资源开始执行,在执行 sell 方法的时候通过 getstatic 指令将主存中 tickets 的值拷贝一份副本到线程 1 的工作内存中,此时这个拷贝的副本的值是 10,线程 1 还没来得及进行下一步操作时线程 2 又得到了 CPU 资源,同样的线程 2 通过 getstatic 指令将主存中 tickets 的值拷贝一份副本到线程 2 的工作内存中,这个拷贝的副本的值也是 10(此前线程 1 还没来得及修改这个值就让出了 CPU 资源并陷入阻塞状态),我们知道 tickets-- 操作是先用 tickets 的值,再将其减一,那么在输出的时候就有可能出现上图结果中两个窗口卖出同一张票的情况。

我们再看一下出现上图结果中卖出第 0 张票的异常情况的一种可能情况: 假设当前主内存中 tickets 的值为 1,根据图中结果,线程 4 此时得到 CPU 资源并执行 sell 方法,不巧的是当线程 4 正在执行 System.out.println(...) 代码并且在请求输出流的时候被阻塞了,那么这时线程 2 得到了 CPU 资源,此时主内存中的 tickets 变量值仍然为 1,因此其 run 方法中的循环条件仍成立,线程 2 开始执行sell 方法。 不巧的事又发生了:线程 2 正在执行 System.out.println(...) 代码并且在请求输出流的时候被阻塞了, 这时线程 4 又得到了 CPU 资源并且得到了输出流资源,那么线程 4 会先从主内存中取出 tickets 的值、打印出车票信息(此时打印的结果为第 1 张车票)、将 tickets 的值减一并且将修改后的值(tickets 修改后值为 0)重新写入主内存,线程 4 结束运行。 之后线程 2 得到 CPU 资源和输出流资源,同样的,线程 2 会先从主内存中取出 tickets 的值(此时已经为 0)、打印出车票信息(此时打印的结果为第 0 张车票 )、将 tickets 的值减一并且将修改后的值(tickets 修改后值为 -1)重新写入主内存,线程 2 结束运行。也就产生了窗口 2 卖出第 0 张车票的结果。

对于这两个现象我们已经分析完了,不知道小伙伴们有没有发现一个问题:在这里我们一开始只定义了 10 张车票(tickets 初始值为 10),以计算机的执行速度,只要某个线程一获得 CPU 资源,那么这个线程几乎瞬间就可以将所有的车票卖完,根本就不需要等线程调度器再去调度其他的线程来卖车票。 那为什么还会出现多个线程轮流卖车票的情况呢?可能此时的你已经已经反应过来了:在每个线程的 run 方法中不是调用了 Thread.sleep(1000); 方法吗,这个方法会让当前执行的线程让出 CPU 资源并且陷入休眠,每卖出一张车票,这个线程就会休眠 1 秒。所以当然会有多个线程轮流卖车票的现象。 我得承认这个说法相当正确,但是我们反过来想:如果我们把每个线程中的 run 方法中的 Thread.sleep(1000) 去掉,那么就会出现所有的车票都是由一个线程卖出的现象吗?我们不妨试试:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 售卖火车票的测试类
 */
public static class SellTickets {
	static int tickets = 10; // 10 张火车票
	
	static String name = "";
	static String firstName = "";
	static boolean isSameThread = true;
	
	protected static void sell() {
		System.out.println(Thread.currentThread().getName() + "卖出了第 " + tickets-- + " 张票");
	}
	
	public static void startSell() {
		// 开启 5 个线程售票
		for (int i = 0; i < 5; i++) {
			new Thread("窗口" + (i+1)) {
				@Override
				public void run() {
					while (tickets > 0) {
						sell();
					}
				}
			}.start();
		}
	}
}

public static void main(String[] args) {
	SellTickets.startSell();
}

结果:

遗憾的是,答案并不是我们想要的,虽然说线程 1 卖出了大部分车票,但是并不是全部。那么导致这个现象的问题是什么?其实也很简单:sell 方法中我们是调用了 System.out.println(...) 方法的,这个方法是进行数据输出的方法(即为 IO 操作),还记得我们在第一篇文章:Java 多线程(1) — 初识线程 中提到的:IO 操作可能会导致线程让出 CPU 进入等待状态吗?所以即使我们去掉了 Thread.sleep(1000) 方法,也不能完全保证所有的车票都会被同一个线程卖出。因为 System.out.println(...) 方法在进行输出的同时也起了 Thread.sleep(1000) 的作用。

不能在子线程中采用输出方法,那么我们怎么检验我们刚刚的结论呢? 我们可以这样想:在线程卖出车票的时候我们并不输出,而是将第一次卖出车票的线程名记录下来,之后每当有线程卖出车票时都和前一个卖出车票的线程名对比,看看是不是同一个线程,当所有的车票都卖完之后,我们再在主线程输出我们检测结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 售卖火车票的测试类
 */
public static class SellTickets {
    int tickets = 10; // 10 张火车票
    String firstSellName = null;
    String currentSellName = null;
    boolean isSameThread = true; // 记录是否是同一个线程将所有的车票卖完
    // 5 个售票线程,为了好理解,这里采用了 Thread 类型的数组来保存售票线程,
    // 如果熟悉 ThreadGroup 的话完全可以使用 ThreadGroup 来管理这 5 个售票线程
    Thread[] sellThreads = new Thread[5]; 

    // 获取当前售票线程中还未结束售票过程的线程数
    private int getSellThreadActiveCount() {
        int activeCount = 0;
        for (Thread thread : sellThreads) {
            activeCount += (thread.isAlive() ? 1 : 0);
        }
        return activeCount;
    }

    protected void sell() {
        System.out.println(Thread.currentThread().getName() + "卖出了第 " + tickets-- + " 张票");
    }

    public void startSell() {
        Thread curThread;
        // 开启 5 个线程售票
        for (int i = 0; i < sellThreads.length; i++) {
            curThread = new Thread("窗口" + (i+1)) {
                @Override
                public void run() {
                    while (tickets > 0) {
                        tickets--;
                        // 第一次卖出车票
                        if (firstSellName == null) {
                            firstSellName = currentSellName = Thread.currentThread().getName();
                        // 当前卖出车票的线程和之前卖出车票的线程不是同一个线程
                        } else if (!currentSellName.equals(Thread.currentThread().getName())) {
                            isSameThread = false;
                            currentSellName = Thread.currentThread().getName();
                        }
                    }
                }
            };
            curThread.start();
            sellThreads[i] = curThread;
        }
        // 当当前活动的售票线程数大于 0(即为存在卖车票的子线程时),主线程应该礼让出 CPU 资源,
        // 等待所有的子线程结束之后再进行结果输出
        while (getSellThreadActiveCount() > 0) {
            Thread.yield();
        }
        System.out.println("第一次卖出车票的线程名:" + firstSellName + "\n最后一次卖出车票的线程名:"
                            + currentSellName + "\n是否只有一个线程卖出所有的车票:" + isSameThread);
    }
}

public static void main(String[] args) {
    new SellTickets().startSell();
}

结果:

当前结果确实证明了我们的猜想,那么对于所有的结果都是这样吗?这个我不敢保证,随着 tickets 的值逐渐增大的时候,情况就完全不一样了,小伙伴们可以将 tickets 的值设为 10000,试试,这是我随机截的一张将 tickets 值改为 10000 的运行结果图:

可以看到这 10000 张车票并不是由同一个线程卖出的。原因也很简单:随着 tickets 值的增大,线程 run 方法需要执行的循环次数就越多,而对于每个线程来说,其只在一个很小的时间片段内可以使用 CPU 资源, 如果在这个时间片段内其 run 方法没有执行完成,线程调度器就会使当前线程让出 CPU 资源。 因此可能在某个线程 run 方法执行到一部分的时候,线程调度器就打断了这个线程,使其让出 CPU 资源,并且调度其它线程得到 CPU 资源并执行。当然,也有可能线程调度器还是调度当前线程获得 CPU 资源,具体哪个线程得到 CPU 资源取决于线程调度的调度。

我们再来看一个常见的多线程并发导致的问题:开 10 个线程,每个线程对同一个变量递增 10000 次,最后打印结果。:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 多线程累加测试类
 */
public static class ThreadsAddTest {
    int sum = 0;
    Thread[] addThreads = new Thread[10]; // 10 个累加线程,原理同上面的售票线程代码

    // 获取当前累加线程中还未结束累加过程的线程数
    private int getAddThreadActiveCount() {
        int activeCount = 0;
        for (Thread thread : addThreads) {
            activeCount += (thread.isAlive() ? 1 : 0);
        }
        return activeCount;
    }

    private void add() {
        sum++;
    }

    void startAdd() {
        Thread curThread;
        for (int i = 0; i < addThreads.length; i++) {
            curThread = new Thread() {
                @Override
                public void run() {
                    for (int j = 0; j < 10000; j++) {
                        add();
                    }
                }
            };
            curThread.start();
            addThreads[i] = curThread;
        }
        // 当还存在累加线程的时候,证明子线程累加还未结束,主线程应该让出 CPU 资源,
        // 直到子线程累加结束后再进行打印 sum 的值
        while (getAddThreadActiveCount() > 0) {
            Thread.yield();
        }
        System.out.println("sum: " + sum);
    }
}

public static void main(String[] args) {
    new ThreadsAddTest().startAdd();
}

结果:

这个结果是不确定的,每次运行都会产生一个新的 sum 值, 但是这个值一定会小于 100000。至于原因就留给小伙伴们自己思考了(可以从 sum++; 操作是否具有原子性进行思考)。

好了。Java 多线程第三篇就到这里了,关于上面提出的问题的解决办法会在下一篇文章中给出。 对于一些 Java 字节码中的指令的意思,可以参数这篇文章: http://www.cnblogs.com/ggzwtj/archive/2012/03/26/2418634.html

如果博客中有什么不正确的地方,还请多多指点。如果这篇文章对您有帮助,请不要吝啬您的赞,欢迎继续关注本专栏。

谢谢观看。。。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018年03月17日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
使用 Python 按行和按列对矩阵进行排序
假设我们采用了一个输入的 MxM 矩阵。我们现在将使用嵌套的 for 循环对给定的输入矩阵进行逐行和按列排序。
很酷的站长
2023/02/22
6.6K0
使用 Python 按行和按列对矩阵进行排序
C#经典十大排序算法(完结)
冒泡排序算法是一种基础的排序算法,它的实现原理比较简单。核心思想是通过相邻元素的比较和交换来将最大(或最小)的元素逐步"冒泡"到数列的末尾。
追逐时光者
2023/10/25
3400
重学数据结构和算法(五)之归并排序、快速排序
冒泡排序、插入排序、选择排序这三种排序算法,它们的时间复杂度都是 O(n2),比较高,适合小规模数据的排序。归并排序和快速排序的时间复杂度为 O(nlogn) 。这两种排序算法适合大规模的数据排序
六月的雨
2021/09/26
1.3K0
重学数据结构和算法(五)之归并排序、快速排序
十大经典排序算法最强总结(含Java、Python码实现)
所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。排序算法在很多领域得到相当地重视,尤其是在大量数据的处理方面。一个优秀的算法可以节省大量的资源。在各个领域中考虑到数据的各种限制和规范,要得到一个符合实际的优秀算法,得经过大量的推理和分析。
10JQKA
2020/12/31
9690
十大排序算法理解总结
1、时间复杂度:O(n2)O(n^2)O(n2) 2、空间复杂度:O(1)O(1)O(1) 3、稳定排序 4、原地排序
鳄鱼儿
2024/05/21
1370
文心一言 VS 讯飞星火 VS chatgpt (79)-- 算法导论7.4 4题
首先,为了证明RANDOMIZED-QUICKSORT的期望运行时间是Ω(nlg n),我们需要证明在最坏的情况下,该算法的运行时间是O(nlg n)。然后,我们需要证明在最坏的情况下,算法的期望运行时间是Ω(nlg n)。
福大大架构师每日一题
2023/08/29
3110
文心一言 VS 讯飞星火 VS chatgpt (79)-- 算法导论7.4 4题
归并排序
归并排序是采用分治法的典型应用,而且是一种稳定的排序方式,不过需要使用到额外的空间。
Li_XiaoJin
2022/06/10
2940
归并排序
JavaScript 数据结构与算法之美 - 归并排序、快速排序、希尔排序、堆排序
笔者写的 JavaScript 数据结构与算法之美 系列用的语言是 JavaScript ,旨在入门数据结构与算法和方便以后复习。
夜尽天明
2019/07/30
2.4K0
排序算法-下(Java语言实现)
的排序算法,归并排序和快速排序。这两种排序算法适合大规模的数据排序,比上一节讲的那三种排序算法要更常用。
acc8226
2022/05/17
4560
排序算法-下(Java语言实现)
快排查找数组中的第K个最大元素
两种时间复杂度为O(nlogn)的排序算法,归并排序和快速排序。这两种排序算法适合大规模数据排序,更常用。
JavaEdge
2021/12/07
4.1K0
快排查找数组中的第K个最大元素
八大排序算法总结与java实现
因为健忘,加上对各种排序算法理解不深刻,过段时间面对排序就蒙了。所以决定对我们常见的这几种排序算法进行统一总结,强行学习。首先罗列一下常见的十大排序算法:
林老师带你学编程
2019/05/27
9160
八大排序算法
​ 八大排序算法是面试经常考到的,尤其是快排,希尔排序和归并也是经常会让写代码的题目,其实只要用一句话说明了他们的原理我们写起代码就没那么困难。 冒泡排序 思想:有 n 个数我们就进行 n-1 趟排序,每一趟我们都选取最大的一个数放到已经排序的位置即可。 伪代码:两个 For 循环,外层表示要进行的趟数,内层则是找出最大的数,找最大的数的方法就是比较、交换。 时间复杂度:O(n2) 空间复杂度:O(n) 代码: package Sorting; import org.junit.jupiter.ap
lwen
2018/04/17
9270
八大排序算法
剑指offer | 面试题38:数组中的逆序对
「归并排序」与「逆序对」是息息相关的。归并排序体现了 “分而治之” 的算法思想,具体为:
千羽
2022/02/23
1K0
剑指offer | 面试题38:数组中的逆序对
排序算法之希尔、归并、堆和基数排序
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
海仔
2019/10/28
5250
Python3实现快速排序、归并排序、堆
# -*- coding: utf-8 -*- # @Time : 2019-03-26 16:46 # @Author : Jayce Wong # @ProjectName : leetcode # @FileName : sorting.py # @Blog : https://blog.51cto.com/jayce1111 # @Github : https://github.com/SysuJayce import random
py3study
2020/01/06
3420
[数据结构与算法] 排序算法之冒泡排序与快速排序(快排)
冒泡排序法 冒泡排序(Bubble Sorting)的基本思想是: 通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水
时间静止不是简史
2020/07/25
3840
排序算法最强总结及其代码实现(Python/Java)
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
蛮三刀酱
2019/03/26
5180
排序算法最强总结及其代码实现(Python/Java)
排序进行曲-v2.0
![在这里插入图片描述](https://img-blog.csdnimg.cn/b9733adc7ec9467cb835499ec469cdac.png
学编程的小程
2023/10/11
1840
排序进行曲-v2.0
Python Data Structures - C2 Sort
参考内容: 1.Problem Solving with Python Chapter5: Search and Sorting online_link 2.算法导论
宅男潇涧
2018/08/01
4420
Python Data Structures - C2 Sort
【剑指offer:数组中的逆序对】暴力法、归并排序(JavaScript实现)
时间复杂度是$O(N^2)$。在 leetcode 上会 TLE,无法通过(毕竟这是道标注「困难」的题目)。
心谭博客
2020/04/21
1K0
相关推荐
使用 Python 按行和按列对矩阵进行排序
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档