<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Frank&#39;s Blog</title>
  
  <subtitle>Frank的个人博客</subtitle>
  <link href="https://blog.frankfang.cn/atom.xml" rel="self"/>
  
  <link href="https://blog.frankfang.cn/"/>
  <updated>2021-09-28T19:16:08.000Z</updated>
  <id>https://blog.frankfang.cn/</id>
  
  <author>
    <name>Frank Fang</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Java并发编程：线程安全</title>
    <link href="https://blog.frankfang.cn/posts/202109796.html"/>
    <id>https://blog.frankfang.cn/posts/202109796.html</id>
    <published>2021-09-28T19:16:08.000Z</published>
    <updated>2021-09-28T19:16:08.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、基本概念"><a href="#一、基本概念" class="headerlink" title="一、基本概念"></a>一、基本概念</h2><h3 id="1-1-定义"><a href="#1-1-定义" class="headerlink" title="1.1 定义"></a>1.1 定义</h3><p>当多个线程访问某个类时，不管运行时环境采用何种调度方式或者这些线程将如何交替执行，并且在主调代码中不需要任何额外的同步或协同，这个类都能表现出正确的行为，那么就称这个类是线程安全的。</p><h3 id="1-2-无状态对象"><a href="#1-2-无状态对象" class="headerlink" title="1.2 无状态对象"></a>1.2 无状态对象</h3><p>如果只是看上面的定义是无法弄清楚什么是线程安全的，因此我们需要举一些例子来进行说明，就比如说Java Web开发中经常使用的<code>Servlet</code>，一般来说<code>Servlet</code>是单例的（也可以是多例的，但这里我们只讨论单例的情况），        实例中的方法会被多个线程进行调用，因此<code>Servlet</code>要保证其是线程安全的。所以<code>Servlet</code>通常都被设计成无状态的，下面给出一个例子：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">StatelessServlet</span> <span class="title">implments</span> <span class="title">Servlet</span> </span>&#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">service</span><span class="params">(ServletRequest req, ServletResponse resp)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">int</span> i = handleReq(req);</span><br><span class="line">        sendResponse(i);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br><span class="line">        </span><br></pre></td></tr></table></figure><p>与大多数<code>Servlet</code>相同，<code>StatelessServlet</code>是无状态的：它既<strong>不包含任何域</strong>，也<strong>不包含任何对其他类中域的引用</strong>。计算过程中的临时状态仅存在于线程栈上的局部变量中，并且只能由正在执行的线程访问。访问该<code>Servlet</code>的线程不会影响另一个访问同一个<code>Servlet</code>的线程的计算结果，因为这两个线程并没有共享状态，就好像它们都在访问不同的实例。由于线程访问无状态对象的行为并不会影响其他线程中操作的正确性，因此无状态对象是线程安全的。</p><blockquote><p>无状态对象一定是线程安全的</p></blockquote><p>大多数<code>Servlet</code>都是无状态的，从而极大地降低了在实现<code>Servlet</code>线程安全性时的复杂性。只有当<code>Servlet</code>在处理请求时需要保存一些信息，线程安全性才会成为一个问题。</p><h3 id="1-3-实例：多窗口售票"><a href="#1-3-实例：多窗口售票" class="headerlink" title="1.3 实例：多窗口售票"></a>1.3 实例：多窗口售票</h3><p>假设有一个公园，该公园每天卖100张票，共有3个窗口进行售票，现在需要写一个程序来模拟售票的过程。下面就来实现该需求：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Ticket</span> <span class="keyword">implements</span> <span class="title">Runnable</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">int</span> ticket = <span class="number">100</span>;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">while</span> (<span class="keyword">true</span>) &#123;</span><br><span class="line">            <span class="keyword">if</span> (ticket &gt; <span class="number">0</span>) &#123;</span><br><span class="line">                System.out.println(Thread.currentThread().getName() + <span class="string">&quot;进行售票: &quot;</span> + ticket--);</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们创建了一个<code>Ticket</code>类实现<code>Runnable</code>接口，接下来我们来编写测试类来进行测试：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SyncDemo</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line">        Ticket ticket = <span class="keyword">new</span> Ticket();</span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; <span class="number">3</span>; i++) &#123;</span><br><span class="line">            Thread t = <span class="keyword">new</span> Thread(ticket);</span><br><span class="line">            t.start();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>既然写好了测试类，那我们就来进行测试。结果却出现了下面的情况：</p><p><img src="https://cdn.frankfang.cn/img/0c23e7e2-5a40-4da0-ad49-1c48c1459de5.png" alt="pic1.png"></p><ul><li>注：为了更容易观察出这种结果可以让每个线程执行完<code>ticket--</code>后休眠一段时间。</li></ul><p>通过控制台的输出我们可以轻易的观察到不同窗口（线程）竟然出现售出同一张票的情况，这显然是不符合预期的，那么该如何解决这种问题呢？</p><h2 id="二、原子性"><a href="#二、原子性" class="headerlink" title="二、原子性"></a>二、原子性</h2><h3 id="2-1-竟态条件"><a href="#2-1-竟态条件" class="headerlink" title="2.1 竟态条件"></a>2.1 竟态条件</h3><p>在上面多窗口售票的例子中，之所以会出现多个线程售出同一张的情况，原因就在于<code>ticket--</code>虽然看起来只是一个操作，但这个操作是非<strong>原子性</strong>的，因此并不会作为一个不可分割的操作来执行。实际上，它包含了三个独立的操作：读取<code>ticket</code>的值，将值减1，然后将计算结果写入<code>ticket</code>。这是一个“读取-修改-写入”的操作序列，并且其结果状态依赖于之前的状态。<br>​</p><p>因此在多窗口售票的例子中，在某种情况下，每个线程读到的值都是同一个值，接着执行递减操作，并将<code>ticket</code>的值设为原先的值减1，结果就出现不同窗口（线程）售出同一张票的情况。<br>​</p><p>在并发编程中，这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况，它有一个正式的名字：**竞态条件(Race Condition)**。</p><h3 id="2-2-实例：错误的单例模式实现"><a href="#2-2-实例：错误的单例模式实现" class="headerlink" title="2.2 实例：错误的单例模式实现"></a>2.2 实例：错误的单例模式实现</h3><p>在很多情况下需要某个类是单例的，下面给出单例模式的一种实现：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Singleton</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> Singleton instance;</span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="title">Singleton</span><span class="params">()</span> </span>&#123;&#125;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> Singleton <span class="title">getInstance</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (instance == <span class="keyword">null</span>) &#123;</span><br><span class="line">            instance = <span class="keyword">new</span> Singleton();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> instance;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>实际上上面的实现是错误的，在<code>getInstance</code>方法中存在竟态条件：假设在A、B线程中同时调用该方法，A判断<code>instance</code>为空，因此实例化一个<code>Singleton</code>对象；而B也判断<code>instance</code>为空，因此又实例化一个<code>Singleton</code>对象，这显然违背了单例模式的要求。</p><h2 id="三、加锁机制"><a href="#三、加锁机制" class="headerlink" title="三、加锁机制"></a>三、加锁机制</h2><p>多窗口售票和单例模式实现的例子中都包含一组需要以原子方式执行（或者说不可分割）的操作。<strong>要避免竞态条件问题，就必须在某个线程修改该变量时，通过某种方式防止其他线程使用这个变量，从而确保其他线程只能在修改操作完成之前或之后读取和修改状态，而不是在修改状态的过程中</strong>。如果多窗口售票中的<code>ticket--</code>是原子操作的话，那么竟态条件就不会发生，换句话说就是“读取-修改-写入”这个复合操作要么全部不执行，要么全部执行。<br>Java的内置锁相当于一种互斥体（互斥锁），这意味着<strong>最多只有一个线程能持有这种锁</strong>。当线程A尝试获取一个由线程B持有的锁时，线程A必须等待或者阻塞，直到线程B释放这个锁。如果B永远不释放锁，那么A也将永远地等下去。由于每次只能有一个线程执行内置锁保护的代码块，因此，由这个锁保护的同步代码块会以原子方式执行，多个线程在执行该代码块时也不会相互干扰。并发环境中的原子性与事务应用程序中的原子性有着相同的含义一组语句作为一个不可分割的单元被执行。<strong>任何一个执行同步代码块的线程，都不可能看到有其他线程正在执行由同一个锁保护的同步代码块</strong>。</p><h3 id="3-1-synchronized"><a href="#3-1-synchronized" class="headerlink" title="3.1 synchronized"></a>3.1 synchronized</h3><p>Java提供了一种内置的锁机制来支持原子性：<strong>同步代码块（Synchronized Block）</strong>。同步代码块包括两部分：一个作为锁的对象引用，一个作为由这个锁保护的代码块。以关键字_synchronized_来修饰的方法就是一种横跨整个方法体的同步代码块，其中该<strong>同步代码块的锁就是方法调用所在的对象</strong>，<strong>静态的</strong>_<strong>synchronized</strong>_<strong>方法以Class对象作为锁</strong>。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">synchronized</span>(lock) &#123;</span><br><span class="line">    <span class="comment">// 访问或修改由锁保护的共享状态</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>每个Java对象都可以用做一个实现同步的锁</strong>，这些锁被称为内置锁（Intrinsic Lock）或<strong>监视器锁（Monitor Lock）</strong>。线程在<strong>进人同步代码块之前会自动获得锁</strong>，并且在<strong>退出同步代码块时自动释放锁</strong>，而无论是通过正常的控制路径退出，还是通过从代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。<br>Java支持通过同步代码块和同步方法的形式进行加锁，下面分别介绍这两种方式：</p><h4 id="3-1-1-同步代码块"><a href="#3-1-1-同步代码块" class="headerlink" title="3.1.1 同步代码块"></a>3.1.1 同步代码块</h4><p>代码格式如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">synchronized</span>(lock) &#123;</span><br><span class="line">    <span class="comment">// 访问或修改由锁保护的共享状态</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果同步代码块处于非静态方法内，则可以选择当前类本身（this）或当前类中任意类型的类对象作为锁；如果如果同步代码块处于静态方法内，则可以选择当前类的Class对象作为锁。</p><h4 id="3-1-2-同步方法"><a href="#3-1-2-同步方法" class="headerlink" title="3.1.2 同步方法"></a>3.1.2 同步方法</h4><p>代码格式如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title">func</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 访问或修改由锁保护的共享状态</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>同步方法本质上是选择当前类的Class对象作为锁。</p><h3 id="3-2-重入"><a href="#3-2-重入" class="headerlink" title="3.2 重入"></a>3.2 重入</h3><h4 id="3-2-1-概念"><a href="#3-2-1-概念" class="headerlink" title="3.2.1 概念"></a>3.2.1 概念</h4><p>当某个线程请求一个由其他线程持有的锁时，发出请求的线程就会阻塞。然而由于内置锁是可重入的，因此如果某个线程试图获得一个已经由它自己持有的锁，那么这个请求就会成功。<strong>“重入”意味着获取锁的操作的粒度是“线程”，而不是“调用”</strong>。</p><h4 id="3-2-2-原理"><a href="#3-2-2-原理" class="headerlink" title="3.2.2 原理"></a>3.2.2 原理</h4><p>重入的一种实现方法是<strong>为每个锁关联一个获取计数器值和一个所有者线程</strong>。当计数器值为0时，这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时，JVM将记下锁的持有者，并且将获取计数器值置为1.如果同一个线程再次获取这个锁，计数值将递增，而当线程退出同步代码块时，计数器会相应地递减，当计数值为0时，这个锁将被释放。</p><h4 id="3-2-3-为什么使用可重入锁"><a href="#3-2-3-为什么使用可重入锁" class="headerlink" title="3.2.3 为什么使用可重入锁"></a>3.2.3 为什么使用可重入锁</h4><p><strong>重入进一步提升了加锁行为的封装性，因此简化了面向对象并发代码的开发。</strong>例如下面一段代码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Parent</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title">doSomething</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Child</span> <span class="keyword">extends</span> <span class="title">Parent</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title">doSomething</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">        <span class="keyword">super</span>.doSomething();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果没有可重入锁，那么这段代码将产生死锁。因为 Parent 和 Child 中 doSomething 方法都是 synchronized 方法，因此每个 doSomething 方法在执行前都会获取 Parent 上的锁，如果内置锁不是可重入的，那么在调用 super.doSomething 时将无法获取 Parent 上的锁，因为这个锁已经被持有，从而线程将永远停顿下去，等待一个永远也无法获得的锁。</p><h3 id="3-3-实例：线程安全的多窗口售票"><a href="#3-3-实例：线程安全的多窗口售票" class="headerlink" title="3.3 实例：线程安全的多窗口售票"></a>3.3 实例：线程安全的多窗口售票</h3><p>我们使用内置锁对上面的代码进行改造：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Ticket</span> <span class="keyword">implements</span> <span class="title">Runnable</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">int</span> ticket = <span class="number">100</span>;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">while</span> (<span class="keyword">true</span>) &#123;</span><br><span class="line">            <span class="keyword">synchronized</span> (<span class="keyword">this</span>) &#123;</span><br><span class="line">                <span class="keyword">if</span> (ticket &gt; <span class="number">0</span>) &#123;</span><br><span class="line">                    System.out.println(Thread.currentThread().getName() + <span class="string">&quot;进行售票: &quot;</span> + ticket--);</span><br><span class="line">                &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                    <span class="keyword">break</span>;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>再次运行程序，可以看到控制台已经输出了正确的结果。</p><h3 id="3-4-实例：同步的单例模式实现"><a href="#3-4-实例：同步的单例模式实现" class="headerlink" title="3.4 实例：同步的单例模式实现"></a>3.4 实例：同步的单例模式实现</h3><p>只需将 getInstance 方法改为同步方法即可：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Singleton</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> Singleton instance;</span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="title">Singleton</span><span class="params">()</span> </span>&#123;&#125;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="keyword">static</span> Singleton <span class="title">getInstance</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (instance == <span class="keyword">null</span>) &#123;</span><br><span class="line">            instance = <span class="keyword">new</span> Singleton();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> instance;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-5-实现原理"><a href="#3-5-实现原理" class="headerlink" title="3.5 实现原理"></a>3.5 实现原理</h3><p>我们通过一段代码来分析其实现原理：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Singleton</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> Singleton instance;</span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="title">Singleton</span><span class="params">()</span> </span>&#123;&#125;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> Singleton <span class="title">getInstance</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">synchronized</span> (Singleton.class) &#123;</span><br><span class="line">            <span class="keyword">if</span> (instance == <span class="keyword">null</span>) &#123;</span><br><span class="line">                instance = <span class="keyword">new</span> Singleton();</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> instance;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们先使用<code>javac</code>编译器对其进行编译，随后使用<code>javap -v</code>命令对编译后的字节码文件进行反编译，结果如下：</p><p><img src="https://cdn.frankfang.cn/img/d2002261-1463-4d71-a77f-5bcfef3c6a17.png" alt="pic2.png"></p><p>注意到<code>monitorenter</code>和<code>monitorexit</code>就是进入和退出同步锁（监视器锁）的指令，这也是 synchronized 的实现原理。</p><h3 id="3-6-性能"><a href="#3-6-性能" class="headerlink" title="3.6 性能"></a>3.6 性能</h3><p>使用Java的内置锁虽然可以解决线程安全问题，但实际上这是一种很低效的方式，因为<strong>同一时刻</strong>只能有一个线程能持有锁并执行任务。因此千万<strong>不要滥用内置锁</strong>，这可能会带来非常严重的性能问题，同时尽量在可能出现线程安全问题的位置使用同步代码块，而不是简单的使用同步方法。<br><strong>要判断同步代码块的合理大小，需要在各种设计需求之间进行权衡，包括安全性、简单性和性能</strong>。</p><blockquote><p>通常，在简单与性能之间存在着相互制约因素。当实现某个同步策略时，一定不要盲目地为了性能而牺牲简单性（这可能会破坏安全性）</p></blockquote><p>当使用锁时，要清楚代码块中实现的功能，以及在执行该代码块时是否需要很长的时间。无论是执行计算密集的操作，还是在执行某个可能阻塞的操作，如果持有锁的时间过长，那么都会带来活跃性或性能问题。<br>​</p><p><strong>注：当执行时间较长的计算或者可能无法快速完成的操作时（例如网络I/O或控制台I/O），一定不要持有锁。</strong></p>]]></content>
    
    
    <summary type="html">本文主要介绍Java线程安全相关的基本概念</summary>
    
    
    
    <category term="Java" scheme="https://blog.frankfang.cn/categories/Java/"/>
    
    
    <category term="Java SE" scheme="https://blog.frankfang.cn/tags/Java-SE/"/>
    
    <category term="多线程" scheme="https://blog.frankfang.cn/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>MySQL入门：安装与使用</title>
    <link href="https://blog.frankfang.cn/posts/202109446.html"/>
    <id>https://blog.frankfang.cn/posts/202109446.html</id>
    <published>2021-09-22T18:06:45.000Z</published>
    <updated>2021-09-22T18:07:55.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="一、简介"><a href="#一、简介" class="headerlink" title="一、简介"></a>一、简介</h3><p>MySQL是目前最流行的<strong>开源</strong>的<strong>关系型数据库管理系统（RDBMS，Relational Database Management System）</strong>。</p><p>MySQL 由瑞典 MySQL AB 公司开发，目前属于 Oracle 公司。MySQL 是一种关联数据库管理系统，关联数据库将数据保存在不同的表中，而不是将所有数据放在一个大仓库内，这样就增加了速度并提高了灵活性。</p><ul><li>MySQL 是开源的，目前隶属于 Oracle 旗下产品。</li><li>MySQL 支持大型的数据库。可以处理拥有上千万条记录的大型数据库。</li><li>MySQL 使用标准的 SQL 数据语言形式。</li><li>MySQL 可以运行于多个系统上，并且支持多种语言。这些编程语言包括 C、C++、Python、Java、Perl、PHP、Eiffel、Ruby 和 Tcl 等。</li><li>MySQL 对PHP有很好的支持，PHP 是目前最流行的 Web 开发语言。</li><li>MySQL 支持大型数据库，支持 5000 万条记录的数据仓库，32 位系统表文件最大可支持 4GB，64 位系统支持最大的表文件为8TB。</li><li>MySQL 是可以定制的，采用了 GPL 协议，你可以修改源码来开发自己的 MySQL 系统。</li></ul><p>MySQL的部分用户：</p><p><img src="https://cdn.frankfang.cn/img/4bd120bc-62de-4cd5-9144-3ff6b5dab5a3.png" alt="p1.png"></p><h3 id="二、安装"><a href="#二、安装" class="headerlink" title="二、安装"></a>二、安装</h3><p>所有平台的MySQL安装地址均为：<a href="https://dev.mysql.com/downloads/mysql/">https://dev.mysql.com/downloads/mysql/</a>，根据个人需求选择 MySQL的平台和版本。下面将介绍在Linux、Windows平台的安装方法，若需获取所有平台的安装方法，请参阅：<a href="https://dev.mysql.com/doc/refman/8.0/en/installing.html">https://dev.mysql.com/doc/refman/8.0/en/installing.html</a>。</p><h4 id="2-1-Linux"><a href="#2-1-Linux" class="headerlink" title="2.1 Linux"></a>2.1 Linux</h4><p>接下来在 Centos7 系统下使用 yum 命令安装 MySQL。</p><h5 id="2-1-1-下载-yum-包"><a href="#2-1-1-下载-yum-包" class="headerlink" title="2.1.1 下载 yum 包"></a>2.1.1 下载 yum 包</h5><p>首先需要去官网下载 yum 包，官网地址：<a href="https://dev.mysql.com/downloads/repo/yum/">https://dev.mysql.com/downloads/repo/yum/</a></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">wget https://repo.mysql.com//mysql80-community-release-el7-3.noarch.rpm</span><br><span class="line">rpm -ivh mysql80-community-release-el7-3.noarch.rpm</span><br><span class="line">yum update</span><br><span class="line">yum install mysql-server</span><br></pre></td></tr></table></figure><h5 id="2-1-2-权限设置"><a href="#2-1-2-权限设置" class="headerlink" title="2.1.2 权限设置"></a>2.1.2 权限设置</h5><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">chown -R mysql:mysql /var/lib/mysql</span><br></pre></td></tr></table></figure><h5 id="2-1-3-初始化MySQL"><a href="#2-1-3-初始化MySQL" class="headerlink" title="2.1.3 初始化MySQL"></a>2.1.3 初始化MySQL</h5><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mysqld --initialize</span><br></pre></td></tr></table></figure><h5 id="2-1-4-启动MySQL"><a href="#2-1-4-启动MySQL" class="headerlink" title="2.1.4 启动MySQL"></a>2.1.4 启动MySQL</h5><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">systemctl start mysqld</span><br></pre></td></tr></table></figure><h5 id="2-1-5-验证安装"><a href="#2-1-5-验证安装" class="headerlink" title="2.1.5 验证安装"></a>2.1.5 验证安装</h5><p>在成功安装 MySQL 后，一些基础表会表初始化，在服务器启动后，你可以通过简单的测试来验证 MySQL 是否工作正常。我们可以使用 mysqladmin 命令来检查服务器的版本, 在 linux 上该二进制文件位于 /usr/bin 目录，在 Windows 上该二进制文件位于C:\mysql\bin 。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mysqladmin --version</span><br></pre></td></tr></table></figure><p>若需获取更详细的内容，请参阅：<a href="https://dev.mysql.com/doc/refman/8.0/en/linux-installation.html">https://dev.mysql.com/doc/refman/8.0/en/linux-installation.html</a></p><h4 id="2-2-Windows"><a href="#2-2-Windows" class="headerlink" title="2.2 Windows"></a>2.2 Windows</h4><h5 id="2-2-1-下载安装包"><a href="#2-2-1-下载安装包" class="headerlink" title="2.2.1 下载安装包"></a>2.2.1 下载安装包</h5><p>最新版的安装包地址：<a href="https://dev.mysql.com/downloads/mysql/">https://dev.mysql.com/downloads/mysql/</a>，推荐下载ZIP格式的包。<br><img src="https://cdn.frankfang.cn/img/074237a3-4988-4f7e-ba2c-1159813efac0.png" alt="p2.png"><br>下载完成后在自定义位置进行解压缩即可。</p><h5 id="2-2-2-创建配置文件"><a href="#2-2-2-创建配置文件" class="headerlink" title="2.2.2 创建配置文件"></a>2.2.2 创建配置文件</h5><p>打开刚才解压缩的文件夹，在根目录下创建<code>my.ini</code>配置文件，使用记事本打开并进行编辑：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">[client]</span><br><span class="line"># 设置mysql客户端默认字符集</span><br><span class="line">default-character-set&#x3D;utf8</span><br><span class="line"> </span><br><span class="line">[mysqld]</span><br><span class="line"># 设置3306端口</span><br><span class="line">port &#x3D; 3306</span><br><span class="line"># 设置mysql的安装目录</span><br><span class="line">basedir&#x3D;C:\\path\\mysql-8.0.xx</span><br><span class="line"># 设置 mysql数据库的数据的存放目录，MySQL 8+ 不需要以下配置，系统自己生成即可，否则有可能报错</span><br><span class="line"># datadir&#x3D;C:\\path\\xxxx</span><br><span class="line"># 允许最大连接数</span><br><span class="line">max_connections&#x3D;20</span><br><span class="line"># 服务端使用的字符集默认为8比特编码的latin1字符集</span><br><span class="line">character-set-server&#x3D;utf8</span><br><span class="line"># 创建新表时将使用的默认存储引擎</span><br><span class="line">default-storage-engine&#x3D;INNODB</span><br></pre></td></tr></table></figure><h5 id="2-2-3-初始化数据库"><a href="#2-2-3-初始化数据库" class="headerlink" title="2.2.3 初始化数据库"></a>2.2.3 初始化数据库</h5><p><strong>以管理员身份打开CMD</strong>，切换目录至MySQL安装路径下的<code>bin</code>文件夹中，并执行以下命令：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mysqld -<span class="literal">-initialize</span> -<span class="literal">-console</span></span><br></pre></td></tr></table></figure><p>执行完成后，会<strong>输出 root 用户的初始默认密码</strong>，<strong>需将此密码记录下来</strong>，以便之后使用。</p><h5 id="2-2-4-安装数据库"><a href="#2-2-4-安装数据库" class="headerlink" title="2.2.4 安装数据库"></a>2.2.4 安装数据库</h5><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mysqld install</span><br></pre></td></tr></table></figure><h5 id="2-2-5-初始化Data目录-5-7版本之后"><a href="#2-2-5-初始化Data目录-5-7版本之后" class="headerlink" title="2.2.5 初始化Data目录(5.7版本之后)"></a>2.2.5 初始化Data目录(5.7版本之后)</h5><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mysqld -<span class="literal">-initialize</span><span class="literal">-insecure</span></span><br></pre></td></tr></table></figure><h5 id="2-2-6-启动"><a href="#2-2-6-启动" class="headerlink" title="2.2.6 启动"></a>2.2.6 启动</h5><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net <span class="built_in">start</span> mysql</span><br></pre></td></tr></table></figure><h3 id="三、使用"><a href="#三、使用" class="headerlink" title="三、使用"></a>三、使用</h3><h4 id="3-1-登录"><a href="#3-1-登录" class="headerlink" title="3.1 登录"></a>3.1 登录</h4><p>当 MySQL 服务已经运行时，我们就可以通过 MySQL 自带的客户端工具登录到 MySQL 数据库中。首先打开命令提示符，输入以下命令：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mysql <span class="literal">-h</span> 主机名 <span class="literal">-P</span> 端口号 <span class="literal">-u</span> 用户名 <span class="literal">-p</span></span><br></pre></td></tr></table></figure><p>下面对其中的一些参数进行说明：</p><ul><li>-h : 指定客户端所要登录的 MySQL 主机名, 登录本机(localhost 或 127.0.0.1)该参数可以省略</li><li>-P：指定 MySQL主机的端口号，一般默认为3306</li><li>-u : 登录的用户名</li><li>-p : 告诉服务器将会使用一个密码来登录, 如果所要登录的用户名密码为空, 可以忽略此选项</li></ul><p><img src="https://cdn.frankfang.cn/img/7fa14a4d-3fde-4126-b217-982776c2c1a4.png" alt="p3.png"></p><h4 id="3-2-修改root密码"><a href="#3-2-修改root密码" class="headerlink" title="3.2 修改root密码"></a>3.2 修改root密码</h4><p>如果是首次登录，通常需要修改root密码。在之前安装MySQL时生成了一个初始的root账号密码，首先用这个密码登录，之后输入以下指令：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">set</span> password <span class="keyword">for</span> root@localhost = password(<span class="string">&#x27;新密码&#x27;</span>);</span><br></pre></td></tr></table></figure><p>之后重新使用新密码登录即可。</p><h4 id="3-3-可视化管理"><a href="#3-3-可视化管理" class="headerlink" title="3.3 可视化管理"></a>3.3 可视化管理</h4><p>如果直接用命令行来管理MySQL数据库会比较麻烦，因此可使用一些可视化的工具来管理MySQL数据库，下面推荐几款比较好用的工具。</p><h5 id="3-3-1-Navicat"><a href="#3-3-1-Navicat" class="headerlink" title="3.3.1 Navicat"></a>3.3.1 Navicat</h5><p>这是一款很多公司都在使用的数据库管理工具，功能非常强大，官网地址：<a href="https://www.navicat.com.cn/">https://www.navicat.com.cn/</a><br><img src="https://cdn.frankfang.cn/img/769a021b-c719-4b84-9e12-9e4fe82aa241.png" alt="p4.png"><br>不过该工具需要付费购买，而且价格也不便宜，不过有能力还是尽量支持正版。</p><h5 id="3-3-2-SQLyog"><a href="#3-3-2-SQLyog" class="headerlink" title="3.3.2 SQLyog"></a>3.3.2 SQLyog</h5><p>这是Webyog公司的一款数据库管理工具，官方网址：<a href="https://webyog.com/">https://webyog.com/</a><br><img src="https://cdn.frankfang.cn/img/0b2f726f-6f0a-428c-aeca-6a349143de70.png" alt="p5.png"><br>这是一款易于使用、快速而简洁的MySQL数据库图形化管理工具，可以直观的在任何地点高效地管理你的数据库。</p>]]></content>
    
    
    <summary type="html">本文主要介绍了MySQL数据库的安装和基本使用方法</summary>
    
    
    
    <category term="MySQL" scheme="https://blog.frankfang.cn/categories/MySQL/"/>
    
    
    <category term="数据库" scheme="https://blog.frankfang.cn/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    <category term="MySQL" scheme="https://blog.frankfang.cn/tags/MySQL/"/>
    
  </entry>
  
  <entry>
    <title>Java并发编程：创建线程</title>
    <link href="https://blog.frankfang.cn/posts/202106301.html"/>
    <id>https://blog.frankfang.cn/posts/202106301.html</id>
    <published>2021-06-02T09:30:39.000Z</published>
    <updated>2021-09-28T19:16:28.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="一、Thread"><a href="#一、Thread" class="headerlink" title="一、Thread"></a>一、Thread</h3><h4 id="1、基本介绍"><a href="#1、基本介绍" class="headerlink" title="1、基本介绍"></a>1、基本介绍</h4><p><code>Thread</code>类位于<code>java.lang</code>包中，是Java中表示线程的一个类，实现了<code>Runnable</code>接口。<code>Thread</code>类中常用的方法如下：</p><table><thead><tr><th>方法</th><th>说明</th></tr></thead><tbody><tr><td><code>public Thread()</code></td><td>创建一个新线程</td></tr><tr><td><code>public Thread(String name)</code></td><td>创建一个指定名称的新线程</td></tr><tr><td><code>public Thread(Runnable target)</code></td><td>创建一个带有指定目标的新线程</td></tr><tr><td><code>public Thread(Runnable target, String name)</code></td><td>创建一个带有指定目标和名称的新线程</td></tr><tr><td><code>public void start() </code></td><td>开启当前线程</td></tr><tr><td><code>public void run()</code></td><td>执行当前线程任务</td></tr><tr><td><code>public String getName()</code></td><td>获取当前线程名称</td></tr><tr><td><code>public static void sleep(long millis)</code></td><td>使当前执行的线程在指定的毫秒数内休眠</td></tr><tr><td><code>public static Thread currentThread()</code></td><td>返回当前正在执行的线程的引用</td></tr><tr><td><code>public final void setDaemon(boolean on)</code></td><td>将当前线程设置为守护线程</td></tr><tr><td><code>public static native void sleep(long millis)</code></td><td>让当前线程进入休眠状态</td></tr><tr><td><code>public static native void yield()</code></td><td>声明了当前线程已经完成了生命周期中最重要的部分，可以切换给其它线程来执行。</td></tr></tbody></table><p>若需获取<code>Thread</code>类更多的API，请参阅：<a href="https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html">java.lang.Thread</a>。</p><h4 id="2、创建线程"><a href="#2、创建线程" class="headerlink" title="2、创建线程"></a>2、创建线程</h4><h5 id="（1）继承Thread类"><a href="#（1）继承Thread类" class="headerlink" title="（1）继承Thread类"></a>（1）继承Thread类</h5><p>可以通过继承<code>Thread</code>类并重写其中的<code>run()</code>方法来创建一个自定义的线程，代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MyThread</span> <span class="keyword">extends</span> <span class="title">Thread</span> </span>&#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;继承Thread类创建线程&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ThreadDemo01</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 1.创建一个新线程</span></span><br><span class="line">        Thread thread1 = <span class="keyword">new</span> MyThread();</span><br><span class="line">        <span class="comment">// 2.开启线程</span></span><br><span class="line">        thread1.start();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果自定义的子类只在创建线程时出现过一次，也可以使用匿名内部类来创建一个自定义的线程：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ThreadDemo01</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 1.创建一个新线程</span></span><br><span class="line">        Thread t1 = <span class="keyword">new</span> Thread() &#123;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>&#123;</span><br><span class="line">                System.out.println(<span class="string">&quot;使用匿名内部类来创建线程&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;;</span><br><span class="line">        <span class="comment">// 2.开启线程</span></span><br><span class="line">        t1.start();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h5 id="（2）实现Runnable接口"><a href="#（2）实现Runnable接口" class="headerlink" title="（2）实现Runnable接口"></a>（2）实现Runnable接口</h5><p>除了通过继承<code>Thread</code>类来创建线程，我们还可以通过实现<code>Runnable</code>接口来创建线程。通过这种方式创建线程的步骤如下：</p><ul><li>定义一个实现类实现<code>Runnable</code>接口</li><li>在<code>Thread</code>类的构造方法中传入实现类</li></ul><p>下面将演示如何创建线程：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MyRunnable</span> <span class="keyword">implements</span> <span class="title">Runnable</span> </span>&#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;实现Runnable接口&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ThreadDemo01</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 1.创建Runnable接口的实现类对象</span></span><br><span class="line">        Runnable runnable = <span class="keyword">new</span> MyRunnable();</span><br><span class="line">        <span class="comment">// 2.将实现类对象传入Thread的构造方法中</span></span><br><span class="line">        Thread thread2 = <span class="keyword">new</span> Thread(runnable);</span><br><span class="line">        <span class="comment">// 3.开启线程</span></span><br><span class="line">        thread2.start();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果该实现类只在创建线程时使用一次，可以使用匿名实现类来实现<code>Runnable</code>接口。在Java 8 及之后的版本支持 <em>lambda</em> 表达式，因此也可以用 <em>lambda</em> 表达式来代替匿名实现类（这里可以使用 <em>lambda</em> 表达式是因为该接口只有一个方法，即<code>run()</code>方法）</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ThreadDemo01</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 1.创建一个新线程</span></span><br><span class="line">        Thread t2 = <span class="keyword">new</span> Thread(() -&gt; System.out.println(<span class="string">&quot;实现Runnale接口&quot;</span>));</span><br><span class="line">        <span class="comment">// 2.开启线程</span></span><br><span class="line">        t2.start();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h5 id="（3）实现Callable接口"><a href="#（3）实现Callable接口" class="headerlink" title="（3）实现Callable接口"></a>（3）实现Callable接口</h5><p>与<code>Runnable</code>不同的是，<code>Callable</code>是有返回值的，返回值通过<code>FutureTask</code>进行封装。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MyCallable</span> <span class="keyword">implements</span> <span class="title">Callable</span>&lt;<span class="title">Integer</span>&gt; </span>&#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Integer <span class="title">call</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="number">123</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ThreadDemo01</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 1.创建Callable类的实现类对象</span></span><br><span class="line">        MyCallable mc = <span class="keyword">new</span> MyCallable();</span><br><span class="line">        <span class="comment">// 2.创建FutureTask对象并传入Callable类的实现类对象</span></span><br><span class="line">        FutureTask&lt;Integer&gt; ft = <span class="keyword">new</span> FutureTask&lt;&gt;(mc);</span><br><span class="line">        <span class="comment">// 3.创建Thread对象并传入FutureTask对象（FutureTask类实现了Runnable接口的子类接口）</span></span><br><span class="line">        Thread thread3 = <span class="keyword">new</span> Thread(ft);</span><br><span class="line">        thread3.start();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="二、-Thread-or-Runnable"><a href="#二、-Thread-or-Runnable" class="headerlink" title="二、 Thread or Runnable"></a>二、 Thread or Runnable</h3><p>如果一个类继承<code>Thread</code>，则不适合资源共享（因为Java不支持多继承）。但是如果实现了<code>Runable</code>接口的话，则很容易的实现资源共享（因为Java支持实现多个接口）。</p><p>实现<code>Runnable</code>接口比继承<code>Thread</code>类所具有的优势：</p><ul><li>适给多个相同的程序代码的线程去共享同一个资源。</li><li>可以避免Java中的单继承的局限性。</li><li>增加程序的健壮性，实现解耦操作，代码可以被多个线程共享，代码和线程独立。</li><li>线程池只能放入实现<code>Runable</code>或<code>Callable</code>类线程，不能直接放入继承<code>Thread</code>的类。</li></ul><p>因此推荐使用实现<code>Runnable</code>接口的方式来创建线程。</p><h3 id="三、Executor"><a href="#三、Executor" class="headerlink" title="三、Executor"></a>三、Executor</h3><h4 id="1、简介"><a href="#1、简介" class="headerlink" title="1、简介"></a>1、简介</h4><p>在Java 5之后，并发编程引入了一堆新的启动、调度和管理线程的API。Executor 框架便是 Java 5 中引入的，其内部使用了线程池机制，它在<code>java.util.cocurrent </code>包下，通过该框架来控制线程的启动、执行和关闭，可以简化并发编程的操作。下面给出 Executor 框架的 UML 图：</p><p><img src="https://cdn.frankfang.cn/img/6b433049-e8d2-4ea9-8aa2-b27b39824173.png" alt="1621236580792.png"></p><p>在 Java 5 之后，通过 Executor 来启动线程比使用 Thread 的 start 方法更好，除了更易管理，效率更好（用线程池实现，节约开销）外，还有关键的一点：有助于避免 this 逃逸问题（ this 逃逸问题是指如果我们在构造器中启动一个线程，因为另一个任务可能会在构造器结束之前开始执行，此时可能会访问到初始化了一半的对象，这会引起一些意想不到的错误，因此要避免这种情况的发生）。</p><p>Eexecutor 作为灵活且强大的异步执行框架，其支持多种不同类型的任务执行策略，提供了一种标准的方法将任务的提交过程和执行过程解耦开发，基于生产者-消费者模式，其提交任务的线程相当于生产者，执行任务的线程相当于消费者，并用 Runnable 来表示任务，Executor 的实现还提供了对生命周期的支持，以及统计信息收集，应用程序管理机制和性能监视等机制。</p><h4 id="2、Executor-amp-ExecutorService"><a href="#2、Executor-amp-ExecutorService" class="headerlink" title="2、Executor &amp; ExecutorService"></a>2、Executor &amp; ExecutorService</h4><h5 id="（1）Executor"><a href="#（1）Executor" class="headerlink" title="（1）Executor"></a>（1）Executor</h5><p><code>Executor</code>是一个接口，在该接口中含有一个<code>execute</code>方法，功能是执行一个任务，其源代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">Executor</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Executes the given command at some time in the future.  The command</span></span><br><span class="line"><span class="comment">     * may execute in a new thread, in a pooled thread, or in the calling</span></span><br><span class="line"><span class="comment">     * thread, at the discretion of the &#123;<span class="doctag">@code</span> Executor&#125; implementation.</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> command the runnable task</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@throws</span> RejectedExecutionException if this task cannot be</span></span><br><span class="line"><span class="comment">     * accepted for execution</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@throws</span> NullPointerException if command is null</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">void</span> <span class="title">execute</span><span class="params">(Runnable command)</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>因此，我们可以使用<code>Executor</code>并调用<code>execute</code>方法来隐式地创建一个线程。</p><h5 id="（2）ExecutorService"><a href="#（2）ExecutorService" class="headerlink" title="（2）ExecutorService"></a>（2）ExecutorService</h5><p><code>ExecutorService</code>继承于<code>Executor</code>，是一个比<code>Executor</code>更泛用的接口，该接口提供了生命周期管理的方法。下面列出<code>ExecutorService</code>中的部分方法：</p><table><thead><tr><th>方法</th><th>说明</th></tr></thead><tbody><tr><td><code>void shutdown()</code></td><td>有序关闭其中执行以前提交的任务，但不接受新任务</td></tr><tr><td><code>&lt;T&gt; Future&lt;T&gt; submit(Callable&lt;T&gt; task)</code></td><td>提交一个<code>Callable</code>任务以供执行<br>并返回一个表示任务挂起结果的<code>Future</code></td></tr><tr><td><code>Future&lt;?&gt; submit(Runnable task)</code></td><td>提交<code>Runnable</code>任务以执行<br>并返回表示该任务的<code>Future</code></td></tr></tbody></table><p>若需获取更多<code>ExecutorService</code>接口的API，请参阅：<a href="https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html">java.util.concurrent.ExecutorService</a>。</p><h4 id="3、Executors"><a href="#3、Executors" class="headerlink" title="3、Executors"></a>3、Executors</h4><p><code>Executors</code>是一个工具类，该类提供了一系列工厂方法用于创建线程池，返回的线程池都实现了ExecutorService接口。</p><p>下面介绍<code>Executors</code>中常用的工厂方法：</p><table><thead><tr><th>方法</th><th>返回值</th></tr></thead><tbody><tr><td><code>newFixedThreadPool(int nThreads)</code></td><td>创建一个线程池，该线程池重用在共享无边界队列上运行的<strong>固定数量</strong>的线程</td></tr><tr><td><code>newCachedThreadPool()</code></td><td>创建一个<strong>可缓存</strong>的线程池，调用<code>execute</code>将重用以前构造的线程（如果线程可用）。如果没有可用的线程，则创建一个新线程并添加到池中</td></tr><tr><td><code>newSingleThreadExecutor()</code></td><td>创建一个<strong>单线程</strong>化的<code>Executor</code></td></tr><tr><td><code>newSingleThreadScheduledExecutor()</code></td><td>创建一个支持<strong>定时及周期性的任务执行</strong>的线程池</td></tr></tbody></table><p>若需获取更多<code>Executors</code>类的API，请参阅：<a href="https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executors.html">java.util.concurrent.Executors</a>。</p><h4 id="4、实例：使用ExecutorService创建线程"><a href="#4、实例：使用ExecutorService创建线程" class="headerlink" title="4、实例：使用ExecutorService创建线程"></a>4、实例：使用ExecutorService创建线程</h4><p>上面介绍了<code>ExecutorService</code>接口及<code>Executors</code>工具类，下面我们将演示如何使用<code>ExecutorService</code>来创建线程并执行线程任务：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 1.使用Executors工具类中的工厂方法创建线程池</span></span><br><span class="line">ExecutorService executorService = Executors.newCachedThreadPool();</span><br><span class="line"><span class="comment">// 2.执行线程任务</span></span><br><span class="line"><span class="comment">// 2.1 执行线程任务A</span></span><br><span class="line">executorService.execute(<span class="keyword">new</span> Runnable() &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            Thread.sleep(<span class="number">1000</span>);</span><br><span class="line">            System.out.println(<span class="string">&quot;线程任务A&quot;</span>);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br><span class="line"><span class="comment">// 2.2 执行线程任务B</span></span><br><span class="line">executorService.execute(<span class="keyword">new</span> Runnable() &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;线程任务B&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br><span class="line"><span class="comment">// 3.有序关闭其中执行以前提交的任务</span></span><br><span class="line">executorService.shutdown();</span><br></pre></td></tr></table></figure><h4 id="5、ThreadPoolExecutor"><a href="#5、ThreadPoolExecutor" class="headerlink" title="5、ThreadPoolExecutor"></a>5、ThreadPoolExecutor</h4><h5 id="（1）构造方法"><a href="#（1）构造方法" class="headerlink" title="（1）构造方法"></a>（1）构造方法</h5><p>上面我们介绍了如何使用Executors工具类中的工厂方法创建线程池，那么这些工厂方法具体是如何实现的呢，我们以<code>newCachedThreadPool</code>方法为例，来分析工厂方法是如何创建线程池的。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Creates a thread pool that creates new threads as needed, but</span></span><br><span class="line"><span class="comment"> * will reuse previously constructed threads when they are</span></span><br><span class="line"><span class="comment"> * available.  These pools will typically improve the performance</span></span><br><span class="line"><span class="comment"> * of programs that execute many short-lived asynchronous tasks.</span></span><br><span class="line"><span class="comment"> * Calls to &#123;<span class="doctag">@code</span> execute&#125; will reuse previously constructed</span></span><br><span class="line"><span class="comment"> * threads if available. If no existing thread is available, a new</span></span><br><span class="line"><span class="comment"> * thread will be created and added to the pool. Threads that have</span></span><br><span class="line"><span class="comment"> * not been used for sixty seconds are terminated and removed from</span></span><br><span class="line"><span class="comment"> * the cache. Thus, a pool that remains idle for long enough will</span></span><br><span class="line"><span class="comment"> * not consume any resources. Note that pools with similar</span></span><br><span class="line"><span class="comment"> * properties but different details (for example, timeout parameters)</span></span><br><span class="line"><span class="comment"> * may be created using &#123;<span class="doctag">@link</span> ThreadPoolExecutor&#125; constructors.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> the newly created thread pool</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> ExecutorService <span class="title">newCachedThreadPool</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> ThreadPoolExecutor(<span class="number">0</span>, Integer.MAX_VALUE,</span><br><span class="line">                                  <span class="number">60L</span>, TimeUnit.SECONDS,</span><br><span class="line">                                  <span class="keyword">new</span> SynchronousQueue&lt;Runnable&gt;());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过阅读源码，我们可以清楚地知道<code>Executors</code>工具类的工厂方法是通过具体情况创建一个<code>ThreadPoolExecutor</code>对象来创建线程池的。下面我们对<code>ThreadPoolExecutor</code>构造方法的参数进行简单介绍：</p><table><thead><tr><th>参数名</th><th>类型</th><th>说明</th></tr></thead><tbody><tr><td><code>corePoolSize</code></td><td><code>int</code></td><td>线程池中所保存的核心线程数，包括空闲线程</td></tr><tr><td><code>maximumPoolSize</code></td><td><code>int</code></td><td>线程池中允许的最大线程数</td></tr><tr><td><code>keepAliveTime</code></td><td><code>long</code></td><td>线程池中的空闲线程所能持续的最长时间</td></tr><tr><td><code>unit</code></td><td><code>TimeUnit</code></td><td>持续时间的单位</td></tr><tr><td><code>workQueue</code></td><td><code>BlockingQueue&lt;Runnable&gt;</code></td><td>任务执行前保存任务的队列，仅保存由<code>execute</code>方法提交的<code>Runnable</code>任务</td></tr></tbody></table><ul><li>注1：<code>TimeUnit</code>是一个枚举类型。</li><li>注2：<code>BlockingQueue</code>是一个特殊的队列，当其为空时，获取元素线程被阻塞直到队列变为非空；当其为满时，添加元素线程被阻塞直到队列不满。详细内容请参阅：<a href="https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/BlockingQueue.html">java.util.concurrent.BlockingQueue</a>。</li></ul><h5 id="（2）实现execute方法"><a href="#（2）实现execute方法" class="headerlink" title="（2）实现execute方法"></a>（2）实现execute方法</h5><p>介绍完<code>ThreadPoolExecutor</code>类的构造方法的相关参数的含义，下面我们将说明<code>ThreadPoolExecutor</code>是如何实现<code>execute</code>方法的：</p><ul><li>如果线程池中的线程数量少于<code>corePoolSize</code>，即使线程池中有空闲线程，也会创建一个新的线程来执行新添加的任务。</li><li>如果线程池中的线程数量大于等于<code>corePoolSize</code>，但缓冲队列<code>workQueue</code>未满，则将新添加的任务放到<code>workQueue</code>中，按照FIFO的原则依次等待执行（线程池中有线程空闲出来后依次将缓冲队列中的任务交付给空闲的线程执行）。</li><li>如果线程池中的线程数量大于等于<code>corePoolSize</code>，且缓冲队列<code>workQueue</code>已满，但线程池中的线程数量小于<code>maximumPoolSize</code>，则会创建新的线程来处理被添加的任务。</li><li>如果线程池中的线程数量等于了<code>maximumPoolSize</code>，可通过<code>RejectedExecutionHandler</code>来处理线程溢出问题，详细内容请参考源码，这里不再展开。</li></ul><p>总结起来就是：当有新的任务要处理时，先看线程池中的线程数量是否大于<code>corePoolSize</code>，再看缓冲队列<code>workQueue</code>是否满，最后看线程池中的线程数量是否大于<code>maximumPoolSize</code>。</p><p>另外，当线程池中的线程数量大于<code>corePoolSize</code>时，如果里面有线程的空闲时间超过了<code>keepAliveTime</code>，就将其移除线程池，这样就可以动态地调整线程池中线程的数量。</p><h5 id="（3）缓冲队列"><a href="#（3）缓冲队列" class="headerlink" title="（3）缓冲队列"></a>（3）缓冲队列</h5><p>在前面的内容中已经多次提到了缓冲队列，下面我们将详细介绍一下缓冲队列的内容。<code>ThreadPoolExecutor</code>类的构造方法中需要传入<code>BlockingQueue&lt;Runnable&gt;</code>类型的参数，<code>BlockingQueue&lt;E&gt;</code>是一个接口，它的实现类如下：</p><p><img src="https://cdn.frankfang.cn/img/3cc4280e-8c42-4f06-af9c-8665d01e9da4.png" alt="1622621740984.png"></p><p>通过<a href="https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/BlockingQueue.html">API文档</a>，我们可以知道<code>BlockingQueue&lt;E&gt;</code>有很多的实现类，那么我们该选择哪个实现类来实现该接口呢？</p><p>缓冲队列一般有<strong>直接提交</strong>、<strong>无界队列</strong>和<strong>有界队列</strong>这几种排队策略，下面我们来介绍一下这些策略的特点和使用场景。知道了这些，我们就可以对上面的问题做出回答了。</p><table><thead><tr><th>名称</th><th>说明</th></tr></thead><tbody><tr><td>直接提交</td><td>缓冲队列采用<code>SynchronousQueue</code>，它将任务直接交给线程处理而不保持它们。<br/>如果不存在可用于立即运行任务的线程（即线程池中的线程都在工作），则试图把任务加入缓冲队列将会失败，因此会构造一个新的线程来处理新添加的任务，并将其加入到线程池中<br/>直接提交通常要求无界 maximumPoolSizes（<code>Integer.MAX_VALUE</code>） 以避免拒绝新提交的任务。<code>newCachedThreadPool</code>采用的便是这种策略。<br/></td></tr><tr><td>无界队列</td><td>使用无界队列（典型的便是采用预定义容量的<code>LinkedBlockingQueue</code>，理论上是该缓冲队列可以对无限多的任务排队）将导致在所有 corePoolSize 线程都工作的情况下将新任务加入到缓冲队列中。<br/>这样，创建的线程就不会超过 corePoolSize，也因此，maximumPoolSize 的值也就无效了。<br/>当每个任务完全独立于其他任务，即任务执行互不影响时，适合于使用无界队列。<code>newFixedThreadPool</code>采用的便是这种策略。<br/></td></tr><tr><td>有界队列</td><td>当使用有限的 maximumPoolSizes 时，有界队列（一般缓冲队列使用<code>ArrayBlockingQueue</code>，并制定队列的最大长度）有助于防止资源耗尽，但是可能较难调整和控制，队列大小和最大池大小需要相互折衷，需要设定合理的参数。</td></tr></tbody></table><h5 id="（4）缓冲型线程池和固定型线程池的区别"><a href="#（4）缓冲型线程池和固定型线程池的区别" class="headerlink" title="（4）缓冲型线程池和固定型线程池的区别"></a>（4）缓冲型线程池和固定型线程池的区别</h5><p>前面我们简单介绍了Executors的几种工厂方法，在了解Executors工厂方法的实现细节之后，我们便可总结出缓冲型线程池和固定型线程池的区别。</p><ul><li><code>newCachedThreadPool()</code></li></ul><p>缓存型线程池，先查看池中有没有以前建立的线程，如果有，就进行重用，如果没有，就建一个新的线程加入池中。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> ExecutorService <span class="title">newCachedThreadPool</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> ThreadPoolExecutor(<span class="number">0</span>, Integer.MAX_VALUE,</span><br><span class="line">                                  <span class="number">60L</span>, TimeUnit.SECONDS,</span><br><span class="line">                                  <span class="keyword">new</span> SynchronousQueue&lt;Runnable&gt;());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>它将 corePoolSize 设定为 0，而将 maximumPoolSize 设定为了 Integer 的最大值，<strong>线程空闲超过60秒，将会从线程池中移除</strong>。由于核心线程数为 0，因此每次添加任务，都会先从线程池中找空闲线程，如果没有就会创建一个线程来执行新的任务，并将该线程加入到线程池中，而最大允许的线程数为 Integer 的最大值 ，因此这个线程池<strong>理论上可以不断扩大</strong>。</p><p><strong>CachedThreadPool 通常用于执行一些生存期很短的异步型任务</strong>，因此在一些面向连接的 daemon 型服务中用得不多。但对于生存期短的异步任务，它是Executor的首选。</p><ul><li><code>newFixedThreadPool(int nThreads)</code></li></ul><p>FixedThreadPool 与 CacheThreadPool 差不多，也可以进行复用，但不能随时建新的线程。任意时间点，最多只能有固定数目的活动线程存在，此时如果有新的线程要建立，只能放在另外的队列中等待，直到当前的线程中某个线程终止直接被移出池子。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> ExecutorService <span class="title">newFixedThreadPool</span><span class="params">(<span class="keyword">int</span> nThreads)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> ThreadPoolExecutor(nThreads, nThreads,</span><br><span class="line">                                  <span class="number">0L</span>, TimeUnit.MILLISECONDS,</span><br><span class="line">                                  <span class="keyword">new</span> LinkedBlockingQueue&lt;Runnable&gt;());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>它将 corePoolSize 和 maximumPoolSize 都设定为了 nThreads，这样便实现了线程池的大小的固定，不会动态地扩大。另外，keepAliveTime 设定为了 0，意味着线程只要空闲下来，就会被移除线程池。</p><p>和CacheThreadPool不同，FixedThreadPool 没有IDLE机制（闲时机制），所以 <strong>FixedThreadPool 主要用于一些很稳定很固定的并发线程，多用于服务器</strong>。</p><h3 id="四、Executor-or-Thread"><a href="#四、Executor-or-Thread" class="headerlink" title="四、Executor or Thread"></a>四、Executor or Thread</h3><p>如果项目使用的JDK版本为1.5及以上，推荐使用 Executor 框架来进行多线程应用开发。当然，如果只需开启线程去执行一些简单的任务，使用 Thread 也未曾不可。</p>]]></content>
    
    
    <summary type="html">本文主要介绍了Java语言中多线程的基本概念及相关原理</summary>
    
    
    
    <category term="Java" scheme="https://blog.frankfang.cn/categories/Java/"/>
    
    
    <category term="Java SE" scheme="https://blog.frankfang.cn/tags/Java-SE/"/>
    
    <category term="多线程" scheme="https://blog.frankfang.cn/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>几种常见的排序算法</title>
    <link href="https://blog.frankfang.cn/posts/202105074.html"/>
    <id>https://blog.frankfang.cn/posts/202105074.html</id>
    <published>2021-05-10T08:26:50.000Z</published>
    <updated>2021-05-10T08:26:50.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="一、插入排序"><a href="#一、插入排序" class="headerlink" title="一、插入排序"></a>一、插入排序</h3><p>插入排序是一种简单直观的排序方法，其基本思想是每次将一个待排序的记录按其关键字大小插入前面已排好序的子序列，直到全部记录插入完成。下面将介绍几种常见的插入排序算法。</p><h4 id="1、直接插入排序"><a href="#1、直接插入排序" class="headerlink" title="1、直接插入排序"></a>1、直接插入排序</h4><p>这是一种最简单的插入排序算法。假设当前索引为 i，在这之前的为有序序列，在这之后的为无序序列，我们要做的就是把当前索引为 i 的元素插入到前面的有序序列中，组成一个新的有序序列，之后将索引向后移动一位，重复上述操作，直到整个序列为有序序列。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 直接插入排序</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">insert_sort</span><span class="params">(<span class="keyword">int</span>* arr, <span class="keyword">int</span> length)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">for</span>(<span class="keyword">int</span> i = <span class="number">1</span>; i &lt; length; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="comment">// 有序队列的末尾下标</span></span><br><span class="line">        <span class="keyword">int</span> insert_idx = i<span class="number">-1</span>;</span><br><span class="line">        <span class="comment">// 待插入的数</span></span><br><span class="line">        <span class="keyword">int</span> insert_val = arr[i];</span><br><span class="line">        <span class="comment">// 寻找待插入的位置</span></span><br><span class="line">        <span class="keyword">while</span>(insert_idx &gt;= <span class="number">0</span> &amp;&amp; arr[insert_idx] &gt; insert_val)</span><br><span class="line">        &#123;</span><br><span class="line">            arr[insert_idx+<span class="number">1</span>] = arr[insert_idx];</span><br><span class="line">            insert_idx--;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 插入数据</span></span><br><span class="line">        arr[insert_idx + <span class="number">1</span>] = insert_val;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>该算法的时间复杂度为$O(n^2)$，空间复杂度为$O(1)$。</p><h4 id="2、折半插入排序"><a href="#2、折半插入排序" class="headerlink" title="2、折半插入排序"></a>2、折半插入排序</h4><p>当排序表为顺序表时，可对直接插入排序算法做一些改进，在查找插入位置时使用二分查找，查找完成之后统一地向后移动元素。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">insert_sort</span><span class="params">(<span class="keyword">int</span>* arr, <span class="keyword">int</span> n)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">int</span> i, j, low, high, mid;</span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">2</span>; i &lt;= n; i++) &#123;</span><br><span class="line">        <span class="comment">// 将arr[i]暂存到arr[0]中</span></span><br><span class="line">        arr[<span class="number">0</span>] = arr[i];</span><br><span class="line">        <span class="comment">// 二分查找</span></span><br><span class="line">        low = <span class="number">1</span>; high = i<span class="number">-1</span>;</span><br><span class="line">        <span class="keyword">while</span> (low &lt;= high) &#123;</span><br><span class="line">            mid = (low + high) / <span class="number">2</span>;</span><br><span class="line">            <span class="keyword">if</span> (arr[mid] &gt; arr[<span class="number">0</span>]) high = mid - <span class="number">1</span>;</span><br><span class="line">            <span class="keyword">else</span> low = mid + <span class="number">1</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 统一移动元素</span></span><br><span class="line">        <span class="keyword">for</span> (j = i - <span class="number">1</span>; j &gt;= high + <span class="number">1</span>; --j)</span><br><span class="line">            arr[j+<span class="number">1</span>] = arr[j];</span><br><span class="line">        <span class="comment">// 插入操作</span></span><br><span class="line">        arr[high + <span class="number">1</span>] = arr[<span class="number">0</span>];</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>该算法的时间复杂度为$O(n^2)$，空间复杂度为$O(1)$。</p><h4 id="3、希尔排序"><a href="#3、希尔排序" class="headerlink" title="3、希尔排序"></a>3、希尔排序</h4><p><strong>希尔排序（Shell Sort）</strong>，又称缩小增量排序，是对直接插入排序的一个改进。</p><p>交换式算法：</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">shell_sort</span><span class="params">(<span class="keyword">int</span>* arr, <span class="keyword">int</span> length)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">for</span>(<span class="keyword">int</span> gap = length; gap &gt; <span class="number">0</span>; gap /= <span class="number">2</span>) <span class="comment">// 增量为 gap/2 向下取整</span></span><br><span class="line">        <span class="keyword">for</span>(<span class="keyword">int</span> i = gap; i &lt; length; i++)</span><br><span class="line">            <span class="keyword">for</span>(<span class="keyword">int</span> j = i - gap; j &gt;= <span class="number">0</span>; j -= gap)</span><br><span class="line">                <span class="keyword">if</span>(arr[j] &gt; arr[j+gap])</span><br><span class="line">                    swap(&amp;arr[j], &amp;arr[j+gap]); <span class="comment">// 交换</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果采用这种写法，排序的效率甚至不如直接插入排序，原因是大量的时间消耗在交换上了。</p><p>移动式排序：</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">shell_sort2</span><span class="params">(<span class="keyword">int</span>* arr, <span class="keyword">int</span> length)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="comment">// 增量gap, 并逐步缩小增量</span></span><br><span class="line">    <span class="keyword">for</span>(<span class="keyword">int</span> gap = length; gap &gt; <span class="number">0</span>; gap /= <span class="number">2</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="comment">// 从第gap个元素起，逐个对其所在的组进行直接插入排序</span></span><br><span class="line">        <span class="keyword">for</span>(<span class="keyword">int</span> i = gap; i &lt; length; i++)</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">int</span> j = i;</span><br><span class="line">            <span class="keyword">int</span> tmp = arr[j];</span><br><span class="line">            <span class="keyword">while</span>(j-gap &gt;= <span class="number">0</span> &amp;&amp; arr[j-gap] &gt; tmp)</span><br><span class="line">            &#123;</span><br><span class="line">                arr[j] = arr[j-gap];</span><br><span class="line">                j -= gap;</span><br><span class="line">            &#125;</span><br><span class="line">            arr[j] = tmp;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="二、交换排序"><a href="#二、交换排序" class="headerlink" title="二、交换排序"></a>二、交换排序</h3><h4 id="1、冒泡排序"><a href="#1、冒泡排序" class="headerlink" title="1、冒泡排序"></a>1、冒泡排序</h4><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">bubble_sort</span><span class="params">(<span class="keyword">int</span>* arr, <span class="keyword">int</span> length)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">int</span> i, j;</span><br><span class="line">    <span class="keyword">int</span> tmp; <span class="comment">// 用于暂存变量</span></span><br><span class="line">    <span class="keyword">for</span>(i = <span class="number">0</span>; i &lt; length - <span class="number">1</span>; i++)</span><br><span class="line">        <span class="keyword">for</span>(j = <span class="number">0</span>; j &lt; length - <span class="number">1</span> - i; j++)</span><br><span class="line">            <span class="keyword">if</span> (arr[j] &gt; arr[j+<span class="number">1</span>])</span><br><span class="line">            &#123;</span><br><span class="line">                tmp = arr[j];</span><br><span class="line">                arr[j] = arr[j+<span class="number">1</span>];</span><br><span class="line">                arr[j+<span class="number">1</span>] = tmp;</span><br><span class="line">            &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>该算法的时间复杂度为$O(n^2)$，空间复杂度为$O(1)$。</p><p>改进算法：</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">bubble_sort2</span><span class="params">(<span class="keyword">int</span>* arr, <span class="keyword">int</span> length)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">int</span> i, j;</span><br><span class="line">    <span class="keyword">int</span> tmp; <span class="comment">// 用于暂存变量</span></span><br><span class="line">    <span class="keyword">bool</span> flag = <span class="literal">false</span>; <span class="comment">// 标识变量，表示是否进行过交换</span></span><br><span class="line">    <span class="keyword">for</span>(i = <span class="number">0</span>; i &lt; length - <span class="number">1</span>; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">for</span>(j = <span class="number">0</span>; j &lt; length - <span class="number">1</span> - i; j++)</span><br><span class="line">            <span class="keyword">if</span> (arr[j] &gt; arr[j+<span class="number">1</span>])</span><br><span class="line">            &#123;</span><br><span class="line">                flag = <span class="literal">true</span>;</span><br><span class="line">                tmp = arr[j];</span><br><span class="line">                arr[j] = arr[j+<span class="number">1</span>];</span><br><span class="line">                arr[j+<span class="number">1</span>] = tmp;</span><br><span class="line">            &#125;</span><br><span class="line">        <span class="keyword">if</span> (!flag) <span class="comment">// 在一趟排序中一次交换都没有发生过</span></span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="2、快速排序"><a href="#2、快速排序" class="headerlink" title="2、快速排序"></a>2、快速排序</h4><p>详见《<a href="https://frankfang.cn/article/202104862">快速排序</a>》。</p><h3 id="三、选择排序"><a href="#三、选择排序" class="headerlink" title="三、选择排序"></a>三、选择排序</h3><h4 id="1、简单选择排序"><a href="#1、简单选择排序" class="headerlink" title="1、简单选择排序"></a>1、简单选择排序</h4><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">select_sort</span><span class="params">(<span class="keyword">int</span>* arr, <span class="keyword">int</span> length)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">int</span> i, j;</span><br><span class="line">    <span class="keyword">int</span> min_idx;</span><br><span class="line">    <span class="keyword">for</span>(i = <span class="number">0</span>; i &lt; length - <span class="number">1</span>; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        min_idx = i;</span><br><span class="line">        <span class="keyword">for</span>(j = i + <span class="number">1</span>; j &lt; length; j++)</span><br><span class="line">            <span class="keyword">if</span> (arr[j] &lt; arr[min_idx])</span><br><span class="line">                min_idx = j;</span><br><span class="line">        <span class="comment">// 用封装的swap()函数进行交换</span></span><br><span class="line">        swap(&amp;arr[i], &amp;arr[min_idx]);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>该算法的时间复杂度为$O(n^2)$，空间复杂度为$O(1)$。</p><p>改进算法：</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">select_sort2</span><span class="params">(<span class="keyword">int</span>* arr, <span class="keyword">int</span> length)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">int</span> i, j;</span><br><span class="line">    <span class="keyword">int</span> min_idx;</span><br><span class="line">    <span class="keyword">for</span>(i = <span class="number">0</span>; i &lt; length - <span class="number">1</span>; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        min_idx = i;</span><br><span class="line">        <span class="keyword">for</span>(j = i + <span class="number">1</span>; j &lt; length; j++)</span><br><span class="line">            <span class="keyword">if</span> (arr[j] &lt; arr[min_idx])</span><br><span class="line">                min_idx = j;</span><br><span class="line">        <span class="comment">// 判断是否需要进行交换</span></span><br><span class="line">        <span class="keyword">if</span> (min_idx != i)</span><br><span class="line">            swap(&amp;arr[i], &amp;arr[min_idx]);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>选择排序的时间要小于冒泡排序。</p><h4 id="2、堆排序"><a href="#2、堆排序" class="headerlink" title="2、堆排序"></a>2、堆排序</h4><p>详见《<a href="https://frankfang.cn/article/202104530">堆排序</a>》。</p><h3 id="四、归并排序"><a href="#四、归并排序" class="headerlink" title="四、归并排序"></a>四、归并排序</h3><p>详见《<a href="https://frankfang.cn/article/202105939">归并排序</a>》。</p><h3 id="五、排序算法之间的比较"><a href="#五、排序算法之间的比较" class="headerlink" title="五、排序算法之间的比较"></a>五、排序算法之间的比较</h3><p><img src="https://cdn.frankfang.cn/img/0474dea1-0305-4e63-8df3-d5d8f501c97c.png" alt="1620634901341.png"></p>]]></content>
    
    
    <summary type="html">本文主要介绍几种常见的排序算法及实现原理</summary>
    
    
    
    <category term="算法" scheme="https://blog.frankfang.cn/categories/%E7%AE%97%E6%B3%95/"/>
    
    
    <category term="算法" scheme="https://blog.frankfang.cn/tags/%E7%AE%97%E6%B3%95/"/>
    
  </entry>
  
  <entry>
    <title>归并排序</title>
    <link href="https://blog.frankfang.cn/posts/202105939.html"/>
    <id>https://blog.frankfang.cn/posts/202105939.html</id>
    <published>2021-05-10T07:07:23.000Z</published>
    <updated>2021-05-10T07:07:23.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="一、基本概念"><a href="#一、基本概念" class="headerlink" title="一、基本概念"></a>一、基本概念</h3><p><strong>归并排序（Merge Sort）</strong> 是建立在归并操作上的一种有效，稳定的排序算法，该算法是采用<strong>分治法（Divide and Conquer）</strong> 的一个非常典型的应用。将已有序的子序列合并，得到完全有序的序列；即先使每个子序列有序，再使子序列段间有序。</p><p><img src="https://cdn.frankfang.cn/img/cab31cd1-f116-49e3-b944-c449b19cf295.png" alt="1620627961727.png"></p><h3 id="二、具体实现"><a href="#二、具体实现" class="headerlink" title="二、具体实现"></a>二、具体实现</h3><h4 id="1、divide"><a href="#1、divide" class="headerlink" title="1、divide"></a>1、divide</h4><p>分（divide）的过程很简单，只需不断的从中间划分序列即可。代码实现如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">mergeSort</span><span class="params">(<span class="keyword">int</span>[] arr, <span class="keyword">int</span> low, <span class="keyword">int</span> high, <span class="keyword">int</span>[] temp)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (low &lt; high) &#123;</span><br><span class="line">        <span class="comment">// 从中间划分两个子序列</span></span><br><span class="line">        <span class="keyword">int</span> mid = (low + high) / <span class="number">2</span>;</span><br><span class="line">        <span class="comment">// 对左侧子序列进行递归排序</span></span><br><span class="line">        mergeSort(arr, low, mid, temp);</span><br><span class="line">        <span class="comment">// 对右侧子序列进行递归排序</span></span><br><span class="line">        mergeSort(arr, mid + <span class="number">1</span>, high, temp);</span><br><span class="line">        <span class="comment">// 归并</span></span><br><span class="line">        merge(arr, low, mid, high, temp);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="2、conquer"><a href="#2、conquer" class="headerlink" title="2、conquer"></a>2、conquer</h4><p>在治（conquer）的阶段需要将两个已经有序的子序列合并成一个有序序列。例如上图中的最后一次合并，要将${4,5,7,8}$和${1,2,3,6}$两个已经有序的子序列，合并为最终序列${1,2,3,4,5,6,7,8}$，下面将介绍具体的实现步骤：</p><p><img src="https://cdn.frankfang.cn/img/8184d207-643f-452d-897f-468f43d85277.png" alt="1620628967724.png"></p><p><img src="https://cdn.frankfang.cn/img/cf57221a-ad8c-4c0d-8971-3b86c1ed7671.png" alt="1620629188420.png"></p><p>代码实现如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">merge</span><span class="params">(<span class="keyword">int</span>[] arr, <span class="keyword">int</span> low, <span class="keyword">int</span> mid, <span class="keyword">int</span> high, <span class="keyword">int</span>[] temp)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">int</span> i = low; <span class="comment">// 初始化i，左边有序序列的初始索引</span></span><br><span class="line">    <span class="keyword">int</span> j = mid + <span class="number">1</span>; <span class="comment">// 初始化j，右边有序序列的初始索引</span></span><br><span class="line">    <span class="keyword">int</span> t = <span class="number">0</span>; <span class="comment">// 指向辅助数组的当前索引</span></span><br><span class="line">    <span class="comment">// 1.将左右两边的数据按规则添加到辅助数组中</span></span><br><span class="line">    <span class="keyword">while</span> (i &lt;= mid &amp;&amp; j &lt;= high) &#123;</span><br><span class="line">        <span class="keyword">if</span> (arr[i] &lt; arr[j]) &#123;</span><br><span class="line">            temp[t++] = arr[i++];</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            temp[t++] = arr[j++];</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 2.将有剩余数据的一边依次添加到辅助数组中</span></span><br><span class="line">    <span class="comment">// 注意: 下面的两个while循环最多只会执行其中一个</span></span><br><span class="line">    <span class="keyword">while</span> (i &lt;= mid) &#123;</span><br><span class="line">        temp[t++] = arr[i++];</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> (j &lt;= high) &#123;</span><br><span class="line">        temp[t++] = arr[j++];</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 3.保存辅助数组的当前长度并将指针初始化</span></span><br><span class="line">    <span class="keyword">int</span> len = t;</span><br><span class="line">    t = <span class="number">0</span>;</span><br><span class="line">    <span class="comment">// 4.将辅助数组中的数据复制到原数组中</span></span><br><span class="line">    <span class="keyword">while</span> (t &lt; len) &#123;</span><br><span class="line">        arr[low + t] = temp[t];</span><br><span class="line">        t++;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>完整的实现已上传Gitee，仓库地址：<a href="https://gitee.com/fzcoder/data-structure-and-algorithms-demo">https://gitee.com/fzcoder/data-structure-and-algorithms-demo</a>。</p><h3 id="三、性能分析"><a href="#三、性能分析" class="headerlink" title="三、性能分析"></a>三、性能分析</h3><h4 id="1、时间效率"><a href="#1、时间效率" class="headerlink" title="1、时间效率"></a>1、时间效率</h4><p>每趟归并的时间复杂度为$O(n)$，共需进行$\lceil log_2n \rceil$趟归并，所以算法的时间复杂度为$O(nlog_2n)$。</p><h4 id="2、空间效率"><a href="#2、空间效率" class="headerlink" title="2、空间效率"></a>2、空间效率</h4><p>归并操作中，辅助空间的为 n 个单元，因此算法的空间复杂度为$O(n)$。</p><h4 id="3、稳定性"><a href="#3、稳定性" class="headerlink" title="3、稳定性"></a>3、稳定性</h4><p>由于归并操作不会改变相同关键字记录的相对次序，所以归并排序算法是一种稳定的排序算法。</p>]]></content>
    
    
    <summary type="html">本文主要介绍了归并排序的基本概念及实现方法</summary>
    
    
    
    <category term="算法" scheme="https://blog.frankfang.cn/categories/%E7%AE%97%E6%B3%95/"/>
    
    
    <category term="算法" scheme="https://blog.frankfang.cn/tags/%E7%AE%97%E6%B3%95/"/>
    
    <category term="分治法" scheme="https://blog.frankfang.cn/tags/%E5%88%86%E6%B2%BB%E6%B3%95/"/>
    
  </entry>
  
  <entry>
    <title>优先队列</title>
    <link href="https://blog.frankfang.cn/posts/202105042.html"/>
    <id>https://blog.frankfang.cn/posts/202105042.html</id>
    <published>2021-05-06T14:28:47.000Z</published>
    <updated>2021-05-06T14:28:47.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="一、基本概念"><a href="#一、基本概念" class="headerlink" title="一、基本概念"></a>一、基本概念</h3><p>队列是一种先进先出的数据结构，元素在队列尾追加，而从队列头删除。而在<strong>优先队列（Priority Queue）</strong> 中，元素被赋予优先级。当访问元素时，<strong>具有最高优先级的元素最先删除</strong>。优先队列具有最高级先出 （first in, largest out）的行为特征。通常采用<strong>堆</strong>这种数据结构来实现。</p><p>通常，优先队列包括<strong>最大优先队列</strong>和<strong>最小优先队列</strong>。最大优先队列可以确保每次获取到整个队列中的最大元素并将其删除，而最小优先队列则确保每次获取到整个队列中的最小元素并将其删除。</p><h3 id="二、具体实现"><a href="#二、具体实现" class="headerlink" title="二、具体实现"></a>二、具体实现</h3><h4 id="1、基本功能"><a href="#1、基本功能" class="headerlink" title="1、基本功能"></a>1、基本功能</h4><p>我们可以创建一个<code>PriorityQueue&lt;E&gt;</code>类来表示优先队列，下面将分别介绍一下该类的成员变量和公共方法：</p><ul><li><p>成员变量</p><table><thead><tr><th>成员名</th><th>类型</th><th>说明</th></tr></thead><tbody><tr><td><code>queue</code></td><td><code>Object[]</code></td><td>用于存放队列中的元素</td></tr><tr><td><code>size</code></td><td><code>int</code></td><td>当前队列中元素的个数</td></tr><tr><td><code>comparator</code></td><td><code>Comparator&lt;? super E&gt;</code></td><td>比较器，用于支持自定义比较</td></tr></tbody></table></li><li><p>公共方法</p><table><thead><tr><th>方法名及参数</th><th>返回值类型</th><th>说明</th></tr></thead><tbody><tr><td><code>isEmpty()</code></td><td><code>boolean</code></td><td>判断队列是否为空</td></tr><tr><td><code>size()</code></td><td><code>int</code></td><td>获取队列中的元素个数</td></tr><tr><td><code>offer(E e)</code></td><td><code>boolean</code></td><td>向队列中插入一个元素</td></tr><tr><td><code>poll()</code></td><td><code>E</code></td><td>获取队首元素并将其从队列中移除</td></tr><tr><td><code>peek()</code></td><td><code>E</code></td><td>获取队首元素但保留该元素</td></tr></tbody></table></li></ul><p>该部分的代码实现如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">PriorityQueue</span>&lt;<span class="title">E</span>&gt; </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 使用一个数组存放数据</span></span><br><span class="line">    <span class="keyword">private</span> Object[] queue;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 当前存放的元素数量</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">int</span> size;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 比较器，用于支持自定义比较</span></span><br><span class="line">    <span class="keyword">private</span> Comparator&lt;? <span class="keyword">super</span> E&gt; comparator;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 构造方法（默认比较器）</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">PriorityQueue</span><span class="params">(<span class="keyword">int</span> capacity)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>(capacity, <span class="keyword">null</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 构造方法（自定义比较器）</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">PriorityQueue</span><span class="params">(<span class="keyword">int</span> capacity, Comparator&lt;? <span class="keyword">super</span> E&gt; comparator)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (capacity &lt; <span class="number">1</span>) <span class="keyword">throw</span> <span class="keyword">new</span> IllegalArgumentException();</span><br><span class="line">        <span class="keyword">this</span>.queue = <span class="keyword">new</span> Object[capacity];</span><br><span class="line">        <span class="keyword">this</span>.comparator = comparator;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 判断队列是否为空</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">isEmpty</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> size == <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 获取队列中的元素个数</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">size</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> size;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 添加元素</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">offer</span><span class="params">(E e)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">int</span> i = size;</span><br><span class="line">        <span class="comment">// 当队列满时返回false</span></span><br><span class="line">        <span class="keyword">if</span> (i == queue.length) <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">        size = i + <span class="number">1</span>;</span><br><span class="line">        <span class="keyword">if</span> (i == <span class="number">0</span>)</span><br><span class="line">            queue[<span class="number">0</span>] = e;</span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">            shiftUp(i, e);</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 获取队首元素并将其从队列中移除</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> E <span class="title">poll</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 当队列为空时返回null</span></span><br><span class="line">        <span class="keyword">if</span> (size == <span class="number">0</span>) <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">        <span class="keyword">int</span> s = --size;</span><br><span class="line">        E result = (E) queue[<span class="number">0</span>];</span><br><span class="line">        E x = (E) queue[s];</span><br><span class="line">        queue[s] = <span class="keyword">null</span>;</span><br><span class="line">        <span class="keyword">if</span> (s != <span class="number">0</span>) shiftDown(<span class="number">0</span>, x);</span><br><span class="line">        <span class="keyword">return</span> result;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 获取队首元素但保留该元素</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> E <span class="title">peek</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> size == <span class="number">0</span> ? <span class="keyword">null</span> : (E) queue[<span class="number">0</span>];</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在上面的代码中使用了<code>shiftUp</code>和<code>shiftDown</code>方法，它们分别表示上移和下移，下面将重点介绍这两个方法是如何实现的。</p><h4 id="2、上移"><a href="#2、上移" class="headerlink" title="2、上移"></a>2、上移</h4><p>如果在普通的队列中插入一个元素是非常容易的，只需将其插入队尾即可。但在优先队列中，除了将其插入队列中，还要确保插入元素后整个队列仍然保持优先队列的性质。这时，我们就需要通过上移操作来实现优先队列的插入操作。下面将通过实例来介绍上移操作的具体过程。</p><p>假设有一个优先队列${3,4,5,6,8}$，现在需要插入结点2。首先将新插入的结点放在队列的末尾，如下图所示：</p><p><img src="https://cdn.frankfang.cn/img/a644651f-d91d-4ae8-b433-f4b12b69cb0e.png" alt="1620304116437.png"></p><p>之后将新插入的结点与其父结点进行比较。该例中新插入的结点所在位置的索引为 5，因此其父结点的索引为$(5-1)/2 = 2$，在索引为 2 的结点值为 5，由于该队列是最小优先队列，因此需要将新插入的结点与其父结点进行交换。</p><p><img src="https://cdn.frankfang.cn/img/678bee75-3fad-4855-b7f6-5fd6750fe74a.png" alt="1620304979325.png"></p><p>继续判断是否满足优先队列的条件。此时新插入结点的索引为 2，因此其父结点的索引为$(2-1)/2=0$，此处的值为 3，仍不满足条件，再次进行交换。</p><p><img src="https://cdn.frankfang.cn/img/44bd5586-4f71-4d1e-aad3-27ba20d79b91.png" alt="1620305309613.png"></p><p>此时满足优先队列的条件，完成插入。</p><p><img src="https://cdn.frankfang.cn/img/bef74d8c-2eea-45bb-8c57-bcaffae4a5ba.png" alt="1620307185120.png"></p><p>上移操作的代码实现如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">shiftUp</span><span class="params">(<span class="keyword">int</span> k , E e)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (comparator != <span class="keyword">null</span>) &#123;</span><br><span class="line">        <span class="keyword">while</span> (k &gt; <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="comment">// 获取待插入位置的父结点索引</span></span><br><span class="line">            <span class="keyword">int</span> parent = (k-<span class="number">1</span>) &gt;&gt;&gt; <span class="number">1</span>;</span><br><span class="line">            <span class="comment">// 获取父结点的值</span></span><br><span class="line">            Object elem = queue[parent];</span><br><span class="line">            <span class="comment">// 判断当前结点与其父结点的大小关系</span></span><br><span class="line">            <span class="comment">// 这里采用的是最小优先队列，因此当此结点大于其父结点时跳出循环</span></span><br><span class="line">            <span class="keyword">if</span> (comparator.compare(e, (E) elem) &gt;= <span class="number">0</span>)</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            queue[k] = elem;</span><br><span class="line">            k = parent;</span><br><span class="line">        &#125;</span><br><span class="line">        queue[k] = e;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        Comparable&lt;? <span class="keyword">super</span> E&gt; key =  (Comparable&lt;? <span class="keyword">super</span> E&gt;) e;</span><br><span class="line">        <span class="keyword">while</span> (k &gt; <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="comment">// 获取待插入位置的父结点索引</span></span><br><span class="line">            <span class="keyword">int</span> parent = (k-<span class="number">1</span>) &gt;&gt;&gt; <span class="number">1</span>;</span><br><span class="line">            <span class="comment">// 获取父结点的值</span></span><br><span class="line">            Object elem = queue[parent];</span><br><span class="line">            <span class="comment">// 判断当前结点与其父结点的大小关系</span></span><br><span class="line">            <span class="comment">// 这里采用的是最小优先队列，因此当此结点大于其父结点时跳出循环</span></span><br><span class="line">            <span class="keyword">if</span> (key.compareTo((E) elem) &gt;= <span class="number">0</span>)</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            queue[k] = elem;</span><br><span class="line">            k = parent;</span><br><span class="line">        &#125;</span><br><span class="line">        queue[k] = e;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="3、下移"><a href="#3、下移" class="headerlink" title="3、下移"></a>3、下移</h4><p>当删除优先队列中的元素时，首先返回队首元素，之后将队尾元素放在队首，并删除队尾。之后通过下移操作调整队列，使其满足条件。下面将通过实例来说明下移操作的完整过程。</p><p>接着上面的例子，假设需要删除 2，首先将队首元素 2 保存起来作为结果返回，并将队尾元素 5 放在队首，并删除队尾元素。</p><p><img src="https://cdn.frankfang.cn/img/53f6d8b2-de4b-4b6c-bdf4-ad950c0f913b.png" alt="1620308050501.png"></p><p>之后对队首结点的两个孩子进行比较（若右孩子存在），选择优先级最小的结点（这里是结点 3）与队首结点进行比较，因为队首元素的优先级大于结点 3，因此将这两个结点进行交换。</p><p><img src="https://cdn.frankfang.cn/img/c10ce860-a745-46c5-8836-db9183f761d0.png" alt="1620309511909.png"></p><p>由于此时原队首元素已经是叶子结点，因此下移操作结束，整个删除操作完成。</p><p><img src="https://cdn.frankfang.cn/img/01e98d92-1fe3-4c5b-8ea8-4aae09d473cd.png" alt="1620309712529.png"></p><p>下移操作的代码实现如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">shiftDown</span><span class="params">(<span class="keyword">int</span> k, E e)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (comparator != <span class="keyword">null</span>) &#123;</span><br><span class="line">        <span class="keyword">int</span> half = size &gt;&gt;&gt; <span class="number">1</span>;</span><br><span class="line">        <span class="comment">// 当为非叶子结点时进行循环</span></span><br><span class="line">        <span class="keyword">while</span> (k &lt; half) &#123;</span><br><span class="line">            <span class="comment">// 获取当前结点的左孩子</span></span><br><span class="line">            <span class="keyword">int</span> child = (k &lt;&lt; <span class="number">1</span>) + <span class="number">1</span>;</span><br><span class="line">            Object c = queue[child];</span><br><span class="line">            <span class="comment">// 当前结点的右孩子的索引（若存在）</span></span><br><span class="line">            <span class="keyword">int</span> right = child + <span class="number">1</span>;</span><br><span class="line">            <span class="comment">// 比较当前结点左孩子和右孩子（若存在）的优先级</span></span><br><span class="line">            <span class="comment">// 若右孩子的优先级小，则将待比较的结点改为右孩子</span></span><br><span class="line">            <span class="keyword">if</span> (right &lt; size &amp;&amp; comparator.compare((E) c, (E) queue[right]) &gt; <span class="number">0</span>)</span><br><span class="line">                c = queue[child = right];</span><br><span class="line">            <span class="comment">// 将当前结点与其左（或右）孩子进行比较</span></span><br><span class="line">            <span class="comment">// 若当前结点的优先级小于被比较的结点，则跳出循环</span></span><br><span class="line">            <span class="keyword">if</span> (comparator.compare(e, (E) c) &lt;= <span class="number">0</span>)</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            queue[k] = c;</span><br><span class="line">            k = child;</span><br><span class="line">        &#125;</span><br><span class="line">        queue[k] = e;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        Comparable&lt;? <span class="keyword">super</span> E&gt; key = (Comparable&lt;? <span class="keyword">super</span> E&gt;)e;</span><br><span class="line">        <span class="keyword">int</span> half = size &gt;&gt;&gt; <span class="number">1</span>;</span><br><span class="line">        <span class="comment">// 当为非叶子结点时进行循环</span></span><br><span class="line">        <span class="keyword">while</span> (k &lt; half) &#123;</span><br><span class="line">            <span class="comment">// 获取当前结点的左孩子</span></span><br><span class="line">            <span class="keyword">int</span> child = (k &lt;&lt; <span class="number">1</span>) + <span class="number">1</span>;</span><br><span class="line">            Object c = queue[child];</span><br><span class="line">            <span class="comment">// 当前结点的右孩子的索引（若存在）</span></span><br><span class="line">            <span class="keyword">int</span> right = child + <span class="number">1</span>;</span><br><span class="line">            <span class="comment">// 比较当前结点左孩子和右孩子（若存在）的优先级</span></span><br><span class="line">            <span class="comment">// 若右孩子的优先级小，则将待比较的结点改为右孩子</span></span><br><span class="line">            <span class="keyword">if</span> (right &lt; size &amp;&amp; ((Comparable&lt;? <span class="keyword">super</span> E&gt;) c).compareTo((E) queue[right]) &gt; <span class="number">0</span>)</span><br><span class="line">                c = queue[child = right];</span><br><span class="line">            <span class="comment">// 将当前结点与其左（或右）孩子进行比较</span></span><br><span class="line">            <span class="comment">// 若当前结点的优先级小于被比较的结点，则跳出循环</span></span><br><span class="line">            <span class="keyword">if</span> (key.compareTo((E) c) &lt;= <span class="number">0</span>)</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            queue[k] = c;</span><br><span class="line">            k = child;</span><br><span class="line">        &#125;</span><br><span class="line">        queue[k] = key;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>优先队列的完整实现已上传到 Gitee，仓库地址：<a href="https://gitee.com/fzcoder/data-structure-and-algorithms-demo">https://gitee.com/fzcoder/data-structure-and-algorithms-demo</a>。</p><h3 id="三、总结"><a href="#三、总结" class="headerlink" title="三、总结"></a>三、总结</h3><p>优先队列与普通的队列相似，但可以确保每次出队的元素是当前队列中的最大（或最小）元素，可使用于需要获取最大的 K 个元素等情况。</p>]]></content>
    
    
    <summary type="html">本文主要介绍了优先队列的基本概念和实现原理</summary>
    
    
    
    <category term="数据结构" scheme="https://blog.frankfang.cn/categories/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    
    <category term="数据结构" scheme="https://blog.frankfang.cn/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    <category term="队列" scheme="https://blog.frankfang.cn/tags/%E9%98%9F%E5%88%97/"/>
    
    <category term="堆" scheme="https://blog.frankfang.cn/tags/%E5%A0%86/"/>
    
  </entry>
  
  <entry>
    <title>堆排序</title>
    <link href="https://blog.frankfang.cn/posts/202104530.html"/>
    <id>https://blog.frankfang.cn/posts/202104530.html</id>
    <published>2021-04-28T12:36:28.000Z</published>
    <updated>2021-04-28T12:36:28.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="一、基本概念"><a href="#一、基本概念" class="headerlink" title="一、基本概念"></a>一、基本概念</h3><h4 id="1、堆"><a href="#1、堆" class="headerlink" title="1、堆"></a>1、堆</h4><p><strong>堆（heap）</strong> 是一个<strong>数组</strong>，它可以被看成一个近似的<strong>完全二叉树</strong>。树上的每一个结点对应数组中的一个元素，除最底层外，该树是完全充满的，而且是从左向右填充的。</p><p><img src="https://cdn.frankfang.cn/img/4334fcf4-23b2-464c-8190-89d3bddc3296.png" alt="1619596388530.png"></p><p>二叉堆可以分为两种形式：<strong>最大堆</strong>和<strong>最小堆</strong>。在这两种堆中，结点的值都要满足堆的性质，但一些细节定义则有所差异。在最大堆中，最大堆性质是指除了根以外的所有结点都要满足$parent.val \ge node.val$，也就是<strong>父结点大于等于子结点</strong>；而最小堆正好相反，即除了根以外的所有结点都要满足$parent.val \le node.val$，也就是<strong>父结点小于等于子结点</strong>。</p><h4 id="2、堆排序"><a href="#2、堆排序" class="headerlink" title="2、堆排序"></a>2、堆排序</h4><p><strong>堆排序（heap sort）</strong>，顾名思义，就是一种通过堆实现排序的算法，<strong>在堆排序中使用的是最大堆</strong>。堆排序的基本思想如下：</p><ul><li>将待排序的序列构造成一个最大堆，此时整个序列的最大值就是堆顶的根节点。</li><li>将其与末尾元素进行交换，此时末尾就为最大值。</li><li>然后将剩余$n-1$个元素重新构造成一个最大堆， 这样会得到剩余$n-1$个元素的最大值。</li><li>如此反复执行，便能得到一个有序的序列。</li></ul><p>接下来我们将介绍堆排序的具体实现。</p><h3 id="二、具体实现"><a href="#二、具体实现" class="headerlink" title="二、具体实现"></a>二、具体实现</h3><h4 id="1、建堆"><a href="#1、建堆" class="headerlink" title="1、建堆"></a>1、建堆</h4><p>首先我们需要做的就是将无需的序列建立一个初始堆（升序采用最大堆，降序采用最小堆），下面将结合实例来分析建堆的过程。</p><p>假设存在一个无序序列，该序列的二叉树形式和数组形式如下：</p><p><img src="https://cdn.frankfang.cn/img/2877406b-8d27-4bfb-8286-5716319da8f2.png" alt="1619605561083.png"></p><p>(1) 首先我们从最后一个非叶子结点开始（叶结点不需要调整，第一个非叶子结点$arr.length/2-1=5/2-1=1$,也就是结点6），从左至右，从下至上进行调整。</p><p><img src="https://cdn.frankfang.cn/img/d962207e-5f3d-47f9-bfb4-adcbea91bb9c.png" alt="1619605866673.png"></p><p>(2) 接下来找到第二个非叶子结点 4，由于在${4,9,8}$中结点 9 最大，因此将结点 4 和结点 9 交换。</p><p><img src="https://cdn.frankfang.cn/img/6bf9f8b1-4d5e-430f-9a11-d35a950024d1.png" alt="1619606037015.png"></p><p>(3) 然而上面的交换导致了子树中${4, 5,6}$不满足最大堆的要求，因此继续调整，在${4, 5,6}$中结点 6 最大，所以交换结点 4 和结点 6。此时，我们就将一个无序序列构造成了一个最大堆。</p><p><img src="https://cdn.frankfang.cn/img/47274932-6919-4c21-86f0-82d687870962.png" alt="1619606309343.png"></p><h4 id="2、调整堆"><a href="#2、调整堆" class="headerlink" title="2、调整堆"></a>2、调整堆</h4><p>在建好一个初始堆之后，需要将堆顶元素与末尾元素进行交换，使末尾元素最大。然后继续调整堆，再将堆顶元素与末尾元素交换，得到第二大元素。如此反复进行，直至将序列变成一个有序序列。下面将接着上面的例子来分析具体的过程。</p><p>(1) 首先，将堆顶元素 9 和末尾元素 4 进行交换。</p><p><img src="https://cdn.frankfang.cn/img/6aa64352-b465-45c6-86f3-fe50e2bc8be8.png" alt="1619606905687.png"></p><p>(2) 重新调整结构，使其继续满足堆定义。</p><p><img src="https://cdn.frankfang.cn/img/6a7138cf-b0f1-4538-9a39-673f6a1ca9e5.png" alt="1619607061385.png"></p><p>(3) 再将堆顶元素 8 与末元素 5 进行交换，得到第二大元素 8。</p><p><img src="https://cdn.frankfang.cn/img/7224d2f7-622a-4bab-9940-ccec00ef754c.png" alt="1619607208321.png"></p><p>(4) 继续进行调整，如此反复进行，最终使得整个序列有序。</p><p><img src="https://cdn.frankfang.cn/img/6e6f118f-9d46-4788-946d-00a74f0fb9ce.png" alt="1619607383721.png"></p><h4 id="3、代码实现"><a href="#3、代码实现" class="headerlink" title="3、代码实现"></a>3、代码实现</h4><p>堆排序采用Java语言的完整实现已上传Gitee，项目地址：<a href="https://gitee.com/fzcoder/data-structure-and-algorithms-demo">https://gitee.com/fzcoder/data-structure-and-algorithms-demo</a>，下面给出部分算法的实现。</p><p>(1) 堆排序算法</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">heapSort</span><span class="params">(<span class="keyword">int</span>[] arr)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 建堆</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">int</span> i = arr.length / <span class="number">2</span> - <span class="number">1</span>; i &gt;= <span class="number">0</span>; i--) &#123;</span><br><span class="line">        adjustHeap(arr, i, arr.length);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 反复进行调整</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">int</span> j = arr.length - <span class="number">1</span>; j &gt; <span class="number">0</span>; j--) &#123;</span><br><span class="line">        <span class="keyword">int</span> temp = arr[j];</span><br><span class="line">        arr[j] = arr[<span class="number">0</span>];</span><br><span class="line">        arr[<span class="number">0</span>] = temp;</span><br><span class="line">        adjustHeap(arr, <span class="number">0</span>, j);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>(2) 对以下标为 index 的根结点的子树进行调整</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">adjustHeap</span><span class="params">(<span class="keyword">int</span>[] arr, <span class="keyword">int</span> index, <span class="keyword">int</span> length)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">int</span> temp = arr[index]; <span class="comment">// 暂存树的根结点</span></span><br><span class="line">    <span class="keyword">for</span>(<span class="keyword">int</span> k = <span class="number">2</span> * index + <span class="number">1</span>; k &lt; length; k = k * <span class="number">2</span> + <span class="number">1</span>) &#123;</span><br><span class="line">        <span class="keyword">if</span> (k + <span class="number">1</span> &lt; length &amp;&amp; arr[k] &lt; arr[k+<span class="number">1</span>]) &#123;</span><br><span class="line">            k++; <span class="comment">// 取值较大的子结点的下标</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (arr[k] &gt; temp) &#123;</span><br><span class="line">            arr[index] = arr[k]; <span class="comment">// 将较大的子结点调整到双亲结点上</span></span><br><span class="line">            index = k; <span class="comment">// 修改 index 值，以便继续向下筛选</span></span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    arr[index] = temp; <span class="comment">// 被筛选结点的值放入最终位置</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="三、性能分析"><a href="#三、性能分析" class="headerlink" title="三、性能分析"></a>三、性能分析</h3><h4 id="1、时间效率"><a href="#1、时间效率" class="headerlink" title="1、时间效率"></a>1、时间效率</h4><p>建堆时间为$O(n)$，之后有$n-1$次向下调整操作，每次调整的时间复杂度为$O(h)$。因此在最好、最坏和平均情况下，堆排序的时间复杂度为$O(nlog_2n)$。</p><h4 id="2、空间效率"><a href="#2、空间效率" class="headerlink" title="2、空间效率"></a>2、空间效率</h4><p>仅使用了常数个辅助单元，所以空间复杂度为$O(1)$。</p><h4 id="3、稳定性"><a href="#3、稳定性" class="headerlink" title="3、稳定性"></a>3、稳定性</h4><p>进行筛选时，有可能把后面相同关键字的元素调整到前面，所以<strong>堆排序算法是一种不稳定的排序方法</strong>。</p>]]></content>
    
    
    <summary type="html">本文主要介绍了堆排序的基本概念及实现原理</summary>
    
    
    
    <category term="算法" scheme="https://blog.frankfang.cn/categories/%E7%AE%97%E6%B3%95/"/>
    
    
    <category term="算法" scheme="https://blog.frankfang.cn/tags/%E7%AE%97%E6%B3%95/"/>
    
    <category term="堆" scheme="https://blog.frankfang.cn/tags/%E5%A0%86/"/>
    
  </entry>
  
  <entry>
    <title>快速排序</title>
    <link href="https://blog.frankfang.cn/posts/202104862.html"/>
    <id>https://blog.frankfang.cn/posts/202104862.html</id>
    <published>2021-04-28T06:07:38.000Z</published>
    <updated>2021-04-28T06:07:38.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="一、基本概念"><a href="#一、基本概念" class="headerlink" title="一、基本概念"></a>一、基本概念</h3><p><strong>快速排序（Quick Sort）</strong> 的基本思想是基于分治法的，该算法通过一趟排序将要排序的数据分割成独立的两部分，其中一部分的所有数据都比另外一部分的所有数据都要小，然后再按此方法对这两部分数据分别进行快速排序，整个排序过程可以递归进行，以此达到整个数据变成有序序列。</p><p><img src="https://cdn.frankfang.cn/img/85eb7dfd-da28-4d6d-9d57-61ee6d313989.png" alt="1619426443362.png"></p><h3 id="二、具体实现"><a href="#二、具体实现" class="headerlink" title="二、具体实现"></a>二、具体实现</h3><p>由上图可知，在每一趟快速排序中，都需要选择一个元素将待排序表划分为两部分，这个用来划分待排序表的元素叫做<strong>主元（pivot element）</strong>，或者叫做基准。一趟快速排序的过程是一个交替搜索和交换的过程，下面通过实例来进行介绍。</p><h4 id="1、操作过程"><a href="#1、操作过程" class="headerlink" title="1、操作过程"></a>1、操作过程</h4><p>假设有一个待排序的数组$= {49, 38, 65, 97, 76, 13, 27}$，我们选择数组的首个元素 49 作为主元，下面是一趟快速排序的过程：</p><ul><li>指针 high 向前搜索，找到第一个小于主元49的元素27，并将其交换到 low 所指位置</li></ul><p><img src="https://cdn.frankfang.cn/img/4216f8b2-1147-4670-9144-cde922c2c400.png" alt="1619429389440.png"></p><ul><li>指针 low 向后搜索，找到第一个大于主元49的元素65，并将其交换到 high 所指位置</li></ul><p><img src="https://cdn.frankfang.cn/img/87cf19b9-4566-4575-ba98-4d4ca8e7e910.png" alt="1619429915689.png"></p><ul><li>指针 high 继续向前搜索找到第一个小于主元49的元素13，并将其交换到 low 所指位置</li></ul><p><img src="https://cdn.frankfang.cn/img/e1422428-e48a-4952-bb2b-2456a0f007a0.png" alt="1619588458616.png"></p><ul><li>指针 low 向后搜索，找到第一个大于主元49的元素97，并将其交换到 high 所指位置</li></ul><p><img src="https://cdn.frankfang.cn/img/798d0934-781e-4224-9d57-3d85d8e5df20.png" alt="1619588489530.png"></p><ul><li>指针 high 继续向前搜索，直至$low = high$，之后将主元插入到 <em>low</em> 位置即可</li></ul><p><img src="https://cdn.frankfang.cn/img/04edd524-dc5c-4351-ba3d-8a934ff5f04d.png" alt="1619588532850.png"></p><h4 id="2、代码实现"><a href="#2、代码实现" class="headerlink" title="2、代码实现"></a>2、代码实现</h4><p>由上文可知，在每趟快速排序时需要一个划分算法对待排序的列表进行划分操作，之后对每块划分的区域再进行快速排序，因此可采用递归来实现快速排序算法，代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">quickSort</span><span class="params">(<span class="keyword">int</span>[] arr, <span class="keyword">int</span> low, <span class="keyword">int</span> high)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (low &lt; high) &#123;</span><br><span class="line">        <span class="comment">// 将数组进行划分</span></span><br><span class="line">        <span class="keyword">int</span> pivot_pos = partition(arr, low, high);</span><br><span class="line">        <span class="comment">// 递归排序左子表</span></span><br><span class="line">        quickSort(arr, low, pivot_pos - <span class="number">1</span>);</span><br><span class="line">        <span class="comment">// 递归排序右子表</span></span><br><span class="line">        quickSort(arr, pivot_pos + <span class="number">1</span>, high);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中划分算法如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">int</span> <span class="title">partition</span><span class="params">(<span class="keyword">int</span>[] arr, <span class="keyword">int</span> low, <span class="keyword">int</span> high)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 将当前表中的第一个元素设为主元，对表进行划分</span></span><br><span class="line">    <span class="keyword">int</span> pivot = arr[low];</span><br><span class="line">    <span class="comment">// 当low指针小于high指针时进行划分</span></span><br><span class="line">    <span class="keyword">while</span>(low &lt; high) &#123;</span><br><span class="line">        <span class="comment">// 比较所指元素与主元元素的大小关系, 并将high指针向左移动</span></span><br><span class="line">        <span class="keyword">while</span> (low &lt; high &amp;&amp; arr[high] &gt;= pivot) --high;</span><br><span class="line">        <span class="comment">// 将比主元小的元素移动到左端</span></span><br><span class="line">        arr[low] = arr[high];</span><br><span class="line">        <span class="comment">// 比较所指元素与主元元素的大小关系, 并将low指针向右移动</span></span><br><span class="line">        <span class="keyword">while</span> (low &lt; high &amp;&amp; arr[low] &lt;= pivot) ++low;</span><br><span class="line">        <span class="comment">// 将比主元大的元素移动到右端</span></span><br><span class="line">        arr[high] = arr[low];</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 主元元素存放到最终位置</span></span><br><span class="line">    arr[low] = pivot;</span><br><span class="line">    <span class="comment">// 返回存放主元的最终位置</span></span><br><span class="line">    <span class="keyword">return</span> low;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="三、性能分析"><a href="#三、性能分析" class="headerlink" title="三、性能分析"></a>三、性能分析</h3><h4 id="1、时间效率"><a href="#1、时间效率" class="headerlink" title="1、时间效率"></a>1、时间效率</h4><p>快速排序的运行时间与划分是否对称有关。快速排序的最坏情况发生在两个区域分别包含 $n-1$ 和 0 个元素时，若每层递归都会出现这种情况，则时间复杂度为$O(n^2)$；若划分的比较平衡，此时的时间复杂度为$O(nlog_2n)$。快速排序在一般情况下与其最佳情况下的运行时间很接近。<strong>快速排序是所有内部排序算法中平均性能最优的排序算法</strong>。</p><h4 id="2、空间效率"><a href="#2、空间效率" class="headerlink" title="2、空间效率"></a>2、空间效率</h4><p>最好情况下为$O(log_2n)$，最坏情况下为$O(n)$ ，平均情况下为$O(log_2n)$ 。</p><h4 id="3、稳定性"><a href="#3、稳定性" class="headerlink" title="3、稳定性"></a>3、稳定性</h4><p>在划分算法中，若右端区域有两个关键字相同，且小于基准值的记录，则在交换到左端区间后，它们的相对位置会发生变化，即<strong>快速排序是一种不稳定的排序方法</strong>。</p>]]></content>
    
    
    <summary type="html">本文主要介绍了快速排序的基本概念和实现原理</summary>
    
    
    
    <category term="算法" scheme="https://blog.frankfang.cn/categories/%E7%AE%97%E6%B3%95/"/>
    
    
    <category term="算法" scheme="https://blog.frankfang.cn/tags/%E7%AE%97%E6%B3%95/"/>
    
    <category term="快速排序" scheme="https://blog.frankfang.cn/tags/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F/"/>
    
  </entry>
  
  <entry>
    <title>几种常见的查找算法</title>
    <link href="https://blog.frankfang.cn/posts/202104374.html"/>
    <id>https://blog.frankfang.cn/posts/202104374.html</id>
    <published>2021-04-25T14:39:28.000Z</published>
    <updated>2021-04-25T14:39:28.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="一、线性表的查找"><a href="#一、线性表的查找" class="headerlink" title="一、线性表的查找"></a>一、线性表的查找</h3><h4 id="1、顺序查找"><a href="#1、顺序查找" class="headerlink" title="1、顺序查找"></a>1、顺序查找</h4><p>顺序查找（Sequential Search）又称线性查找，它对顺序表和链表都是适用的。对于顺序表，可通过数组下标递增来顺序扫描每个元素；对于链表，可通过指针域来依次扫描每个元素。</p><ul><li>查找表的数据结构如下：</li></ul><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    ElemType *elem; <span class="comment">// 元素空间的基地址</span></span><br><span class="line">    <span class="keyword">int</span> length; <span class="comment">// 表的长度</span></span><br><span class="line">&#125;SSTable;</span><br></pre></td></tr></table></figure><ul><li>顺序查找的算法如下：</li></ul><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">seq_search</span><span class="params">(SSTable st, ElemType key)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">int</span> i;</span><br><span class="line">    st.elem[<span class="number">0</span>] = key; <span class="comment">// 哨兵</span></span><br><span class="line">    <span class="keyword">for</span>(i = st.length; st.elem[i] != key; --i); <span class="comment">// 从后向前找</span></span><br><span class="line">    <span class="keyword">return</span> i; <span class="comment">// 若表中不存在关键字为key的元素，将查找到i为0时退出for循环</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在上述算法中使用了一个“哨兵”，即<code>st.elem[0]</code>，目的是使得算法内的循环不必判断数组是否会越界。引入“哨兵”可以避免很多不必要的判断语句，从而提高程序的效率。</p><p>顺序查找的平均查找长度（Average Search Length, ASL）为：<br>$$<br>ASL = \frac{1}{n} \sum_{i=1}^{n}i = \frac{n+1}{2}<br>$$<br>时间复杂度为：<br>$$<br>T(n) = O(n)<br>$$<br>下面来说明顺序查找的优点和缺点：</p><ul><li><p>优点：算法简单，对表结构无任何要求，既适用于顺序结构，也适用于链式结构，<strong>无论记录是否按关键字有序均可应用</strong>。</p></li><li><p>缺点：<strong>平均查找长度较大，查找效率较低，所以当 n 很大时，不宜采用顺序查找</strong>。</p></li></ul><h4 id="2、折半查找"><a href="#2、折半查找" class="headerlink" title="2、折半查找"></a>2、折半查找</h4><p>详见《<a href="https://frankfang.cn/article/202104804">二分查找</a>》</p><h4 id="3、分块查找"><a href="#3、分块查找" class="headerlink" title="3、分块查找"></a>3、分块查找</h4><p><strong>分块查找（Blocking Search）</strong>又称<strong>索引顺序查找</strong>，这是一种<strong>性能介于顺序查找和折半查找之间</strong>的一种查找方法。在此查找法中，除表本身以外，尚需建立一个“索引表”，如下图所示。</p><p><img src="https://cdn.frankfang.cn/img/28f6c010-d83f-4c94-a80c-29722939b192.png" alt="1616245504498.png"></p><p>分块查找的基本思想：<strong>将查找表分为若干块</strong>。<strong>块内的元素可以无序，但块之间是有序的</strong>，即第一个块中的最大关键字小于第二个块中的所有记录的关键字，第二个块中的最大关键字小于第三个块中的所有记录的关键字，以此类推。<strong>再建一个索引表，索引表中的每个元素含有各块的最大关键字和各块中的第一个元素的地址，索引表按关键字有序排列</strong>。</p><p>分块查找的过程分为两步：第一步是<strong>在索引表中确定待查记录所在的块</strong>，可以顺序查找或折半查找索引表；第二步是<strong>在表内顺序查找</strong>。由此可见，分块查找的算法顺序为顺序查找和折半查找两种算法的简单合成。</p><p>分块查找的平均查找长度为：<br>$$<br>ASL_{bs} = L_b + L_w<br>$$<br>其中，$L_b$为查找索引表确定所在块的平均查找长度，$L_w$为在块中查找元素的平均查找长度。</p><p>若用顺序查找确定所在块，则分块查找的平均长度为：<br>$$<br>ASL_{bs} = L_b + L_w = \frac{1}{b}\sum_{j=1}^{b}j+\frac{1}{s}\sum_{i=1}^{s}i = \frac{b+1}{2}+\frac{s+1}{2} = \frac{1}{2}(\frac{n}{s}+s)+1<br>$$<br>可见，此时的平均查找长度不仅和表长 <em>n</em> 有关，而且和每一块中的记录个数 <em>s</em> 有关。</p><p>若用折半查找确定所在块，则分块查找的平均查找长度为：<br>$$<br>ASL_{bs} \approx log_2(\frac{n}{s}+1)+\frac{s}{2}<br>$$<br>下面来说明分块查找的优点和缺点：</p><ul><li><p>优点：在表中插人和删除数据元素时，只要找到该元素对应的块，就可以在该块内进行插人和删除运算。由于块内是无序的，故插人和删除比较容易，无需进行大量移动。<strong>如果线性表既要快速查找又经常动态变化，则可采用分块查找</strong>。</p></li><li><p>缺点：<strong>要增加一个索引表的存储空间并对初始索引表进行排序运算</strong>。</p></li></ul><h3 id="二、树表的查找"><a href="#二、树表的查找" class="headerlink" title="二、树表的查找"></a>二、树表的查找</h3><h4 id="1、二叉排序树"><a href="#1、二叉排序树" class="headerlink" title="1、二叉排序树"></a>1、二叉排序树</h4><p>详见《<a href="https://frankfang.cn/article/202103512">二叉排序树</a>》</p><h4 id="2、平衡二叉树"><a href="#2、平衡二叉树" class="headerlink" title="2、平衡二叉树"></a>2、平衡二叉树</h4><p>详见《<a href="https://frankfang.cn/article/202104291">平衡二叉树</a>》</p><h4 id="3、B树与B-树"><a href="#3、B树与B-树" class="headerlink" title="3、B树与B+树"></a>3、B树与B+树</h4><p>详见《<a href="https://frankfang.cn/article/202104078">B树与B+树</a>》</p><h3 id="三、散列表"><a href="#三、散列表" class="headerlink" title="三、散列表"></a>三、散列表</h3><p>详见《<a href="https://frankfang.cn/article/202104852">散列表</a>》</p>]]></content>
    
    
    <summary type="html">本文主要介绍了几种常见的查找算法</summary>
    
    
    
    <category term="算法" scheme="https://blog.frankfang.cn/categories/%E7%AE%97%E6%B3%95/"/>
    
    
    <category term="算法" scheme="https://blog.frankfang.cn/tags/%E7%AE%97%E6%B3%95/"/>
    
    <category term="树" scheme="https://blog.frankfang.cn/tags/%E6%A0%91/"/>
    
    <category term="二分查找" scheme="https://blog.frankfang.cn/tags/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/"/>
    
    <category term="散列表" scheme="https://blog.frankfang.cn/tags/%E6%95%A3%E5%88%97%E8%A1%A8/"/>
    
  </entry>
  
  <entry>
    <title>散列表</title>
    <link href="https://blog.frankfang.cn/posts/202104852.html"/>
    <id>https://blog.frankfang.cn/posts/202104852.html</id>
    <published>2021-04-25T14:30:16.000Z</published>
    <updated>2021-04-25T14:30:16.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="一、基本概念"><a href="#一、基本概念" class="headerlink" title="一、基本概念"></a>一、基本概念</h3><p><strong>散列表（Hash Table）</strong> 也叫哈希表，是根据关键码值（Key Value）而直接进行访问的数据结构。它通过把关键码值映射到表中一个位置来访问记录，以加快查找的速度，这个映射函数叫做散列函数。<strong>散列表建立了关键字和存储地址之间的一种直接映射关系</strong>。</p><p><img src="https://cdn.frankfang.cn/img/e4c40ed2-0f80-4885-95d7-051798a8082c.png" alt="1619317125702.png"></p><p>当关键字全域 U 较小时，我们可以使用一个称为<strong>直接寻址表（direct-address table）</strong>的数组表示一个取自关键字全域的动态集合，其中每个位置称之为<strong>槽（slot）</strong>，对应全域 U 中的一个关键字，槽 k 指向集合中一个关键字为 k 的元素。</p><p>但当关键字全域 U 比较大时，这样做显然是不合适的，因为这样的话直接寻址表的空间开销会很大，而且很大概率会浪费很多空间（取决于实际关键字的数量，如果数量相对于关键字全域很少，那么就会造成大量空间的浪费）。</p><p>因此，我们需要<strong>用散列函数根据关键字来计算出槽的位置</strong>。这样，散列函数 <em>h</em> 就将关键字的全域 U 映射到散列表$T[0..m-1]$的槽位上。<strong>散列表的大小 m 一般比关键字全域 U 小得多</strong>，这样就解决了当使用直接寻址表时造成的大量空间浪费的问题。但是这样做又会出现一个新的问题，假如不同的关键字的散列函数值相同，即不同的关键字被映射到同一个槽中，那么又该怎么办呢？我们称这种情形为<strong>冲突（collision）</strong>，在下文中会介绍几种处理冲突的方法。</p><h3 id="二、构造散列函数"><a href="#二、构造散列函数" class="headerlink" title="二、构造散列函数"></a>二、构造散列函数</h3><p>上文中提到散列函数通过把关键码值映射到表中一个位置来访问记录，那么该如何构造散列函数呢？下面介绍几种常见的构造方法。</p><h4 id="1、直接定址法"><a href="#1、直接定址法" class="headerlink" title="1、直接定址法"></a>1、直接定址法</h4><p>直接<strong>取关键字的某个线性函数值为散列函数</strong>，散列函数为：<br>$$<br>h(key) = a \times key + b<br>$$<br>式中 a 和 b 为常数。这种方法计算最简单，且不会产生冲突。它<strong>适合关键字分布基本连续的情况</strong>，若关键字分布不连续，空位较多，则会造成存储空间的浪费。</p><h4 id="2、除留余数法"><a href="#2、除留余数法" class="headerlink" title="2、除留余数法"></a>2、除留余数法</h4><p>假定散列表表长为 m，取一个不大于 m 但最接近或等于 m 的质数 p，利用模运算将关键字转换为散列地址，散列函数如下：<br>$$<br>h(key) = key % p<br>$$<br>该方法的关键是选择一个好的 p，使得每个关键字通过该函数转换后等概率地映射到散列空间上的任一地址，尽可能减少冲突的可能性。</p><h4 id="3、乘法散列法"><a href="#3、乘法散列法" class="headerlink" title="3、乘法散列法"></a>3、乘法散列法</h4><p>该方法分为两个步骤。第一步，用关键字 <em>key</em> 乘上常数$A(0 &lt; A &lt; 1)$，并提取$key \times A$的小数部分。第二步，用 m 乘以这个值。散列函数如下：<br>$$<br>h(key) = \lfloor m \times (key \times A - \lfloor key \times A \rfloor) \rfloor<br>$$<br>乘法散列法的一个优点就是对 m 的选择不是特别关键，一般令$m = 2^p$（p 为某个整数）。常数 <em>A</em> 建议选择为$(\sqrt5 - 1) / 2$。</p><h3 id="三、处理冲突"><a href="#三、处理冲突" class="headerlink" title="三、处理冲突"></a>三、处理冲突</h3><h4 id="1、链接法"><a href="#1、链接法" class="headerlink" title="1、链接法"></a>1、链接法</h4><p>在<strong>链接法（chaining）</strong>中，<strong>把散列到同一槽中的所有元素都放在一个链表中</strong>，如下图所示。槽 <em>j</em> 中有一个指针，它指向存储所有散列到 <em>j</em> 的元素的链表的表头；如果不存在这样的元素，则槽 <em>j</em>中为空指针。</p><p><img src="https://cdn.frankfang.cn/img/d7132a66-aa7f-4b42-96f2-7a783564f334.png" alt="1619356144872.png"></p><p>存放元素的链表可以是单链表，也可以是双向链表。图中链表画为双链，因为这样删除操作比较快。<strong>链接法适用于经常进行插入和删除的情况</strong>。</p><h4 id="2、开放寻址法"><a href="#2、开放寻址法" class="headerlink" title="2、开放寻址法"></a>2、开放寻址法</h4><p>在<strong>开放寻址法（open addressing）</strong>中，所有的元素都存放在散列表里。也就是说，每个表项或包含动态集合的一个元素，或包含空指针。当查找某个元素时，要系统地检查所有的表项，直到找到所需的元素，或者最终查明该元素不在表中。不像链接法，这里既没有链表，也没有元素存放在散列表外。因此在开放寻址法中，散列表可能会被填满，以至于不能插入任何新的元素。</p><p>开放寻址法的数学递推公式为：<br>$$<br>h_i = (h(key)+d_i) % m<br>$$<br>其中，$h(key)$为散列函数，$i \in [1, m-1]$且为整数，<em>m</em> 为散列表表长，$d_i$为增量序列。</p><p>接下来就要确定增量序列的取法，通常有以下三种取法：</p><h5 id="（1）线性探查"><a href="#（1）线性探查" class="headerlink" title="（1）线性探查"></a>（1）线性探查</h5><p>当$d_i = 0, 1, 2, …, m-1$时，称之为<strong>线性探查（linear probing）</strong>。该方法的思想是：冲突发生时，顺序查看表中下一个单元（探测到表尾时下一个探测地址是表首地址），直到找出一个空闲单元。</p><p>线性探查方法比较容易实现，但它存在着一个问题，称为一次群集（primary clustering）。 随着连续被占用的槽不断增加，平均查找时间也随之不断增加。群集现象很容易出现，这是因为当一个空槽前有 <em>i</em> 个满的槽时，该空槽为下一个将被占用的概率是$(i+ 1)/m$。连续被占用的槽就会变得越来越长，因而平均查找时间也会越来越大。</p><h5 id="（2）二次探查"><a href="#（2）二次探查" class="headerlink" title="（2）二次探查"></a>（2）二次探查</h5><p>当$d_i = 0^2, 1^2, -1^2, 2^2, -2^2, …, k^2, -k^2$，其中$k\le m/2$且 <em>m</em> 为可表示成$4k+3$的素数时，称为<strong>二次探查（quadratic probing）</strong>。</p><p>这是一种处理冲突的较好方法，但会导致一种轻度的群集，称为二次群集（secondary clustering），此外这种方法不能探测到散列表上的所有单元。</p><h5 id="（3）双重散列"><a href="#（3）双重散列" class="headerlink" title="（3）双重散列"></a>（3）双重散列</h5><p><strong>双重散列（double hashing）</strong>是用于开放寻址法的最好方法之一，因为它所产生的排列具有随机选择排列的许多特性。双重散列采用如下形式的散列函数：<br>$$<br>h(k, i) = (h_1(k) + ih_2(k)) % m<br>$$<br>其中$h_1$和$h_2$均为辅助散列函数，<em>i</em> 为冲突的次数，初始值为 0。初始探查位置为$T[h_1(k)]$，后续的探查位置是前一个位置加上偏移量$h_2(k)$模 <em>m</em>。因此，不像线性探查或二次探查，这里的探查序列以两种不同方式依赖于关键字k，因为初始探查位置、偏移量或者二者都可能发生变化。</p><h3 id="四、性能分析"><a href="#四、性能分析" class="headerlink" title="四、性能分析"></a>四、性能分析</h3><p>在分析散列表的性能之前，先来简单介绍一下装载因子的概念。装载因子记为$\alpha$，表示一个表的装满程度，即：<br>$$<br>\alpha = \frac{n}{m}<br>$$<br>其中 n 为表中记录数，m 为散列表的长度。</p><p>散列表的查找效率取决于三个因素：散列函数、处理冲突的方法和装填因子。<strong>散列表的平均查找长度依赖于装填因子，而不直接依赖于表中记录数和散列表的长度</strong>。装填因子越大，表示装填的记录越满，发生冲突的可能性就更大，反之发生冲突的可能性越小。</p>]]></content>
    
    
    <summary type="html">本文主要介绍了散列表的基本概念及实现原理</summary>
    
    
    
    <category term="数据结构" scheme="https://blog.frankfang.cn/categories/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    
    <category term="数据结构" scheme="https://blog.frankfang.cn/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    <category term="散列表" scheme="https://blog.frankfang.cn/tags/%E6%95%A3%E5%88%97%E8%A1%A8/"/>
    
  </entry>
  
  <entry>
    <title>二分查找</title>
    <link href="https://blog.frankfang.cn/posts/202104804.html"/>
    <id>https://blog.frankfang.cn/posts/202104804.html</id>
    <published>2021-04-22T11:57:01.000Z</published>
    <updated>2021-04-22T11:57:01.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="一、基本概念"><a href="#一、基本概念" class="headerlink" title="一、基本概念"></a>一、基本概念</h3><p>折半查找又称<strong>二分查找（Binary Search）</strong>，它<strong>仅适用于有序的顺序表</strong>。</p><p>折半查找的基本思想是：首先将给定的关键字与表中中间位置的元素比较，若相等，则查找成功，返回该元素的存储位置；若不等，则所需查找的元素只能在中间元素以外的前半部分或后半部分。然后在缩小的范围内继续进行同样的查找，如此重复，直到找到为止，或确定表中没有所需要查找的元素，则查找不成功，返回查找失败的信息。</p><h3 id="二、算法实现"><a href="#二、算法实现" class="headerlink" title="二、算法实现"></a>二、算法实现</h3><p>二分查找的Java语言实现如下，完整的实现已上传至Gitee，仓库地址：<a href="https://gitee.com/fzcoder/data-structure-and-algorithms-demo">https://gitee.com/fzcoder/data-structure-and-algorithms-demo</a>。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">int</span> <span class="title">binarySearch</span><span class="params">(<span class="keyword">int</span>[] arr, <span class="keyword">int</span> key)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">int</span> low = <span class="number">0</span>, high = arr.length - <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">while</span> (low &lt;= high) &#123;</span><br><span class="line">        <span class="keyword">int</span> mid = (low + high) / <span class="number">2</span>; <span class="comment">// 取中间位置</span></span><br><span class="line">        <span class="keyword">if</span> (arr[mid] == key) <span class="keyword">return</span> mid; <span class="comment">// 查找成功则返回所在位置下标</span></span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> (arr[mid] &gt; key) high = mid - <span class="number">1</span>; <span class="comment">// 从前面部分继续查找</span></span><br><span class="line">        <span class="keyword">else</span> low = mid + <span class="number">1</span>; <span class="comment">// 从后面部分继续查找</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> -<span class="number">1</span>; <span class="comment">// 查找失败，返回-1</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注：该算法循环执行的条件是<code>low &lt;= high</code>，而不是<code>low &lt; high</code>，因为<code>low=high</code>时，查找区间还有最后一个结点，还要进一步比较。</p><p>下面通过一个例子来说明折半查找算法的执行过程。</p><p>假设有一个有序表为：{ 5，16，20，27，30，36，44，55，60，67，71 }，现在要查找值为 27 的数据元素的位置，过程如下：</p><p><img src="https://cdn.frankfang.cn/img/8a5a74d3-9360-41cc-af16-4b87eff63233.png" alt="1616243140321.png"></p><ul><li>首先 mid = (1 + 11) / 2 = 6，<code>elem[mid]</code>的值为 36，36 &gt; 27，因此<code>high = mid - 1</code>，即 high = 5。</li><li>之后 mid = (1 + 5) / 2 = 3，<code>elem[mid]</code>的值为  20，20 &lt; 27，因此<code>low = mid + 1</code>，即 low = 4。</li><li>最后 mid = (4 + 5) / 2 = 4，<code>elem[mid]</code>的值为  27，27 = 27，因此已经找到位置，返回 4。</li></ul><h3 id="三、算法分析"><a href="#三、算法分析" class="headerlink" title="三、算法分析"></a>三、算法分析</h3><p>上述查找过程的判定树为：</p><p><img src="https://cdn.frankfang.cn/img/ce5f500e-6062-409d-9897-160d83d4e158.png" alt="1616243871299.png"></p><p>由此可见，<strong>折半查找法在查找成功时进行比较的关键字个数最多不超过判定树的深度</strong>。而判定树的形态只与表记录个数 n 相关，而与关键字的取值无关。因此，对于长度为 n 的有序表，折半查找法在查找成功时和给定值进行比较的关键字个数至多为：<br>$$<br>\lfloor log_2n \rfloor + 1<br>$$</p><p>现在假设如下：</p><p>$$<br>有序表的长度为n = 2^h - 1, 判定树的深度为 h = log_2(n+1)的满二叉树。假设表中每个记录的查找概率相等，均为P_i = \frac{1}{n}<br>$$</p><p>则查找成功时折半查找的平均查找长度为：<br>$$<br>ASL = \sum_{i=1}^{n} P_iC_i = \frac{1}{n}\sum_{j=1}^{h}j \cdot 2^{j-1} = \frac{n+1}{n}log_2(n+1)-1<br>$$<br>当 n 较大时，可有如下近似结果：<br>$$<br>ASL = log_2(n+1)-1<br>$$<br>因此，折半查找的时间复杂度为：<br>$$<br>T(n)=O(log_2n)<br>$$<br>下面来说明折半查找的优点和缺点：</p><ul><li><p>优点：<strong>比较次数少，查找效率高</strong>。</p></li><li><p>缺点：对表结构要求高，<strong>只能用于顺序存储的有序表</strong>。查找前需要排序，而排序本身是一种费时的运算。同时为了保持顺序表的有序性，对有序表进行插人和删除时，平均比较和移动表中一半元素，这也是一种费时的运算。因此，<strong>折半查找不适用于数据元素经常变动的线性表</strong>。</p></li></ul>]]></content>
    
    
    <summary type="html">本文主要介绍了二分查找的基本原理和实现方法</summary>
    
    
    
    <category term="算法" scheme="https://blog.frankfang.cn/categories/%E7%AE%97%E6%B3%95/"/>
    
    
    <category term="算法" scheme="https://blog.frankfang.cn/tags/%E7%AE%97%E6%B3%95/"/>
    
    <category term="二分查找" scheme="https://blog.frankfang.cn/tags/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/"/>
    
  </entry>
  
  <entry>
    <title>B树和B+树</title>
    <link href="https://blog.frankfang.cn/posts/202104078.html"/>
    <id>https://blog.frankfang.cn/posts/202104078.html</id>
    <published>2021-04-21T12:38:17.000Z</published>
    <updated>2021-04-21T12:38:17.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="一、B树"><a href="#一、B树" class="headerlink" title="一、B树"></a>一、B树</h3><h4 id="1、基本概念"><a href="#1、基本概念" class="headerlink" title="1、基本概念"></a>1、基本概念</h4><p><strong>B 树是为磁盘或其他直接存取的辅助存储设备而设计的一种平衡搜索树</strong>。B 树类似于红黑树，但它们在降低磁盘I/O操作数方面要更好一些。许多数据库系统使用 B 树或者 B 树的变种（如 B+树）来存储信息。</p><p>B 树中所有结点的孩子个数的最大值称为 B 树的阶，通常用 m 表示，一棵 m 阶 B 树或为空树，或是具有以下性质的 m 叉树：</p><ul><li>在每个结点中含有<em>n</em>个关键字$key_1, key_2, …, key_n$，以非降序存放，使得$key_1\le key_2\le …\le key_n$。</li><li>关键字$key_i$对存储在各子树中的关键字范围加以分割，如果$k_i$为任意一个存储在以当前结点为根结点的子树中的关键字，则有：$k_1\le key_1\le k_2\le key_2\le …\le k_n\le key_n\le k_{n+1}$。</li><li>每个叶子结点具有相同的深度。</li><li>每个结点所包含的关键字个数有上界和下界。除了根结点以外的所有非叶子结点至少有$\lceil m/2 \rceil - 1$个关键字，至多有$m-1$个关键字。</li></ul><p>由上述性质的第四点可知，除了根结点以外的每个内部结点至少有$\lceil m/2 \rceil $个孩子，如果树非空，根结点至少有一个关键字。一个内部结点至多可有 m 个孩子。</p><p><img src="https://cdn.frankfang.cn/img/4ac58607-9536-4307-a65f-a14d834d6993.png" alt="1618997277937.png"></p><h4 id="2、B树的高度"><a href="#2、B树的高度" class="headerlink" title="2、B树的高度"></a>2、B树的高度</h4><p>B 树上大部分的操作所需的磁盘存取次数与 B 树的高度是成正比的。如果$n \ge 1$，那么对任意一棵包含 n 个关键字、高度为 h、阶数为 m 的 B 树 T，有：<br>$$<br>log_m(n+1) \le h \le log_{\lceil m/2 \rceil}((n+1)/2) + 1<br>$$</p><h4 id="3、查找操作"><a href="#3、查找操作" class="headerlink" title="3、查找操作"></a>3、查找操作</h4><p>在 B 树上进行查找与二叉搜索树类似，只是每个结点都是多个关键字的有序表，在每个结点上所做的不是两路分支决定，而是根据该结点的子树所做的多路分支决定。</p><p>B 树上的查找包含以下两个基本操作：</p><ul><li>在B树中查找结点。此操作是在磁盘上进行的，当查找到目标结点后，就把该结点的信息加载进内存。</li><li>在结点内查找关键字。此操作是在内存中进行的，对结点内的关键字列表进行二分查找或顺序查找。</li></ul><p>下面通过一个例子来说明 B 树的查找操作。假设需要在上图中的B树上查找是否存在关键字42，查找的具体步骤如下：</p><p><img src="https://cdn.frankfang.cn/img/a15b4059-cb7f-4ad0-a4b6-3fa01f8c89ab.png" alt="1618989491051.png"></p><p><img src="https://cdn.frankfang.cn/img/4d405943-ff29-4698-9b21-57f362962029.png" alt="1618989733659.png"></p><p><img src="https://cdn.frankfang.cn/img/4e64b9f6-95c6-46b6-aafc-fe85c2a6ae59.png" alt="1618989945941.png"></p><p>注：当查找到叶子结点（对应指针为空指针），则说明树中没有对应的关键字，查找失败。</p><h4 id="4、插入操作"><a href="#4、插入操作" class="headerlink" title="4、插入操作"></a>4、插入操作</h4><p>下面通过一个具体的实例来总结B树插入操作的方法。（设该B树的阶数 m = 5 ）</p><ul><li>首先在空树中插入39。</li></ul><p><img src="https://cdn.frankfang.cn/img/54b832fc-0142-43d2-b750-e14949de7384.png" alt="1619000208595.png"></p><ul><li>继续插入22、97和41。由于结点中的关键字个数没有到达其最大值，因此可以直接插入。另外，B树中每个结点的关键字都是升序排列的，因此将22插在39的前面，将41和97插在39的后面。</li></ul><p><img src="https://cdn.frankfang.cn/img/284d771c-17f5-4411-81f7-21b85dad22f6.png" alt="1619000254960.png"></p><ul><li>继续插入53。注意，由于该B树的阶为5，因此每个结点最多只能有$5-1=4$个关键字，此时该结点已经有4个关键字了（一般称这种结点为<strong>满的（full）</strong>结点），因此要对该结点进行<strong>分裂（split）</strong>操作。该操作将一个满的结点按其<strong>中间关键字（median key）</strong>（位于$\lceil m/2 \rceil $处）分为两部分，<strong>左部分放在原结点处，右部分放在新结点处，中间位置的结点插入原结点的父结点</strong>（如果没有父结点就创建一个新结点）。但是如果父结点也是满的，就必须在插人新的关键字之前将其分裂，最终<strong>满结点的分裂会沿着树向上传播</strong>。</li></ul><p><img src="https://cdn.frankfang.cn/img/72699881-453a-4e8b-99f2-5cb30996a40a.png" alt="1619001602294.png"></p><h4 id="5、删除操作"><a href="#5、删除操作" class="headerlink" title="5、删除操作"></a>5、删除操作</h4><p>B 树中的删除操作与插入操作类似，但要稍微复杂一些，即要使得删除后的结点中的关键字个数$\ge \lceil m/2 \rceil - 1$，因此将涉及结点的合并问题。下面通过具体的实例来总结B树的删除操作。</p><p>（1）当被删除的关键字 k 不在终端结点（最低层非叶子结点）中时，可以**用 k 的前驱（或后继）k’ 来替代 k，然后在相应的结点中删除 k’**。</p><p><img src="https://cdn.frankfang.cn/img/995f4afc-9a3c-4b08-9aea-f64077f14e21.png" alt="1619004369525.png"></p><p>在上图中，首先删除关键字80（用其前驱78替代），然后在终端结点中删除78，因此只需讨论删除终端结点中关键字的情形。</p><p>（2）当被删除的关键字在终端结点中时，分下列三种情况讨论：</p><ul><li><p>若被删除关键字所在的结点关键字个数$\ge \lceil m/2 \rceil $，表明删除该关键字后仍满足B树的定义，则可直接删除该关键字。</p></li><li><p>若被删除关键字所在结点删除前的关键字个数$= \lceil m/2 \rceil - 1$,且与此结点相邻的右（或左）兄弟结点的关键字个数$\ge \lceil m/2 \rceil $，则需要调整该结点、右（或左）兄弟结点及其双亲结点，以达到新的平衡。</p><p><img src="https://cdn.frankfang.cn/img/1fd75fb7-a3c2-4348-bd7e-5cdb898405d9.png" alt="1619005284094.png"></p><p>如上图所示，当删除关键字65时，其右兄弟关键字个数$\ge \lceil m/2 \rceil $，因此将65的父结点71替换65，再用右兄弟结点中的关键字74替换父结点，最后直接删除右兄弟中的结点74。</p></li><li><p>若被删除关键字所在结点删除前的关键字个数$= \lceil m/2 \rceil - 1$,且与此结点相邻的左、右兄弟结点的关键字个数均$= \lceil m/2 \rceil - 1$，则将关键字删除后与左（或右）兄弟结点及双亲结点中的关键字进行合并。</p><p><img src="https://cdn.frankfang.cn/img/447e416c-8d3b-4cc8-b3b6-b806f7fe7a70.png" alt="1619005962891.png"></p><p>由上图所示，当删除结点5时，其右兄弟结点的关键字个数均$= \lceil m/2 \rceil - 1$，因此先删除5，然后将60合并到65结点中。</p></li></ul><p>注：在合并过程中，双亲结点中的关键字个数会减1。若其双亲结点是根结点且关键字个数至0，则直接删除根结点，合并后的新结点为根。若其双亲结点不是根结点，且关键字个数减少到$\lceil m/2 \rceil - 2$，则又要与它自己的兄弟结点进行调整或合并，直到符合 B 树的要求为止。</p><h3 id="二、B-树"><a href="#二、B-树" class="headerlink" title="二、B+树"></a>二、B+树</h3><h4 id="1、基本概念-1"><a href="#1、基本概念-1" class="headerlink" title="1、基本概念"></a>1、基本概念</h4><p>B+树是应数据库所需而出现的一种B树的变形树，它与B树的差异在于：</p><ul><li>非叶结点仅具有索引作用，也就是说，非叶子结点只存储key，不存储value 。</li><li>树的所有叶结点构成一个有序链表，可以按照key排序的次序遍历全部数据。</li></ul><p>一颗典型的B+树如下图所示：</p><p><img src="https://cdn.frankfang.cn/img/80295b7c-2d13-4b7f-8219-19bb33ac3883.png" alt="1619006728980.png"></p><h4 id="2、相关操作"><a href="#2、相关操作" class="headerlink" title="2、相关操作"></a>2、相关操作</h4><p>B+树的查找、插入和删除操作和 B 树的基本类似。只是在查找的过程中，非叶结点上的关键字值等于给定值时并不终止，而是继续向下查找，直到叶结点上的该关键字为止。所以，在B+树中查找时，无论查找是否成功，每次查找都是一条从根结点到叶结点的路径。</p><h3 id="三、B树和B-树的对比"><a href="#三、B树和B-树的对比" class="headerlink" title="三、B树和B+树的对比"></a>三、B树和B+树的对比</h3><p>B+树的优点在于:</p><ul><li><p>由于B+树在非叶子结点上不包含真正的数据，只当做索引使用，因此在内存相同的情况下，能够存放更多的key。</p></li><li><p>B+树的叶子结点都是相连的，因此对整棵树的遍历只需要一次线性遍历叶子结点即可。 而且由于数据顺序排列并且相连，所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。</p></li></ul><p>B树的优点在于:</p><p>由于B树的每-一个节点都包含key和value，因此我们根据key查找value时，只需要找到key所在的位置，就能找到value，但B+树只有叶子结点存储数据，索引每一次查找，都必须一直找到树的最大深度处，也就是叶子结点的深度，才能找到value。</p>]]></content>
    
    
    <summary type="html">本文主要介绍了B树和B+树的基本概念和相关操作的实现原理</summary>
    
    
    
    <category term="数据结构" scheme="https://blog.frankfang.cn/categories/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    
    <category term="数据结构" scheme="https://blog.frankfang.cn/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    <category term="树" scheme="https://blog.frankfang.cn/tags/%E6%A0%91/"/>
    
    <category term="B树" scheme="https://blog.frankfang.cn/tags/B%E6%A0%91/"/>
    
  </entry>
  
  <entry>
    <title>2-3树</title>
    <link href="https://blog.frankfang.cn/posts/202104535.html"/>
    <id>https://blog.frankfang.cn/posts/202104535.html</id>
    <published>2021-04-20T12:25:31.000Z</published>
    <updated>2021-04-20T12:25:31.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="一、基本概念"><a href="#一、基本概念" class="headerlink" title="一、基本概念"></a>一、基本概念</h3><p>2-3树是最简单的 B 树结构，其每个非叶节点都有两个或三个子女，而且所有叶子结点都在同一层上。2-3树不是二叉树，其节点可拥有3个孩子。</p><p>在2-3树中包含<strong>2-结点</strong>和<strong>3-结点</strong>，下面分别对这两个结点进行解释：</p><ul><li><strong>2-结点</strong>：含有一个键（及其对应的值）和两条链，左链接指向2-3树中的键都小于该结点，右链接指向的2-3树中的键都大于该结点。</li><li><strong>3-结点</strong>：含有两个键（及其对应的值）和三条链，左链接指向的2-3树中的键都小于该结点，中链接指向的2-3树中的键都位于该结点的两个键之间，右链接指向的2-3树中的键都大于该结点。</li></ul><p><img src="https://cdn.frankfang.cn/img/c6ec05b3-66af-4856-ab04-70b5ee300976.png" alt="1618815439532.png"></p><h3 id="二、查找"><a href="#二、查找" class="headerlink" title="二、查找"></a>二、查找</h3><p>将二叉搜索树的查找算法一般化我们就能够直接得到2-3树的查找算法。 要判断一个键是否在树中，我们先将它和<br>根结点中的键比较。如果它和其中任意一个相等，查找命中；否则我们就根据比较的结果找到指向相应区间的连<br>接，并在其指向的子树中递归地继续查找。如果这个是空链接，查找未命中。</p><p>例如在上图中的2-3树中查找结点28，下面给出查找过程：</p><p><img src="https://cdn.frankfang.cn/img/9763ce60-aea7-475a-8b53-62c68f1c64f6.png" alt="1618817572384.png"></p><p><img src="https://cdn.frankfang.cn/img/9c2ba7f8-7ade-4f6e-8a3c-21794541837a.png" alt="1618817961773.png"></p><p><img src="https://cdn.frankfang.cn/img/887a8ba6-16af-479c-b877-ae0f3577fa00.png" alt="1618818137391.png"></p><h3 id="三、插入"><a href="#三、插入" class="headerlink" title="三、插入"></a>三、插入</h3><h4 id="1、向2-结点插入新结点"><a href="#1、向2-结点插入新结点" class="headerlink" title="1、向2-结点插入新结点"></a>1、向2-结点插入新结点</h4><p>往2-3树中插入元素和往二叉搜索树中插入元素一样，首先要进行查找，然后将节点挂到未找到的结点上。如果查找后未找到的结点是一个2-结点，那么我们只需要将新的元素放到这个2-结点中使其变成一个3-结点即可。</p><p><img src="https://cdn.frankfang.cn/img/3b2681a7-3db7-40fd-ba23-474480b7850a.png" alt="1618822990595.png"></p><p><img src="https://cdn.frankfang.cn/img/f760cdc6-348e-4215-857c-e00aecf65931.png" alt="1618823266816.png"></p><h4 id="2、向一颗只有3-结点的树插入新结点"><a href="#2、向一颗只有3-结点的树插入新结点" class="headerlink" title="2、向一颗只有3-结点的树插入新结点"></a>2、向一颗只有3-结点的树插入新结点</h4><p>假设2-3树只包含一个3-结点，这个结点有两个键，没有空间来插入第三个键了，最自然的方式是我们假设这个结点能存放三个元素，暂时使其变成一个4-结点，同时他包含四条链接。然后,我们将这个4-结点的中间元素提升，左边的键作为其左子结点，右边的键作为其右子结点。插入完成，变为平衡2-3查找树，树的高度从0变为1。</p><p><img src="https://cdn.frankfang.cn/img/2503da4f-fd8e-438e-a337-e7dda961d983.png" alt="1618823481882.png"></p><h4 id="3、向一个父结点为2-结点的3-结点插入新结点"><a href="#3、向一个父结点为2-结点的3-结点插入新结点" class="headerlink" title="3、向一个父结点为2-结点的3-结点插入新结点"></a>3、向一个父结点为2-结点的3-结点插入新结点</h4><p>和上面的情况一样，我们也可以将新的元素插入到3-结点中，使其成为一个临时的4-结点，然后将该结点中的中间元素提升到父结点即2-结点中，使父结点成为一个3-结点，然后将左右结点分别挂在这个3-结点的恰当位置。</p><p><img src="https://cdn.frankfang.cn/img/abfdc60c-a385-45ed-ab16-02efc42a68e8.png" alt="1618823821539.png"></p><p><img src="https://cdn.frankfang.cn/img/a38b26c1-0562-40e9-8283-1d9213ab8829.png" alt="1618824005815.png"></p><p><img src="https://cdn.frankfang.cn/img/337a0a84-1eeb-4134-aaef-4db3c3f61fff.png" alt="1618824177746.png"></p><h4 id="4、向一个父结点为3-结点的3-结点插入新结点"><a href="#4、向一个父结点为3-结点的3-结点插入新结点" class="headerlink" title="4、向一个父结点为3-结点的3-结点插入新结点"></a>4、向一个父结点为3-结点的3-结点插入新结点</h4><p>当我们插入的结点是3-结点的时候，我们将该结点拆分，中间元素提升至父结点，但是此时父结点是一个3-结点，插入之后，父结点变成了4-结点，然后继续将中间元素提升至其父结点，直至遇到一个父结点是2-结点，然后将其为3-结点，不需要继续进行拆分。</p><p><img src="https://cdn.frankfang.cn/img/8eb457ad-914d-4155-a198-eb1d14792336.png" alt="1618824421287.png"></p><p><img src="https://cdn.frankfang.cn/img/f8bd9a7a-3e73-4e0b-beb7-f7440e1377e7.png" alt="1618827930560.png"></p><p><img src="https://cdn.frankfang.cn/img/2aee37f8-b52f-4b2c-8c5b-e91d351911dc.png" alt="1618828226504.png"></p><p><img src="https://cdn.frankfang.cn/img/66729663-ecc8-4a11-b5f1-ba83ef335af2.png" alt="1618829331362.png"></p><h4 id="5、分解根结点"><a href="#5、分解根结点" class="headerlink" title="5、分解根结点"></a>5、分解根结点</h4><p>当插入结点到根结点的路径上全部是3-结点的时候，最终我们的根结点会编程一个临时的4-结点，此时，就需要将根结点拆分为两个2-结点，树的高度加1。</p><p><img src="https://cdn.frankfang.cn/img/e702e797-783d-49d6-ab21-c6c537f68644.png" alt="1618830309708.png"></p><p><img src="https://cdn.frankfang.cn/img/e55b5473-4d31-410c-bcff-7f6d193d2dc6.png" alt="1618830522165.png"></p><p><img src="https://cdn.frankfang.cn/img/242a809d-a45f-49c2-a2b1-0f4d7b0519c3.png" alt="1618830687703.png"></p><h3 id="四、二叉树的性质"><a href="#四、二叉树的性质" class="headerlink" title="四、二叉树的性质"></a>四、二叉树的性质</h3><p>通过对2-3树插入操作的分析，我们发现在插入的时候，2-3树需要做一些局部的变换来保持2-3树的平衡。一棵完全平衡的2-3树具有以下性质：</p><ol><li>任意空链接到根结点的路径长度都是相等的。</li><li>4-结点变换为3-结点时,树的高度不会发生变化,只有当根结点是临时的4结点,分解根结点时,树高+1。</li><li>2-3树与普通二叉查找树最大的区别在于,普通的二叉查找树是自顶向下生长,而2-3树是自底向_性长。</li></ol><p>2-3查找树实现起来比较复杂，在某些情况插入后的平衡操作可能会使得效率降低。但是2-3查找树作为一种比较重要的概念对于B树和B+树来说非常重要。</p>]]></content>
    
    
    <summary type="html">本文主要介绍2-3树的基本概念及基本操作</summary>
    
    
    
    <category term="数据结构" scheme="https://blog.frankfang.cn/categories/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    
    <category term="数据结构" scheme="https://blog.frankfang.cn/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    <category term="树" scheme="https://blog.frankfang.cn/tags/%E6%A0%91/"/>
    
    <category term="B树" scheme="https://blog.frankfang.cn/tags/B%E6%A0%91/"/>
    
  </entry>
  
  <entry>
    <title>红黑树</title>
    <link href="https://blog.frankfang.cn/posts/202104492.html"/>
    <id>https://blog.frankfang.cn/posts/202104492.html</id>
    <published>2021-04-20T12:19:36.000Z</published>
    <updated>2021-04-20T12:19:36.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="一、基本概念"><a href="#一、基本概念" class="headerlink" title="一、基本概念"></a>一、基本概念</h3><p><strong>红黑树（red-black tree）</strong> 是一棵二叉搜索树，它在每个结点上增加了一个存储位来表示结点的颜色，可以是<em>RED</em>或<em>BLACK</em>。通过对任何一条从根到叶子的简单路径上各个结点的颜色进行约束，<strong>红黑树确保没有一条路径会比其他路径长出2倍</strong>，因而是近似于平衡的。</p><p>红黑树具有以下性质：</p><ul><li>每个结点或是红色的，或是黑色的。</li><li><strong>根结点是黑色的</strong>。</li><li><strong>每个叶结点(NIL)是黑色的</strong>。</li><li>如果<strong>一个结点是红色的</strong>，则它的<strong>两个子结点都是黑色的</strong>。</li><li>对每个结点，从该结点到其所有后代叶结点的简单路径上，均包含相同数目的黑色结点。</li></ul><p>一颗典型的红黑树如下图所示（其中的NIL可以理解为空指针）：</p><p><img src="https://cdn.frankfang.cn/img/7bc6099d-6b96-4b8b-b851-c40cfdde116b.png" alt="1618913549400.png"></p><p>一棵有 n 个内部结点的红黑树的高度至多为$2log_2(n+1)$。关于该结论的证明，可以参阅《算法导论》中的相关证明，这里不再进行赘述。</p><h3 id="二、旋转"><a href="#二、旋转" class="headerlink" title="二、旋转"></a>二、旋转</h3><p>指针结构的修改是通过<strong>旋转（rotation）</strong>来完成的，这是一种<strong>能保持二叉搜索树性质</strong>的搜索树局部操作。下图给出了两种旋转：左旋和右旋。</p><p><img src="https://cdn.frankfang.cn/img/87a0456e-209e-4c06-a725-477e0d76f355.png" alt="1618915142702.png"></p><p>下面来简单介绍一下左旋和右旋是如何实现的：</p><h4 id="1、左旋"><a href="#1、左旋" class="headerlink" title="1、左旋"></a>1、左旋</h4><p><img src="https://cdn.frankfang.cn/img/801bdc3e-c7ae-4079-a399-98ef9088f0be.png" alt="1618915836461.png"></p><h4 id="2、右旋"><a href="#2、右旋" class="headerlink" title="2、右旋"></a>2、右旋</h4><p><img src="https://cdn.frankfang.cn/img/ebba499e-dad1-4bc3-9e87-e92b0900b129.png" alt="1618916198071.png"></p><h3 id="三、插入"><a href="#三、插入" class="headerlink" title="三、插入"></a>三、插入</h3><p>这里先挖个坑，有时间再回来填，这里先推荐几个介绍红黑树的文章和视频：</p><p><a href="https://zhuanlan.zhihu.com/p/79980618">https://zhuanlan.zhihu.com/p/79980618</a></p><p><a href="https://zhuanlan.zhihu.com/p/91960960">https://zhuanlan.zhihu.com/p/91960960</a></p><p><a href="https://www.bilibili.com/video/BV1iJ411E7xW?p=124">https://www.bilibili.com/video/BV1iJ411E7xW?p=124</a></p><h3 id="四、删除"><a href="#四、删除" class="headerlink" title="四、删除"></a>四、删除</h3><p>这里先挖个坑，有时间再回来填。</p>]]></content>
    
    
    <summary type="html">本文主要介绍红黑树的基本概念即相关操作的实现原理</summary>
    
    
    
    <category term="数据结构" scheme="https://blog.frankfang.cn/categories/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    
    <category term="数据结构" scheme="https://blog.frankfang.cn/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    <category term="树" scheme="https://blog.frankfang.cn/tags/%E6%A0%91/"/>
    
    <category term="二叉树" scheme="https://blog.frankfang.cn/tags/%E4%BA%8C%E5%8F%89%E6%A0%91/"/>
    
    <category term="红黑树" scheme="https://blog.frankfang.cn/tags/%E7%BA%A2%E9%BB%91%E6%A0%91/"/>
    
  </entry>
  
  <entry>
    <title>平衡二叉树</title>
    <link href="https://blog.frankfang.cn/posts/202104291.html"/>
    <id>https://blog.frankfang.cn/posts/202104291.html</id>
    <published>2021-04-18T03:18:27.000Z</published>
    <updated>2021-04-18T03:18:27.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="一、基本介绍"><a href="#一、基本介绍" class="headerlink" title="一、基本介绍"></a>一、基本介绍</h3><p>为避免树的高度增长过快，降低二叉排序树的性能，规定在插入和删除二叉树的结点时要保证任意结点的左、右子树的高度差的绝对值不超过 1，将这样的二叉排序树称为<strong>平衡二叉树（Balanced Binary Tree）</strong>，又称AVL树。</p><p>定义结点左子树与右子树的高度差为该结点的<strong>平衡因子</strong>，则平衡二叉树结点的平衡因子的值只可能是 -1、0 或 1。</p><h3 id="二、插入操作"><a href="#二、插入操作" class="headerlink" title="二、插入操作"></a>二、插入操作</h3><p>二叉排序树保证平衡的基本思想为：每当在二叉排序树中插入（或删除）一个结点时，首先检查其插入路径上的结点是否因为此次操作而导致了不平衡。若导致了不平衡，则先<strong>找到插入路径上离插入结点最近的平衡因子的绝对值大于 1 的结点 N，再对以 N 为根的子树，在保持二叉排序树特性的前提下，调整各结点的位置关系，使之重新达到平衡</strong>。</p><p>平衡二叉树的调整存在<strong>LL平衡旋转（右单旋转）</strong>、<strong>RR平衡旋转（左单旋转）</strong>、<strong>LR平衡旋转（先左后右双旋转）</strong>、<strong>RL平衡旋转（先右后左双旋转）</strong>这4种情况，下面将分别介绍这几种情况下的调整方法。</p><h4 id="（1）LL平衡旋转"><a href="#（1）LL平衡旋转" class="headerlink" title="（1）LL平衡旋转"></a>（1）LL平衡旋转</h4><p>假设有一颗平衡二叉树，当该二叉树插入一个结点 6 时，根节点的平衡因子的绝对值大于 1，此时该二叉树不再是一颗平衡二叉树，如下图所示：</p><p><img src="https://cdn.frankfang.cn/img/da264357-307b-45b9-8488-50a7fb878623.png" alt="1618659154970.png"></p><p>可以看到，由于在<em>结点10</em>的<strong>左孩子</strong>（<em>结点8</em>）的<strong>左子树</strong>上插入了一个新结点（<em>结点6</em>），<em>结点10</em>的平衡因子由 1 增至 2，导致以<em>结点10</em>为根的子树失去平衡，因此需要一次<strong>向右</strong>的旋转操作。</p><p>【操作步骤】</p><ul><li>首先创建一个新结点，并将新结点的值设置为根结点的值</li></ul><p><img src="https://cdn.frankfang.cn/img/73c2ae4b-2766-466e-b205-aa4821d66b85.png" alt="1618660328691.png"></p><ul><li>之后将新结点的左孩子设置为根结点的左孩子的右子树，将新结点的右孩子设置为根结点的右孩子</li></ul><p><img src="https://cdn.frankfang.cn/img/4ea6efa5-db39-4d51-82e0-0670cdd56739.png" alt="1618661011041.png"></p><ul><li>然后将根结点的值设置为其左孩子的值，并将根结点的右孩子设置为新结点</li></ul><p><img src="https://cdn.frankfang.cn/img/7fe9e7a4-2b2d-4b2d-bc58-bf76fa213806.png" alt="1618661622032.png"></p><ul><li>最后将根结点的左孩子设置为其左孩子的左子树</li></ul><p><img src="https://cdn.frankfang.cn/img/34e6ecc8-50ca-471e-ab75-2c7b0efebdb8.png" alt="1618661958376.png"></p><p>【实现效果】</p><p><img src="https://cdn.frankfang.cn/img/0fcb8a3a-f454-4cdf-8c15-4ee7720ba424.png" alt="1618662354361.png"></p><p>可以看到，此时的根结点的值为8，原根结点10移动到现根结点8的右侧，相当于进行了一次向右旋转。</p><p>【代码实现】</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// LL平衡旋转</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">rightRotate</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 首先创建一个新结点，并将新结点的值设置为根结点的值</span></span><br><span class="line">    TreeNode newNode = <span class="keyword">new</span> TreeNode(val);</span><br><span class="line">    <span class="comment">// 将新结点的左孩子设置为根结点的左孩子的右子树</span></span><br><span class="line">    newNode.left = left.right;</span><br><span class="line">    <span class="comment">// 将新结点的右孩子设置为根结点的右孩子</span></span><br><span class="line">    newNode.right = right;</span><br><span class="line">    <span class="comment">// 将根结点的值设置为其左孩子的值</span></span><br><span class="line">    val = left.val;</span><br><span class="line">    <span class="comment">// 将根结点的右孩子设置为新结点</span></span><br><span class="line">    right = newNode;</span><br><span class="line">    <span class="comment">// 将根结点的左孩子设置为其左孩子的左子树</span></span><br><span class="line">    left = left.left;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="（2）RR平衡旋转"><a href="#（2）RR平衡旋转" class="headerlink" title="（2）RR平衡旋转"></a>（2）RR平衡旋转</h4><p>假设有一颗平衡二叉树，当该二叉树插入一个结点 8 时，根节点的平衡因子的绝对值大于 1，此时该二叉树不再是一颗平衡二叉树，如下图所示：</p><p><img src="https://cdn.frankfang.cn/img/87836c5c-e563-4a7c-a2d8-8bee235210a5.png" alt="1618666899776.png"></p><p>可以看到，由于在<em>结点4</em>的<strong>右孩子</strong>（<em>结点6</em>）的<strong>右子树</strong>上插入了一个新结点（<em>结点8</em>），<em>结点4</em>的平衡因子由 -1 减至 -2，导致以<em>结点4</em>为根的子树失去平衡，因此需要一次<strong>向左</strong>的旋转操作。</p><p>【操作步骤】</p><ul><li>创建新的结点，并将新结点的值设置为根结点的值</li></ul><p><img src="https://cdn.frankfang.cn/img/df068d14-1772-4ab8-b07b-3d0c5fb7adbd.png" alt="1618667318244.png"></p><ul><li>将新的结点的左孩子设置成根结点的左孩子，并将新的结点的右孩子设置成根结点的右孩子的左子树</li></ul><p><img src="https://cdn.frankfang.cn/img/3a9c0334-8938-4ae2-bea6-345a172a5b32.png" alt="1618667637914.png"></p><ul><li>将根结点的值替换成右孩子的值，并将根结点的左孩子设置成新的结点</li></ul><p><img src="https://cdn.frankfang.cn/img/fce68336-67b3-408b-b815-333c61c1250b.png" alt="1618667727929.png"></p><ul><li>将根结点的右孩子设置成右孩子的右子树</li></ul><p><img src="https://cdn.frankfang.cn/img/7ba4965b-ce68-47aa-b3b6-9a2a4305da6e.png" alt="1618667894411.png"></p><p>【实现效果】</p><p><img src="https://cdn.frankfang.cn/img/c6e15f20-26c6-456c-9bea-6dc8a0c77929.png" alt="1618668058251.png"></p><p>可以看到，此时的根结点的值为6，原根结点4移动到现根结点6的左侧，相当于进行了一次向左旋转。</p><p>【实现代码】</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// RR平衡旋转</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">leftRotate</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 创建新的结点，并将新结点的值设置为根结点的值</span></span><br><span class="line">    TreeNode newNode = <span class="keyword">new</span> TreeNode(val);</span><br><span class="line">    <span class="comment">// 把新的结点的左孩子设置成当前结点的左孩子</span></span><br><span class="line">    newNode.left = left;</span><br><span class="line">    <span class="comment">// 把新的结点的右孩子设置成当前结点的右孩子的左子树</span></span><br><span class="line">    newNode.right = right.left;</span><br><span class="line">    <span class="comment">// 把当前结点的值替换成右孩子的值</span></span><br><span class="line">    val = right.val;</span><br><span class="line">    <span class="comment">// 把当前结点的左孩子设置成新的结点</span></span><br><span class="line">    left = newNode;</span><br><span class="line">    <span class="comment">// 把当前结点的右孩子设置成右孩子的右子树</span></span><br><span class="line">    right = right.right;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="（3）LR平衡旋转"><a href="#（3）LR平衡旋转" class="headerlink" title="（3）LR平衡旋转"></a>（3）LR平衡旋转</h4><p>假设有一颗平衡二叉树，如下图(a)所示，当插入<em>结点9</em>后根结点的平衡因子为2，因此需要进行调整。如果我们直接进行RR平衡旋转，得到的结果如下图(b)所示，此时根结点的平衡因子为-2，仍然不是一颗平衡二叉树。</p><p><img src="https://cdn.frankfang.cn/img/0f5988b1-3129-4abe-9443-cae85e3e3830.png" alt="1618669047014.png"></p><p>那么该如何调整上面的二叉树，使其成为一颗平衡二叉树呢？</p><p>首先通过观察，我们可以发现上图(a)中根结点的<strong>左孩子</strong>的<strong>右子树</strong>高度大于根结点的左孩子的左子树的高度，对于这种情况，我们可以<strong>先对根结点的左孩子进行RR平衡旋转，再对根结点进行LL平衡旋转</strong>，即LR平衡旋转。</p><p>【操作步骤】</p><ul><li>先将根结点的左孩子进行RR平衡旋转</li></ul><p><img src="https://cdn.frankfang.cn/img/6743723b-3b0d-4a8c-8a43-6fe816e9d4d3.png" alt="1618670334825.png"></p><ul><li>再将根结点进行LL平衡旋转</li></ul><p><img src="https://cdn.frankfang.cn/img/120e5f1f-9824-4275-aabf-a5ac96c0f0cb.png" alt="1618670718342.png"></p><h4 id="（4）RL平衡旋转"><a href="#（4）RL平衡旋转" class="headerlink" title="（4）RL平衡旋转"></a>（4）RL平衡旋转</h4><p>与LR平衡旋转类似，当根结点的<strong>右孩子</strong>的<strong>左子树</strong>高度大于根结点的右孩子的右子树的高度时，我们可以<strong>先对根结点的右孩子进行LL平衡旋转，再对根结点进行RR平衡旋转</strong>，即RL平衡旋转。</p><p>【操作步骤】</p><p>假设有一颗平衡二叉树 {2, 1, 6, 5, 7}，在插入结点3后，可根据如下步骤进行调整：</p><ul><li>先将根结点的左孩子进行RR平衡旋转</li></ul><p><img src="https://cdn.frankfang.cn/img/3f797b7f-19d4-4e09-960d-fa185167920d.png" alt="1618672269262.png"></p><ul><li>再将根结点进行LL平衡旋转</li></ul><p><img src="https://cdn.frankfang.cn/img/58361e5c-90aa-4e23-af8d-4e779df09439.png" alt="1618707626317.png"></p><h3 id="三、查找操作"><a href="#三、查找操作" class="headerlink" title="三、查找操作"></a>三、查找操作</h3><p>平衡二叉树的查找操作与二叉排序树相同，详细内容可阅读我之前写的一篇文章<a href="https://frankfang.cn/article/202103512">《二叉排序树》</a>，这里不再进行赘述。这里需要注意的是，平衡二叉树在二叉排序树的基础上进行了优化，解决了极端情况下（二叉树变成了单链表）查找效率降低的问题。因此平衡二叉树的平均查找长度为$O(log_2n)$。</p><h3 id="四、完整实现"><a href="#四、完整实现" class="headerlink" title="四、完整实现"></a>四、完整实现</h3><h4 id="1、获取二叉树的高度"><a href="#1、获取二叉树的高度" class="headerlink" title="1、获取二叉树的高度"></a>1、获取二叉树的高度</h4><p>在实现平衡二叉树的操作之前我们需要获取二叉树的高度，因为只有知道了树的高度才能判断在插入一个新结点后该树是否仍为平衡二叉树。因此我们需要在<code>TreeNode</code>类中添加以下方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 返回当前结点的左子树的高度</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">leftHeight</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> left == <span class="keyword">null</span> ? <span class="number">0</span> : left.height();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 返回当前结点的右子树的高度</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">rightHeight</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> right == <span class="keyword">null</span> ? <span class="number">0</span> : right.height();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 返回以当前结点为根结点的树的高度</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">height</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> Math.max(left == <span class="keyword">null</span> ? <span class="number">0</span> : left.height(), right == <span class="keyword">null</span> ? <span class="number">0</span> : right.height()) + <span class="number">1</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="2、在插入时进行平衡旋转"><a href="#2、在插入时进行平衡旋转" class="headerlink" title="2、在插入时进行平衡旋转"></a>2、在插入时进行平衡旋转</h4><p>当可以获取二叉树的高度之后，就可以在<code>TreeNode</code>类插入结点方法中添加以下内容：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">add</span><span class="params">(TreeNode node)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 插入结点</span></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 判断当前二叉树是否平衡</span></span><br><span class="line">    <span class="keyword">if</span> (rightHeight() - leftHeight() &gt; <span class="number">1</span>) &#123;</span><br><span class="line">        <span class="comment">// 如果当前结点的右孩子的左子树高度大于当前结点的右孩子的右子树的高度</span></span><br><span class="line">        <span class="keyword">if</span> (right != <span class="keyword">null</span> &amp;&amp; right.leftHeight() &gt; right.rightHeight()) &#123;</span><br><span class="line">            <span class="comment">// RL平衡旋转</span></span><br><span class="line">            <span class="comment">// 1.先对当前结点的右孩子向右旋转（LL平衡旋转）</span></span><br><span class="line">            right.rightRotate();</span><br><span class="line">            <span class="comment">// 2.再对当前结点进行向左旋转（RR平衡旋转）</span></span><br><span class="line">            leftRotate();</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="comment">// RR平衡旋转</span></span><br><span class="line">            leftRotate();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (leftHeight() - rightHeight() &gt; <span class="number">1</span>) &#123;</span><br><span class="line">        <span class="comment">// 如果当前结点的左孩子的右子树高度大于当前结点的左孩子的左子树的高度</span></span><br><span class="line">        <span class="keyword">if</span> (left != <span class="keyword">null</span> &amp;&amp; left.rightHeight() &gt; left.leftHeight()) &#123;</span><br><span class="line">            <span class="comment">// LR平衡旋转</span></span><br><span class="line">            <span class="comment">// 1.先对当前结点的左孩子向左旋转（RR平衡旋转）</span></span><br><span class="line">            left.leftRotate();</span><br><span class="line">            <span class="comment">// 2.再对当前结点进行向右旋转（LL平衡旋转）</span></span><br><span class="line">            rightRotate();</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="comment">// LL平衡旋转</span></span><br><span class="line">            rightRotate();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>完整的实现代码已上传到Gitee，仓库地址：<a href="https://gitee.com/fzcoder/data-structure-and-algorithms-demo">https://gitee.com/fzcoder/data-structure-and-algorithms-demo</a>。</p>]]></content>
    
    
    <summary type="html">本文主要介绍了平衡二叉树的基本概念及如何保证二叉树的平衡</summary>
    
    
    
    <category term="数据结构" scheme="https://blog.frankfang.cn/categories/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    
    <category term="数据结构" scheme="https://blog.frankfang.cn/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    <category term="树" scheme="https://blog.frankfang.cn/tags/%E6%A0%91/"/>
    
    <category term="二叉树" scheme="https://blog.frankfang.cn/tags/%E4%BA%8C%E5%8F%89%E6%A0%91/"/>
    
  </entry>
  
  <entry>
    <title>进程与线程</title>
    <link href="https://blog.frankfang.cn/posts/202103697.html"/>
    <id>https://blog.frankfang.cn/posts/202103697.html</id>
    <published>2021-03-29T12:55:10.000Z</published>
    <updated>2021-03-29T12:57:52.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="一、进程"><a href="#一、进程" class="headerlink" title="一、进程"></a>一、进程</h3><h4 id="1、基本概念"><a href="#1、基本概念" class="headerlink" title="1、基本概念"></a>1、基本概念</h4><p>计算机上所有可运行的软件，通常也包括操作系统，被组织成若干顺序进程（sequential process），简称<strong>进程（process）</strong>。 <strong>一个进程就是一个正在执行程序的实例，包括程序计数器、寄存器和变量的当前值</strong>。从概念上说，每个进程拥有它自己的虚拟 CPU。当然，实际上真正的 CPU 在各进程之间来回切换。但为了理解这种系统，考虑在（伪）并行情况下运行的进程集，要比试图跟踪 CPU 如何在程序间来回切换简单得多。</p><p><img src="https://cdn.frankfang.cn/img/873dce2a-ea82-4c10-841a-7a501367e93c.png" alt="1616401404926.png"></p><ul><li><p>在上图 (a) 中可以看到，在一台多道程序计算机的内存中有 4 道程序。</p></li><li><p>在上图 (b) 中，这 4 道程序<strong>被抽象为 4 个各自拥有自已控制流程（即每个程序自己的逻辑程序计数器）的进程，并且每个程序都独立地运行</strong>。当然，实际上只有一个物理程序计数器，所以在每个程序运行时，它的逻辑程序计数器被装人实际的程序计数器中。当该程序执行结束（或暂停执行）时，物理程序计数器被保存在内存中该进程的逻辑程序计数器中。</p></li><li><p>在上图 (c) 中可以看到，<strong>在观察足够长的一段时间后，所有的进程都运行了，但在任何一个给定的瞬间仅有一个进程真正在运行</strong>。即在单核 CPU 中进程是<strong>并发</strong>执行的（注意并发与并行的区别）。</p></li></ul><p>为了使参与并发执行的程序（含数据）能独立地运行，必须为之配置一个专门的数据结构，称为<strong>进程控制块（Process Control Block, PCB）</strong>。操作系统利用 PCB 来描述进程的基本情况和运行情况，进而控制和管理进程。相应地，<strong>由程序段、相关数据段和 PCB 三部分构成了进程映像（进程实体）</strong>。所谓创建进程，实质上是创建进程映像中的 PCB；而撤销进程，实质上是撤销进程的 PCB。要注意，<strong>进程映像是静态的，而进程是动态的</strong>。</p><p>从不同的角度，进程可以有不同的定义，下面列出一些典型的定义：</p><ul><li>进程是程序的一次执行过程。</li><li>进程是一个程序及其数据在处理机上顺序执行时所发生的活动。</li><li>进程是具有独立功能的程序在一个数据集合上运行的过程，它是系统进行资源分配和调度的一个独立单位。</li></ul><p>在介绍了进程映像的相关概念后，可对进程做如下定义：<strong>进程是进程映像的运行过程，是系统进行资源分配和调度的一个独立单位</strong>。</p><h4 id="2、进程的状态"><a href="#2、进程的状态" class="headerlink" title="2、进程的状态"></a>2、进程的状态</h4><p>进程在其生命周期内，由于系统中各进程之间的相互制约关系及系统的运行环境的变化，使得进程的状态也在不断地发生变化。通常进程有以下5种状态，前3种是进程的基本状态。</p><ul><li><p><strong>就绪（Ready）</strong>状态：进程获得了除处理机外的一切所需资源，一旦得到处理机，便可立即运行。</p></li><li><p><strong>运行（Running）</strong>状态：进程正在处理机上运行。在单处理机环境下，每个时刻最多只有一个进程处于运行状态。</p></li><li><p><strong>阻塞（Blocked）</strong>状态：进程正在等待某一事件而暂停运行，如等待某资源为可用（不包括处理机）或等待输入/输出完成。即使处理机空闲，该进程也不能运行。</p><p>就绪、运行和阻塞状态的转换关系如下：</p></li></ul><p><img src="https://cdn.frankfang.cn/img/4240584a-4371-4784-9128-c739cc2a490e.png" alt="1616589672590.png"></p><ul><li><p><strong>创建</strong>状态：进程正在被创建，尚未转到就绪态。</p></li><li><p><strong>结束</strong>状态：进程正从系统中消失，可能是进程正常结束或其他原因中断退出运行。</p><p>在加入创建和终止状态的转换图如下：</p></li></ul><p><img src="https://cdn.frankfang.cn/img/687bb106-4574-4a32-926b-0350f8fda1ca.png" alt="1616589782742.png"></p><h4 id="3、进程控制"><a href="#3、进程控制" class="headerlink" title="3、进程控制"></a>3、进程控制</h4><p>进程控制的主要功能是对系统中的所有进程实施有效的管理，它具有创建新进程、撤销已有进程、实现进程状态转换等功能。在操作系统中，一般把进程控制用的程序称为原语，原语的特点是执行期间不允许中断，它是一个不可分割的基本单位。</p><h5 id="（1）进程的创建"><a href="#（1）进程的创建" class="headerlink" title="（1）进程的创建"></a>（1）进程的创建</h5><p>引起进程创建的事件：</p><ul><li><p><strong>用户登录</strong>。在分时系统中，用户在终端键入登录命令后，若登录成功，系统将为该用户建立一个进程，并把它插入就绪队列中。</p></li><li><p><strong>作业调度</strong>。在多道批处理系统中，当作业调度程序按一定的算法调度到某个(些)作业时，便将它(们)装入内存，为它(们)创建进程，并把它(们)插入就绪队列中。</p></li><li><p><strong>提供服务</strong>。当运行中的用户程序提出某种请求后，系统将专门创建一个进程来提供用户所需要的服务，例如，用户程序要求进行文件打印，操作系统将为它创建一个打印进程，这样不仅可使打印进程与该用户进程并发执行，而且还便于计算为完成打印任务所花费的时间。</p></li><li><p><strong>应用请求</strong>。在上述三种情况下，都是由系统内核为用户创建一 个新进程；而这类事件则是由用户进程自己创建新进程，以便使新进程以同创建者进程并发运行的方式完成特定任务。</p></li></ul><p>进程的创建步骤（创建原语）：</p><ul><li><p><strong>申请空白PCB，为新进程申请获得唯一的数字标识符，并从PCB集合中索取一个空白PCB</strong>。</p></li><li><p> <strong>为新进程分配其运行所需的资源</strong>，包括各种物理和逻辑资源，如内存、文件、I/O 设备和 CPU 时间等。这些资源或从操作系统或仅从其父进程获得。新进程对这些资源的需求详情一般也要提前告知操作系统或其父进程。注意：若资源不足（如内存空间），则并不是创建失败，而是处于阻塞态，等待内存资源。</p></li><li><p><strong>初始化进程控制块</strong>。PCB 的初始化包括：初始化标识信息，将系统分配的标识符和父进程标识符填入新PCB中；初始化处理机状态信息，使程序计数器指向程序的入口地址，使栈指针指向栈项；初始化处理机控制信息，将进程的状态设置为就绪状态或静止就绪状态，对于优先级，通常是将它设置为最低优先级，除非用户以显式方式提出高优先级要求。</p></li><li><p>如果进程就绪队列能够接纳新进程，便<strong>将新进程插入就绪队列</strong>。</p></li></ul><h5 id="（2）进程的终止"><a href="#（2）进程的终止" class="headerlink" title="（2）进程的终止"></a>（2）进程的终止</h5><p>引起进程终止的事件：</p><ul><li><p><strong>正常结束</strong>。表示进程的任务已经完成，准备退出运行。在任何系统中，都应有一个用于表示进程已经运行完成的指示。</p></li><li><p><strong>异常结束</strong>。是指进程在运行时发生了某种异常事件，使程序无法继续运行。常见的异常事件有：越界错、保护错、非法指令、特权指令错、运行超时、等待超时、算术运算错、I/O故障等。</p></li><li><p><strong>外界干预</strong>。是指进程应外界的请求而终止运行。这些干预有：操作员或操作系统干预、父进程请求和父进程终止。</p></li></ul><p>进程的终止步骤（撤销原语）：</p><ul><li><p>根据被终止进程的标识符，从PCB集合中检索出该进程的PCB，从中读出该进程的状态。</p></li><li><p>若被终止进程正处于执行状态，应立即终止该进程的执行，并置调度标志为真，用于指示该进程被终止后应重新进行调度。</p></li><li><p>若该进程还有子孙进程，还应将其所有子孙进程也都予以终止，以防它们成为不可控的进程。</p></li><li><p>将被终止进程所拥有的全部资源或者归还给其父进程，或者归还给系统。</p></li><li><p>将被终止进程(PCB)从所在队列(或链表)中移出，等待其它程序来搜集信息。</p></li></ul><h5 id="（3）阻塞与唤醒"><a href="#（3）阻塞与唤醒" class="headerlink" title="（3）阻塞与唤醒"></a>（3）阻塞与唤醒</h5><p>正在执行的进程，由于期待的某些事件未发生，如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作可做等。由系统自动执行阻塞原语（Block），使自己由运动态变为阻塞态。可见，<strong>进程的阻塞是进程自身的一种主动行为</strong>，也因此<strong>只有处于运行态的进程（获得CPU），才可能将其转为阻塞态</strong>。阻塞原语的执行过程如下：</p><ul><li>找到将要被阻塞进程的标识号对应的PCB。</li><li>若该进程为运行状态，则保护其现场。将其状态转为阻塞态，停止运行。</li><li>把该PCB插入相应事件的等待队列，将处理机资源调度给其它就绪进程。</li></ul><p>当<strong>被阻塞进程所期待的事件出现时</strong>，如它所启动的I/O操作已完成或其所期待的数据已到达，由<strong>有关进程（如：释放该I/O设备的进程，或提供数据的进程）调用唤醒原语（Wakeup）</strong>，将等待该事件的进程唤醒。唤醒原语的执行过程如下：</p><ul><li>在该事件的等待队列中找到相应进程的PCB。</li><li>将其从等待队列中移出，并置其状态为就绪状态。</li><li>把该PCB插入就绪队列，等待调度程序调用。</li></ul><p>需要注意的是，Block 原语和 Wakeup 原语是一对作用刚好相反的原语，<strong>必须成对使用</strong>。Block 原语是<strong>由被阻塞进程自我调用实现的</strong>，而 Wakeup 原语则是<strong>由一个与被唤醒进程合作或被其它相关的进程调用实现的</strong>。</p><h5 id="（4）挂起与激活"><a href="#（4）挂起与激活" class="headerlink" title="（4）挂起与激活"></a>（4）挂起与激活</h5><ul><li>进程的挂起（suspend）</li></ul><p>当系统中出现了引起进程挂起的事件时，OS 将利用挂起原语 suspend 将指定进程或处于阻塞状态的进程挂起。 suspend 的执行过程是：首先检查被挂起进程的状态，若处于活动就绪状态，便将其改为静止就绪；对于活动阻塞状态的进程，则将之改为静止阻塞；为了方便用户或父进程考查该进程的运行情况，而把该进程的 PCB 复制到某指定的内存区域；最后，若被挂起的进程正在执行，则转向调度程序重新调度。</p><ul><li>进程的激活（active）</li></ul><p>当系统中发生激活进程的事件时，OS 将利用激活原语 active，将指定进程激活。激活原语先将进程从外存调入内存，检查该进程的现行状态，若是静止就绪，便将之改为活动.就绪;若为静止阻塞，便将之改为活动阻塞。假如采用的是抢占调度策略，则每当有静止就绪进程被激活而插入就绪队列时，便应检查是否要进行重新调度，即由调度程序将被激活的进程与当前进程两者的优先级进行比较，如果被激活进程的优先级低，就不必重新调度;否则，立即剥夺当前进程的运行，把处理机分配给刚刚被激活的进程。</p><h4 id="4、进程的层次结构"><a href="#4、进程的层次结构" class="headerlink" title="4、进程的层次结构"></a>4、进程的层次结构</h4><p>在操作系统中，允许一个进程创建另一个进程。此时创建者称为<strong>父进程</strong>，被创建的进程称为<strong>子进程</strong>。子进程可以继承父进程所拥有的资源。当子进程被撤销时，应将其从父进程那里获取的资源归还给父进程。此外，在撤销父进程时，必须同时撤销其所有的子进程。<strong>子进程自身可以创建更多的进程</strong>，组成一个进程的层次结构。</p><p><strong>在 UNIX 中，进程和它的所有子进程以及后裔共同组成一个进程组</strong>。当用户从键盘发出一个信号时，该信号被送给当前与键盘相关的进程组中的所有成员（它们通常是在当前窗口创建的所有活动进程）。每个进程可以分别捕获该信号、忽略该信号或采取默认的动作，即被该信号杀死。</p><p>这里有另一个例子，可以用来说明进程层次的作用，考虑 UNIX 在启动时如何初始化自己。一个称为 <em>init</em> 的特殊进程出现在启动映像中。当它开始运行时，读入一个说明终端数量的文件。接着，为每个终端创建一一个新进程。这些进程等待用户登录。如果有一个用户登录成功，该登录进程就执行一个 <em>shell</em> 准备接收命令。所接收的这些命令会启动更多的进程，以此类推。这样，在整个系统中，所有的进程都属于以 <em>init</em> 为根的一棵树。</p><p>相反，<strong>Windows 中没有进程层次的概念，所有的进程都是地位相同的</strong>。唯一类似于进程层次的暗示是在创建进程的时候，父进程得到一个特别的令牌（称为句柄），该句柄可以用来控制子进程。但是，它有权把这个令牌传送给某个其他进程，这样就不存在进程层次了。在 UNIX 中，进程就不能剥夺其子继承的“继承权”。</p><h4 id="5、进程的实现"><a href="#5、进程的实现" class="headerlink" title="5、进程的实现"></a>5、进程的实现</h4><p>为了实现进程模型，操作系统维护着一张表格（一个结构数组），即<strong>进程表（process table）</strong>。 每个进程占用一个<strong>进程表项（即进程控制块，PCB）</strong>该表项包含了进程状态的重要信息，包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息，以及其他在进程由运行态转换到就绪态或阻塞态时必须保存的信息，从而保证该进程随后能再次启动，就像从未被中断过样。</p><p><img src="https://cdn.frankfang.cn/img/b1b6030f-20fe-4116-9ea5-10f35b5b7cf4.png" alt="1616595618757.png"></p><p>上图展示了在一个典型系统中的关键字段。第一列中的字段与进程管理有关。其他两列分别与存储管理和文件管理有关。应该注意到进程表中的字段是与系统密切相关的，不过该图给出了所需要信息的大致介绍。</p><h3 id="二、线程"><a href="#二、线程" class="headerlink" title="二、线程"></a>二、线程</h3><h4 id="1、为什么要使用线程"><a href="#1、为什么要使用线程" class="headerlink" title="1、为什么要使用线程"></a>1、为什么要使用线程</h4><p>如果说，在 OS 中引入进程的目的是为了使多个程序能并发执行，以提高资源利用率和系统吞吐量，那么，在操作系统中再引入<strong>线程（thread）</strong>，则是为了<strong>减少程序在并发执行时所付出的时空开销，使 OS 具有更好的并发性</strong>。</p><h4 id="2、经典的线程模型"><a href="#2、经典的线程模型" class="headerlink" title="2、经典的线程模型"></a>2、经典的线程模型</h4><p>线程最直接的理解就是<strong>轻量级进程（lightweight process）</strong>，它是一个基本的CPU执行单元，也是程序执行流的最小单元。在线程中有一个程序计数器，用来记录接着要执行哪一条指令。线程拥有寄存器，用来保存线程当前的工作变量。线程还拥有一个堆栈，用来记录执行历史，其中每一帧保存了一个已调用的但是还没有从中返回的过程。尽管线程必须在某个进程中执行，但是线程和它的进程是不同的概念，并且可以分别处理。<strong>进程用于把资源集中到一起，而线程则是在CPU上被调度执行的实体</strong>。</p><p>线程是进程中的一个实体，是被系统独立调度和分派的基本单位，<strong>线程自己不拥有系统资源，只拥有在运行中必不可少的资源，但它可与同属一个进程的其它线程共享进程所拥有的全部资源</strong>。一个线程可以创建和撤销另一个线程，同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约，致使线程在运行中呈现出间断性。<strong>线程也有就绪、阻塞和运行三种基本状态</strong>。</p><p><img src="https://cdn.frankfang.cn/img/5f4e97ef-da58-41b2-a127-ec1417396225.png" alt="1616934399628.png"></p><p>例如，在上图(a)中，可以看到三个传统的进程。每个进程有自己的地址空间和单个控制线程。相反，在上图(b)中，可以看到一个进程带有三个控制线程。尽管在两种情形中都有三个线程，但是在上图(a)中，每一个线程都在不同的地址空间中运行，而在上图(b)中，这三个线程全部在相同的地址空间中运行。</p><h4 id="3、线程的实现"><a href="#3、线程的实现" class="headerlink" title="3、线程的实现"></a>3、线程的实现</h4><p>在介绍线程的实现之前，我们先来简单介绍一下 POSIX 线程。为了实现可移植的线程程序，IEEE 在 IEEE 标准 1003.1c 中定义了线程的标准。它定义的线程包叫作<code>pthread</code>。大部分 UNIX 系统都支持该标准。这个标准定义了超过 60 个函数调用。下面将通过一张表格来介绍一些主要的函数，以说明它是如何工作的。</p><table><thead><tr><th>线程调用</th><th>描述</th></tr></thead><tbody><tr><td><code>pthread_create</code></td><td>创建一个新线程</td></tr><tr><td><code>pthread_exit</code></td><td>结束调用的线程</td></tr><tr><td><code>pthread_join</code></td><td>等待一个特定的线程退出</td></tr><tr><td><code>pthread_yield</code></td><td>释放CPU来运行另外一个线程</td></tr><tr><td><code>pthread_attr_init</code></td><td>创建并初始化一个线程的属性结构</td></tr><tr><td><code>pthread_attr_destroy</code></td><td>删除一个线程的属性结构</td></tr></tbody></table><p>所有<code>pthread</code>线程都有某些特性。每一个都含有一个标识符、一组寄存器（包括程序计数器）和一组存储在结构中的属性。这些属性包括堆栈大小、调度参数以及其他线程需要的项目。</p><p>线程的实现可以分为两类：<strong>用户级线程（User-Level Thread, ULT）</strong>和<strong>内核级线程（Kernel-Level Thread, KLT）</strong>。这两种方法各有利弊，下面我们将分别详细介绍这两种方法。</p><h5 id="（1）用户级线程"><a href="#（1）用户级线程" class="headerlink" title="（1）用户级线程"></a>（1）用户级线程</h5><p>在用户级线程中，有关线程管理（线程的创建、撤销和切换等）的所有工作都由应用程序完成，内核意识不到线程的存在。应用程序可以通过使用线程库设计多线程程序。下面将举一个使用<code>pthread</code>线程库编写的一个小程序的案例。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string">&lt;pthread.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> NUMBER_OF_THREADS 10</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> *<span class="title">print_hello_world</span><span class="params">(<span class="keyword">void</span> *tid)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="comment">// 本函数输出线程的标识符，然后退出。</span></span><br><span class="line">    print(<span class="string">&quot;Hello World. Greetings from thread %d\n&quot;</span>, tid);</span><br><span class="line">    pthread_exit(<span class="literal">NULL</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">(<span class="keyword">int</span> argc, <span class="keyword">char</span> *argv[])</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="comment">// 主程序创建10个线程，然后退出。</span></span><br><span class="line">    <span class="keyword">pthread_t</span> threads[NUMBER_OF_THREADS];</span><br><span class="line">    <span class="keyword">int</span> status, i;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span>(i = <span class="number">0</span>; i &lt; NUMBER_OF_THREADS; i++) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;Main here. Creating thread %d\n&quot;</span>, i);</span><br><span class="line">        status = pthread_create(&amp;threads[i], <span class="literal">NULL</span>, print_hello_world, (<span class="keyword">void</span> *)i);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (status != <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="built_in">printf</span>(<span class="string">&quot;Oops. pthread_create returned error code %d\n&quot;</span>, status);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="built_in">exit</span>(<span class="number">-1</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">exit</span>(<span class="literal">NULL</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>所有的这类实现都有同样的通用结构，如下图所示。线程在一个运行时系统的上层运行，该运行时系统是一个管理线程的过程的集合。</p><p><img src="https://cdn.frankfang.cn/img/531ffaf7-5c46-498d-be88-c955a0dc9f7c.png" alt="1617019063838.png"></p><p>在用户空间管理线程时，每个进程需要有其专用的<strong>线程表（thread table）</strong>，用来跟踪该进程中的线程。这些表和内核中的进程表类似，不过它仅仅记录各个线程的属性，如每个线程的程序计数器、堆栈指针、寄存器和状态等。该线程表由运行时系统管理。当一个线程转换到就绪状态或阻塞状态时，在该线程表中存放重新启动该线程所需的信息，与内核在进程表中存放进程的信息完全一样。</p><h5 id="（2）内核级线程"><a href="#（2）内核级线程" class="headerlink" title="（2）内核级线程"></a>（2）内核级线程</h5><p>在内核级线程中，线程管理的所有工作由内核完成，应用程序没有进行线程管理的代码，只有一个到内核级线程的编程接口。内核为进程及其内部的每个线程维护上下文信息，调度也在内核基于线程架构的基础上完成。下图说明了内核级线程的实现方式。</p><p><img src="https://cdn.frankfang.cn/img/9a98af29-86c3-48a1-9818-7c0a8a7bab44.png" alt="1617020345326.png"></p><p>当使用内核级线程时就不再需要运行时系统了。另外，每个进程中也没有线程表。相反，在内核中有用来记录系统中所有线程的线程表。当某个线程希望创建一个新线程或撤销一个已有线程时，它进行一个系统调用，这个系统调用通过对线程表的更新完成线程创建或撤销工作。</p><p>内核的线程表保存了每个线程的寄存器、状态和其他信息。这些信息和在用户空间中（在运行时系统中）的线程是一样的，但是现在保存在内核中。这些信息是传统内核所维护的每个单线程进程信息（即进程状态）的子集。另外，内核还维护了传统的进程表，以便跟踪进程的状态。</p><h5 id="（3）混合实现"><a href="#（3）混合实现" class="headerlink" title="（3）混合实现"></a>（3）混合实现</h5><p>有些系统使用混合方式进行实现，简单来说就是使用内核级线程，然后将用户级线程与某些或者全部内核线程多路复用起来，如下图所示。</p><p><img src="https://cdn.frankfang.cn/img/83d31018-5eed-4afa-9612-db8ba5fc895f.png" alt="1617021218056.png"></p><p>如果采用这种方法，编程人员可以决定有多少个内核级线程和多少个用户级线程彼此多路复用。这一模型带来最大的灵活度。采用这种方法，内核只识别内核级线程，并对其进行调度。其中一些内核级线程会被多个用户级线程多路复用。如同在没有多线程能力操作系统中某个进程中的用户级线程一样，可以创建、撤销和调度这些用户级线程。在这种模型中，每个内核级线程有一个可以轮流使用的用户级线程集合。</p><h4 id="4、多线程模型"><a href="#4、多线程模型" class="headerlink" title="4、多线程模型"></a>4、多线程模型</h4><p>有些系统同时支持用户线程和内核线程，由此产生了不同的多线程模型，即实现用户级线程和内核级线程的连接方式。</p><p><strong>（1）多对一模型</strong></p><p>将多个用户级线程映射到一个内核级线程，线程管理在用户空间完成。此模式中，用户级线程对操作系统不可见（即透明）。</p><p>优点：线程管理是在用户空间进行的，因而效率比较高。</p><p>缺点：一个线程在使用内核服务时被阻塞，整个进程都会被阻塞；多个线程不能并行地运行在多处理机上。</p><p><strong>（2）一对一模型</strong></p><p>将每个用户级线程映射到一个内核级线程。</p><p>优点：当一个线程被阻塞后，允许另一个线程继续执行，所以并发能力较强。</p><p>缺点：每创建一个用户级线程都需要创建一个内核级线程与其对应，这样创建的线程开销比较大，会影响到应用程序的性能。</p><p><strong>（3）多对多模型</strong></p><p>将 n 个用户级线程映射到 m 个内核级线程上，要求 $m \le n$。</p><p>特点：多对多模型是多对一模型和一对一模型的折中，既克服了多对一模型并发度不高的缺点，又克服了一对一模型的一个用户进程占用太多内核级线程而开销太大的缺点。此外，还拥有多对一模型和一对一模型各自的优点，可谓集两者之所长。</p>]]></content>
    
    
    <summary type="html">本文主要介绍了操作系统中进程与线程的基本概念</summary>
    
    
    
    <category term="操作系统" scheme="https://blog.frankfang.cn/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="操作系统" scheme="https://blog.frankfang.cn/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
    <category term="进程" scheme="https://blog.frankfang.cn/tags/%E8%BF%9B%E7%A8%8B/"/>
    
    <category term="线程" scheme="https://blog.frankfang.cn/tags/%E7%BA%BF%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>二叉排序树</title>
    <link href="https://blog.frankfang.cn/posts/202103512.html"/>
    <id>https://blog.frankfang.cn/posts/202103512.html</id>
    <published>2021-03-27T12:57:30.000Z</published>
    <updated>2021-03-27T12:57:30.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="一、基本概念"><a href="#一、基本概念" class="headerlink" title="一、基本概念"></a>一、基本概念</h3><p>二叉排序树（Binary Sort Tree, BST）也称二叉搜索树，或是一颗空树，或者是具有一下性质的二叉树：</p><ul><li>若左子树非空，则<strong>左子树上所有结点的值均小于根结点的值</strong>。</li><li>若右子结点非空，则<strong>右子树上所有结点的值均大于根结点的值</strong>。</li><li>左、右子树也分别是一颗二叉排序树。</li></ul><p>根据二叉排序树的定义有：<strong>左子树结点值 &lt; 根结点值 &lt; 右子树结点值</strong>，所以对二叉排序树进行中序遍历，可以得到一个递增的有序序列。</p><p><img src="https://cdn.frankfang.cn/img/562efb47-2fe4-47e7-b280-c7cf0d15a8c0.png" alt="1616672562904.png"></p><p>如上图所示，该二叉排序树的中序遍历序列为 1 2 3 4 5 6。</p><h3 id="二、相关操作"><a href="#二、相关操作" class="headerlink" title="二、相关操作"></a>二、相关操作</h3><h4 id="1、二叉排序树的插入"><a href="#1、二叉排序树的插入" class="headerlink" title="1、二叉排序树的插入"></a>1、二叉排序树的插入</h4><p>二叉排序树作为一种动态树表，其特点是树的结构通常不是一次生成的，而是在查找过程中，当树中不存在关键字值等于给定值的结点时再进行插入的。</p><p>首先确定二叉排序树结点的数据结构及成员方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">TreeNode</span> </span>&#123;</span><br><span class="line">    <span class="keyword">int</span> val;</span><br><span class="line">    TreeNode left;</span><br><span class="line">    TreeNode right;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">TreeNode</span><span class="params">(<span class="keyword">int</span> val)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.val = val;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 插入结点</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">add</span><span class="params">(TreeNode node)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 如果当前结点的值小于要插入结点的值</span></span><br><span class="line">        <span class="keyword">if</span> (node.val &lt; <span class="keyword">this</span>.val) &#123;</span><br><span class="line">            <span class="comment">// 判断当前结点的左子结点是否为空</span></span><br><span class="line">            <span class="keyword">if</span> (<span class="keyword">this</span>.left == <span class="keyword">null</span>) &#123;</span><br><span class="line">                <span class="comment">// 为空，在当前结点的左子结点位置插入结点</span></span><br><span class="line">                <span class="keyword">this</span>.left = <span class="keyword">new</span> TreeNode(node.val);</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                <span class="comment">// 不为空，继续向左子树查找插入的位置</span></span><br><span class="line">                <span class="keyword">this</span>.left.add(node);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 如果当前结点的值大于要插入结点的值</span></span><br><span class="line">        <span class="keyword">if</span> (node.val &gt; <span class="keyword">this</span>.val) &#123;</span><br><span class="line">            <span class="comment">// 判断当前结点的右子结点是否为空</span></span><br><span class="line">            <span class="keyword">if</span> (<span class="keyword">this</span>.right == <span class="keyword">null</span>) &#123;</span><br><span class="line">                <span class="comment">// 为空，在当前结点的右子结点位置插入结点</span></span><br><span class="line">                <span class="keyword">this</span>.right = <span class="keyword">new</span> TreeNode(node.val);</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                <span class="comment">// 不为空，继续向右子树查找插入的位置</span></span><br><span class="line">                <span class="keyword">this</span>.right.add(node);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注：上面的算法忽略了当插入的值等于当前结点的情况，原因是<strong>二叉排序树一般用于当表中元素的值各不相同的情况时</strong>，如果在二叉排序树中插入多个相同值的结点时会影响查询的效率。</p><p>然后创建<code>BinarySortTree</code>类，代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">BinarySortTree</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 二叉排序树的根结点</span></span><br><span class="line">    <span class="keyword">private</span> TreeNode root;</span><br><span class="line">    <span class="comment">// 插入结点</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">insertNode</span><span class="params">(<span class="keyword">int</span> value)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (root == <span class="keyword">null</span>) &#123;</span><br><span class="line">            root = <span class="keyword">new</span> TreeNode(value);</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            root.add(<span class="keyword">new</span> TreeNode(value));</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>之后在主方法中创建<code>BinarySortTree</code>类并调用其中的<code>insertNode</code>方法即可实现二叉排序树的插入。</p><p>例如，现在有一个数组为 { 45, 24, 53, 45, 12, 24, 90 }，则按上述算法生成二叉排序树的过程如下：</p><p><img src="https://cdn.frankfang.cn/img/fab6fa5a-b5b1-479a-ad0f-56423d4827ea.png" alt="1616847375641.png"></p><h4 id="2、二叉排序树的查找"><a href="#2、二叉排序树的查找" class="headerlink" title="2、二叉排序树的查找"></a>2、二叉排序树的查找</h4><p>二叉排序树的查找是<strong>从根结点开始，沿某个分支逐层向下比较</strong>的过程。若二叉排序树非空，先<strong>将给定值与根结点的关键字比较</strong>，若相等，则查找成功；若不等，<strong>如果小于根结点的关键字，则在根结点的左子树上查找，否则在根结点的右子树上查找</strong>。这是一个递归的过程。</p><p>首先在<code>TreeNode</code>类中添加以下成员方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> TreeNode <span class="title">search</span><span class="params">(<span class="keyword">int</span> value)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="keyword">this</span>.val == value) &#123;</span><br><span class="line">        <span class="comment">// 查找的值等于当前结点的值</span></span><br><span class="line">        <span class="comment">// 返回当前结点</span></span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">this</span>;</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (<span class="keyword">this</span>.val &gt; value &amp;&amp; <span class="keyword">this</span>.left != <span class="keyword">null</span>) &#123;</span><br><span class="line">        <span class="comment">// 查找的值小于当前结点的值且当前结点左子树非空</span></span><br><span class="line">        <span class="comment">// 在当前结点的左子树上查找</span></span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">this</span>.left.search(value);</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (<span class="keyword">this</span>.val &lt; value &amp;&amp; <span class="keyword">this</span>.right != <span class="keyword">null</span>) &#123;</span><br><span class="line">        <span class="comment">// 查找的值小于当前结点的值且当前结点左子树非空</span></span><br><span class="line">        <span class="comment">// 在当前结点的左子树上查找</span></span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">this</span>.right.search(value);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// 查询不到结果，返回空值</span></span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后在<code>BinarySortTree</code>类中添加以下成员方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> TreeNode <span class="title">searchNode</span><span class="params">(<span class="keyword">int</span> value)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="keyword">this</span>.root != <span class="keyword">null</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> root.search(value);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>之后在主方法中调用<code>BinarySortTree</code>类的<code>searchNode</code>方法即可实现二叉排序树的查找。</p><h4 id="3、二叉排序树的删除"><a href="#3、二叉排序树的删除" class="headerlink" title="3、二叉排序树的删除"></a>3、二叉排序树的删除</h4><p>在二叉排序树中删除一个结点时，不能把以该结点为根的子树上的结点都删除，必须先把被删除结点从存储二叉排序树的链表上移除，将因删除结点而断开的二叉链表重新链接起来，同时确保二叉排序树的性质不会丢失。下面将介绍在以下三种情况下删除结点的方法。</p><h5 id="（1）删除叶子结点"><a href="#（1）删除叶子结点" class="headerlink" title="（1）删除叶子结点"></a>（1）删除叶子结点</h5><p>如果被删除的结点是叶子节点，则<strong>直接删除</strong>，不会破坏二叉排序树的性质。在使用代码实现之前，我们需要分析一下算法的实现过程：</p><ul><li>首先需要找到要删除的结点和它的父结点（如果存在的话）。</li><li>判断要删除的结点是其父结点的左子结点还是右子结点，并根据情况进行删除。</li></ul><p>由上面的分析可知，我们需要在原有代码的基础上实现一个可以查找目标结点的父结点的方法，在<code>TreeNode</code>类中添加以下成员方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> TreeNode <span class="title">searchParent</span><span class="params">(<span class="keyword">int</span> value)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 如果当前结点的子结点即为要查找的结点</span></span><br><span class="line">    <span class="keyword">if</span> ((<span class="keyword">this</span>.left != <span class="keyword">null</span> &amp;&amp; <span class="keyword">this</span>.left.val == value)</span><br><span class="line">        || (<span class="keyword">this</span>.right != <span class="keyword">null</span> &amp;&amp; <span class="keyword">this</span>.right.val == value)) &#123;</span><br><span class="line">        <span class="comment">// 返回当前结点</span></span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">this</span>;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (<span class="keyword">this</span>.left != <span class="keyword">null</span> &amp;&amp; value &lt; <span class="keyword">this</span>.val) &#123;</span><br><span class="line">            <span class="comment">// 如果左子树非空且查找的值小于当前结点的值</span></span><br><span class="line">            <span class="comment">// 向左子树查找</span></span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">this</span>.left.searchParent(value);</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (<span class="keyword">this</span>.right != <span class="keyword">null</span> &amp;&amp; value &gt; <span class="keyword">this</span>.val)&#123;</span><br><span class="line">            <span class="comment">// 如果右子树非空且查找的值小于当前结点的值</span></span><br><span class="line">            <span class="comment">// 向右子树查找</span></span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">this</span>.right.searchParent(value);</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="comment">// 查找不到结果，返回空值</span></span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在<code>BinarySortTree</code>类中添加以下成员方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> TreeNode <span class="title">searchParent</span><span class="params">(<span class="keyword">int</span> value)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (root != <span class="keyword">null</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> root.searchParent(value);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>下面我们将实现删除叶子结点的方法，在<code>BinarySortTree</code>类中添加如下成员方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 删除结点</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">deleteNode</span><span class="params">(<span class="keyword">int</span> value)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 1.找到要删除的结点</span></span><br><span class="line">    TreeNode target = searchNode(value);</span><br><span class="line">    <span class="comment">// 如果没有找到要删除的结点直接返回</span></span><br><span class="line">    <span class="keyword">if</span> (target == <span class="keyword">null</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 2.找要删除结点的父结点</span></span><br><span class="line">    TreeNode parent = searchParent(value);</span><br><span class="line">    <span class="comment">// 如果找不到父结点。说明该结点就是根结点</span></span><br><span class="line">    <span class="keyword">if</span> (parent == <span class="keyword">null</span>) &#123;</span><br><span class="line">        root = <span class="keyword">null</span>;</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 3.如果目标结点是叶子结点</span></span><br><span class="line">    <span class="keyword">if</span> (target.left == <span class="keyword">null</span> &amp;&amp; target.right == <span class="keyword">null</span>) &#123;</span><br><span class="line">        <span class="comment">// 如果是父结点的左子结点</span></span><br><span class="line">        <span class="keyword">if</span> (target == parent.left) &#123;</span><br><span class="line">            parent.left = <span class="keyword">null</span>;</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 如果是父结点的右子结点</span></span><br><span class="line">        <span class="keyword">if</span> (target == parent.right) &#123;</span><br><span class="line">            parent.right = <span class="keyword">null</span>;</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h5 id="（2）删除只有一个子结点的结点"><a href="#（2）删除只有一个子结点的结点" class="headerlink" title="（2）删除只有一个子结点的结点"></a>（2）删除只有一个子结点的结点</h5><p>若被删除的结点只有一颗左子树或右子树，则让该结点的子树成为父结点的子树，替代该结点的位置。</p><p>在<code>BinarySortTree</code>类中添加如下成员方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (target.left == <span class="keyword">null</span> &amp;&amp; target.right == <span class="keyword">null</span>) &#123; <span class="comment">// 3.如果目标结点是叶子结点</span></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125; <span class="keyword">else</span> <span class="keyword">if</span> (target.left != <span class="keyword">null</span> &amp;&amp; target.right != <span class="keyword">null</span>) &#123; <span class="comment">// 4.如果目标结点有两个子结点</span></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line">&#125; <span class="keyword">else</span> &#123; <span class="comment">// 5.如果目标结点只有一个子结点</span></span><br><span class="line">    <span class="keyword">if</span> (target.left != <span class="keyword">null</span>) &#123; <span class="comment">// 如果目标结点的左子结点不为空</span></span><br><span class="line">        <span class="keyword">if</span> (target == parent.left) &#123;</span><br><span class="line">            parent.left = target.left;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            parent.right = target.left;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125; <span class="keyword">else</span> &#123; <span class="comment">// 如果目标结点的右子结点不为空</span></span><br><span class="line">        <span class="keyword">if</span> (target == parent.left) &#123;</span><br><span class="line">            parent.left = target.right;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            parent.right = target.right;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上述算法删除只有一个子结点的结点的过程如下图所示：</p><p><img src="https://cdn.frankfang.cn/img/373548e5-bbad-4bad-a93d-b968b73e3cb1.png" alt="1616847871525.png"></p><h5 id="（3）删除有两个子结点的结点"><a href="#（3）删除有两个子结点的结点" class="headerlink" title="（3）删除有两个子结点的结点"></a>（3）删除有两个子结点的结点</h5><p>若被删除的结点有左、右两颗子树，则<strong>令该结点的直接后继（或直接前驱）替代该结点，然后从二叉排序树中删除这个直接后继（或直接前驱）</strong>，这样就转换成了第一或第二种情况。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (target.left == <span class="keyword">null</span> &amp;&amp; target.right == <span class="keyword">null</span>) &#123; <span class="comment">// 3.如果目标结点是叶子结点</span></span><br><span class="line">    ...</span><br><span class="line">&#125; <span class="keyword">else</span> <span class="keyword">if</span> (target.left != <span class="keyword">null</span> &amp;&amp; target.right != <span class="keyword">null</span>) &#123; <span class="comment">// 4.如果目标结点有两个子结点</span></span><br><span class="line">    <span class="comment">// 获取右子树中序第一个子结点的值并删除该结点</span></span><br><span class="line">    <span class="keyword">int</span> val = delRightSubTreeFirstNodeByInOrder(target.right);</span><br><span class="line">    <span class="comment">// 将要被删除的结点值改为右子树中序第一个子结点的值</span></span><br><span class="line">    target.val = val;</span><br><span class="line">&#125; <span class="keyword">else</span> &#123; <span class="comment">// 5.如果目标结点只有一个子结点</span></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中获取右子树中序第一个子结点的方法如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">delRightSubTreeFirstNodeByInOrder</span><span class="params">(TreeNode node)</span> </span>&#123;</span><br><span class="line">    TreeNode target = node;</span><br><span class="line">    <span class="comment">// 循环的查找左子节点，就会找到最小值</span></span><br><span class="line">    <span class="keyword">while</span> (target.left != <span class="keyword">null</span>) &#123;</span><br><span class="line">        target = target.left;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 保存结点的值</span></span><br><span class="line">    <span class="keyword">int</span> value = target.val;</span><br><span class="line">    <span class="comment">// 删除该结点</span></span><br><span class="line">    deleteNode(value);</span><br><span class="line">    <span class="comment">// 返回被删除结点的值</span></span><br><span class="line">    <span class="keyword">return</span> value;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上述算法删除有两个子结点的结点的过程如下图所示：</p><p><img src="https://cdn.frankfang.cn/img/b7caf598-fe04-4b78-b048-d43264011b3f.png" alt="1616848045267.png"></p><h3 id="三、算法分析"><a href="#三、算法分析" class="headerlink" title="三、算法分析"></a>三、算法分析</h3><p><strong>二叉排序树的查找效率，主要取决于树的高度</strong>。若二叉排序树的左、右子树的高度之差的绝对值不超过 1，则这样的二叉排序树称为<strong>平衡二叉树</strong>，它的平均查找长度为$O(log_2n)$。若二叉排序树是一个只有右（左）孩子的单支树（类似于有序的单链表），则其平均查找长度为$O(n)$。</p><p>从查找过程看，二叉排序树与二分查找相似。就平均时间性能而言，二叉排序树上的查找和二分查找差不多。但二分查找的判定树唯一，而二叉排序树的查找不唯一，相同的关键字其插入顺序不同可能生成不同的二叉排序树，如下图所示：</p><p><img src="https://cdn.frankfang.cn/img/e1b4b2ef-466b-4bd5-9408-fbe9da311d0d.png" alt="1616848744097.png"></p><p>就维护表的有序性而言，二叉排序树无需移动结点，只需修改指针即可完成插入和删除操作，平均执行时间为$O(log_2n)$。二叉查找的对象是有序顺序表，若有插入和删除结点的操作，所花的代价是$O(n)$。当有序表是静态查找表时，宜用顺序表作为其存储结构，而采用二分查找实现其查找操作；若有序表是动态查找表，则应选择二叉排序树作为其逻辑结构。</p>]]></content>
    
    
    <summary type="html">本文主要介绍了二叉排序树的基本概念和相关操作</summary>
    
    
    
    <category term="数据结构" scheme="https://blog.frankfang.cn/categories/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    
    <category term="数据结构" scheme="https://blog.frankfang.cn/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    <category term="树" scheme="https://blog.frankfang.cn/tags/%E6%A0%91/"/>
    
    <category term="二叉树" scheme="https://blog.frankfang.cn/tags/%E4%BA%8C%E5%8F%89%E6%A0%91/"/>
    
  </entry>
  
  <entry>
    <title>图</title>
    <link href="https://blog.frankfang.cn/posts/202103147.html"/>
    <id>https://blog.frankfang.cn/posts/202103147.html</id>
    <published>2021-03-18T13:21:16.000Z</published>
    <updated>2021-03-18T13:21:16.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="一、图的基本概念"><a href="#一、图的基本概念" class="headerlink" title="一、图的基本概念"></a>一、图的基本概念</h3><h4 id="1、基本定义"><a href="#1、基本定义" class="headerlink" title="1、基本定义"></a>1、基本定义</h4><p>图（Graph）G 由两个集合 V 和 E 组成，记为 G = ( V, E )，其中 V 是顶点的有穷非空集合，E 是 V 中顶点偶对的有穷集合，这些顶点偶对称为边。V(G )和 E(G) 通常分别表示图 G 的顶点集合和边集合，E(G) 可以为空集。若 E(G) 为空，则图 G 只有顶点而没有边。</p><p><img src="https://cdn.frankfang.cn/img/b0602c55-185b-4ad1-9c05-102116238f66.png" alt="1616065134026.png"></p><p>对于图 G，若边集 E(G) 为有向边的集合，则称该图为有向图；若边集 E(G) 为无向边的集合，则称该图为无向图。</p><h4 id="2、相关术语"><a href="#2、相关术语" class="headerlink" title="2、相关术语"></a>2、相关术语</h4><p>数据结构中图的相关术语与离散数学中的基本相同，这里不再赘述。</p><h3 id="二、图的存储结构"><a href="#二、图的存储结构" class="headerlink" title="二、图的存储结构"></a>二、图的存储结构</h3><h4 id="1、邻接矩阵"><a href="#1、邻接矩阵" class="headerlink" title="1、邻接矩阵"></a>1、邻接矩阵</h4><p><strong>邻接矩阵（Adjacency Matrix）是表示顶点之间相邻关系的矩阵</strong>。设 G(V, E) 是具有 n 个顶点的图，则 G 的邻接矩阵是具有如下性质的 n 阶方阵：</p><p><img src="https://cdn.frankfang.cn/img/cd3e3470-38c6-41b9-9d98-49bba35368b2.png" alt="1616066296866.png"></p><p>例如本文第一张图中的 G1 和 G2 的邻接矩阵如下所示：</p><p><img src="https://cdn.frankfang.cn/img/38799844-6091-4883-841f-b7dc9c3bea7a.png" alt="1616066397429.png"></p><p>对于带权图来说，若顶点 vi 和 vj 之间有边相连，则邻接矩阵中对应项存放着该边对应的权值，若顶点 Vi 和 Vj 不相连，则用 ∞ 来代表这两个顶点之间不存在边：</p><p><img src="https://cdn.frankfang.cn/img/7ee79f2a-af56-4056-a438-47d3e900dc58.png" alt="1616067377293.png"></p><p>其中，Wij表示边上的权值；∞ 表示计算机允许的、大于所有边上权值的数。例如，下图为一个有向网和它的邻接矩阵：</p><p><img src="https://cdn.frankfang.cn/img/45e1c829-a3b5-4f95-906d-17699b56ccb7.png" alt="1616067632022.png"></p><p>用邻接矩阵表示法表示图，除了一-个用于存储邻接矩阵的二维数组外,还需要用一个一维数组来存储顶点信息。其形式说明如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">define</span> MAXINT 0x7FFF <span class="comment">// 表示极大值</span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> MVNum 100 <span class="comment">// 最大顶点数</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">char</span> VerTexType; <span class="comment">// 假设顶点的数据类型为字符型</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">int</span> ArcType; <span class="comment">// 假设边的权值类型为整型</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    VerTexType vexs[MVNum]; <span class="comment">// 顶点表</span></span><br><span class="line">    ArcType arcs[]; <span class="comment">// 邻接矩阵</span></span><br><span class="line">    <span class="keyword">int</span> vexnum, arcnum; <span class="comment">// 图的当前点数和边数</span></span><br><span class="line">&#125;AMGraph;</span><br></pre></td></tr></table></figure><h4 id="2、邻接表"><a href="#2、邻接表" class="headerlink" title="2、邻接表"></a>2、邻接表</h4><p><strong>邻接表（Adjacency List）是图的一种链式存储结构</strong>。在邻接表中，对图中每个顶点 Vi 建立一个单链表，把与 Vi 相邻接的顶点放在这个链表中。邻接表中每个单链表的第一个结点存放有关顶点的信息，把这一结点看成链表的表头，其余结点存放有关边的信息，这样邻接表便由两部分组成：表头结点表和边表。</p><ul><li><p>表头结点表：由所有表头结点以顺序结构的形式存储，以便可以随机访问任一顶点的边链表。表头结点包括数据域和链域两部分，如下图 (a) 所示。其中，数据域用于存储顶点 Vi 的名称或其他有关信息；链域用于指向链表中第-一个结点（即与顶点 V1 邻接的第一个邻接点）。</p></li><li><p>边表：由表示图中顶点间关系的 2n 个边链表组成。边链表中边结点包括邻接点域、数据域和链域三部分，如下图 (b) 所示。其中，邻接点域指示与顶点 Vi 邻接的点在图中的位置；数据域存储和边相关的信息，如权值等；链域指示与顶点 Vi 邻接的下一条边的结点。</p></li></ul><p><img src="https://cdn.frankfang.cn/img/b913d97d-1c7f-474e-9d9c-4b34acab9454.png" alt="1616068547814.png"></p><p>例如，下图 (a) 和 (b) 所示分别为本文第一张图中 G1 和 G2 的邻接表。</p><p><img src="https://cdn.frankfang.cn/img/fd025033-be5a-4fe3-9b59-098cc7464fad.png" alt="1616068698164.png"></p><p>图的邻接表存储结构说明如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">define</span> MVNum 100 <span class="comment">// 最大顶点数</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 边结点</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">ArcNode</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    <span class="keyword">int</span> adjvex; <span class="comment">// 该边所指向的顶点的位置</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">ArcNode</span>* <span class="title">nextarc</span>;</span> <span class="comment">// 指向下一条边的指针</span></span><br><span class="line">    <span class="comment">// 其它相关信息</span></span><br><span class="line">    ...</span><br><span class="line">&#125;ArcNode;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 顶点信息</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">VNode</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    VerTexType data;</span><br><span class="line">    ArcNode* firstarc; <span class="comment">// 指向第一条依附该顶点的边的指针</span></span><br><span class="line">&#125;VNode, AdjList[MVNum]; <span class="comment">// AdjList 表示邻接表类型</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 邻接表</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    AdjList vertices;</span><br><span class="line">    <span class="keyword">int</span> vexnum, arcnum; <span class="comment">// 图的当前顶点数和边数</span></span><br><span class="line">&#125;ALGraph;</span><br></pre></td></tr></table></figure><h3 id="三、图的遍历"><a href="#三、图的遍历" class="headerlink" title="三、图的遍历"></a>三、图的遍历</h3><h4 id="1、深度优先搜索"><a href="#1、深度优先搜索" class="headerlink" title="1、深度优先搜索"></a>1、深度优先搜索</h4><h5 id="（1）简介"><a href="#（1）简介" class="headerlink" title="（1）简介"></a>（1）简介</h5><p><strong>深度优先搜索（Depth First Search, DFS）遍历类似于树的先序遍历，是树的先序遍历的推广</strong>。对于一个连通图，深度优先搜索遍历的过程如下：</p><ul><li><p>从图中某个顶点 v 出发，访问 v。</p></li><li><p>找出刚访问过的顶点的第一个未被访问的邻接点，访问该顶点。以该顶点为新顶点，重复此步骤，直至刚访问过的顶点没有未被访问的邻接点为止。</p></li><li><p>返回前一个访问过的且仍有未被访问的邻接点的顶点，找出该顶点的下一个未被访问的邻接点，访问该顶点。</p></li><li><p>重复步骤 (2) 和 (3)，直至图中所有顶点都被访问过，搜索结束。</p></li></ul><p>以下图 (a) 中所示的无向图 G4 为例，深度优先搜索遍历图的过程如下图 (b) 所示，具体过程如下：</p><p><img src="https://cdn.frankfang.cn/img/29975b73-545f-4590-bd7e-78663bef0293.png" alt="1616070565995.png"></p><ul><li><p>从顶点 v1 出发，访问 v1。</p></li><li><p>在访问了顶点 v1 之后，选择第一个未被访问的邻接点 v2，访问 v2。以 v2 为新顶点，重复此步，访问 v4、v8、v5。 在访问了 v5 之后，由于 v5 的邻接点都已被访问，此步结束。</p></li><li><p>搜索从 v5 回到 v8，由于同样的理由，搜索继续回到 v4，v2 直至 v1，此时由于 v1 的另一个邻接点未被访问，则搜索又从 v1 到 v3，再继续进行下去。由此，得到的顶点访向序列为：<br>$$<br>v_1 \to v_2 \to v_4 \to v_8 \to v_5 \to v_3 \to v_6 \to v_7<br>$$</p></li></ul><h5 id="（2）算法实现"><a href="#（2）算法实现" class="headerlink" title="（2）算法实现"></a>（2）算法实现</h5><p>深度优先搜索可以通过递归进行实现，算法如下：</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">bool</span> visited[MVNum]; <span class="comment">// 访问标志数组，初值为 false</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">DFS</span><span class="params">(AMGraph G, <span class="keyword">int</span> v)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="comment">// 访问第 v 个结点</span></span><br><span class="line">    visit(v);</span><br><span class="line">    <span class="comment">// 置访问标志数组相应分量值为 true</span></span><br><span class="line">    visited[v] = <span class="literal">true</span>;</span><br><span class="line">    <span class="comment">// 依次检查邻接矩阵v所在的行</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">int</span> w = <span class="number">0</span>; w &lt; G.vexnum; w++)</span><br><span class="line">        <span class="keyword">if</span> ((G.arcs[v][w] != <span class="number">0</span>) &amp;&amp; (!visited[w])) <span class="comment">// G.arcs[v][w] != 0 表示 w 是 v 的邻接点</span></span><br><span class="line">            DFS(G, w); <span class="comment">// 如果 w 未访问则递归调用DFS（本算法也可以用栈来实现）</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="2、广度优先搜索"><a href="#2、广度优先搜索" class="headerlink" title="2、广度优先搜索"></a>2、广度优先搜索</h4><h5 id="（1）简介-1"><a href="#（1）简介-1" class="headerlink" title="（1）简介"></a>（1）简介</h5><p><strong>广度优先搜索（Breadth First Search, BFS）遍历类似于树的按层次遍历的过程</strong>。广度优先搜索遍历的过程如下。</p><ul><li><p>从图中某个顶点 v 出发，访问 v。</p></li><li><p>依次访问 v 的各个未曾访问过的邻接点。</p></li><li><p>分别从这些邻接点出发依次访问它们的邻接点，并使“先被访问的顶点的邻接点先于后被访问的顶点的邻接点被访问。重复步骤 (3)，直至图中所有已被访问的顶点的邻接点都被访问到。</p></li></ul><p>例如，对图G4进行广度优先搜索遍历的过程如下图所示：</p><p><img src="https://cdn.frankfang.cn/img/12eca855-2371-4bc3-8a09-4acf13963a3e.png" alt="1616072439876.png"></p><p>具体过程如下：</p><ul><li><p>从顶点 v1 出发，访问 v1。</p></li><li><p>依次访问 v1 的各个未曾访问过的邻接点以和 v3。</p></li><li><p>依次访问 v2 的邻接点 v4 和 v5，以及 v3 的邻接点 v6 和 v7，最后访问 v4 的邻接点 v8。由于这些顶点的邻接点均已被访问，并且图中所有顶点都被访问，由此完成了图的遍历。得到的顶点访问序列为：<br>$$<br>v_1 \to v_2 \to v_3 \to v_4 \to v_5 \to v_6 \to v_7 \to v_8<br>$$</p></li></ul><h5 id="（2）算法实现-1"><a href="#（2）算法实现-1" class="headerlink" title="（2）算法实现"></a>（2）算法实现</h5><p>广度优先搜索可以通过队列来实现，算法如下：</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">bool</span> visited[MVNum]; <span class="comment">// 访问标志数组，初值为 false</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">BFS</span><span class="params">(AMGraph G, <span class="keyword">int</span> v)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="comment">// 访问第 v 个结点</span></span><br><span class="line">    visit(v);</span><br><span class="line">    <span class="comment">// 置访问标志数组相应分量值为 true</span></span><br><span class="line">    visited[v] = <span class="literal">true</span>;</span><br><span class="line">    <span class="comment">// 辅助队列Q初始化</span></span><br><span class="line">    InitQueue(Q);</span><br><span class="line">    <span class="comment">// v 进队</span></span><br><span class="line">    EnQueue(Q, v);</span><br><span class="line">    <span class="comment">// 队列非空</span></span><br><span class="line">    <span class="keyword">while</span>(!isEmpty(Q))</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="comment">// 队头元素出队并置为u</span></span><br><span class="line">        DeQueue(Q,u);</span><br><span class="line">        <span class="comment">// 依次检查 u 的所有邻接点 w，FirstAdjVex(G,u) 表示 u 的第一个邻接点</span></span><br><span class="line">        <span class="comment">// NextAdjVex(G,u,w) 表示 u 相对于 w 的下一个邻接点，w &gt;= 0 表示存在邻接点</span></span><br><span class="line">        <span class="keyword">for</span>(w = FirstAdjVex(G,u); w &gt;= <span class="number">0</span>; w = NextAdjVex(G,u,w))</span><br><span class="line">            <span class="keyword">if</span> (!visited[w])</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="comment">// 访问w</span></span><br><span class="line">                visit(w);</span><br><span class="line">                <span class="comment">// 置访问标志数组相应分量值为 true</span></span><br><span class="line">                visited[w] = <span class="literal">true</span>;</span><br><span class="line">                <span class="comment">// w 入队</span></span><br><span class="line">                EnQueue(Q,w);</span><br><span class="line">            &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">本文主要介绍图的基本概念</summary>
    
    
    
    <category term="数据结构" scheme="https://blog.frankfang.cn/categories/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    
    <category term="数据结构" scheme="https://blog.frankfang.cn/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    <category term="图" scheme="https://blog.frankfang.cn/tags/%E5%9B%BE/"/>
    
  </entry>
  
  <entry>
    <title>树与二叉树</title>
    <link href="https://blog.frankfang.cn/posts/202103200.html"/>
    <id>https://blog.frankfang.cn/posts/202103200.html</id>
    <published>2021-03-18T05:23:49.000Z</published>
    <updated>2021-03-27T13:01:39.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="一、树"><a href="#一、树" class="headerlink" title="一、树"></a>一、树</h3><h4 id="1、基本定义"><a href="#1、基本定义" class="headerlink" title="1、基本定义"></a>1、基本定义</h4><p><strong>树（Tree）是 n（n 大于等于 0）个结点的有限集</strong>。它或为空树（n = 0），或为非空树（n &gt; 0），对于非空树 T：</p><ul><li>有且仅有一个称之为根的结点。</li><li>除根结点以外的其余结点可分为 m（m &gt; 0）个互不相交的有限集 <em>T1，T2，…，Tm</em>，其中每一个集合本身又是一棵树，并称为根的子树。</li></ul><p>树具有以下特点：</p><ul><li>树的根结点没有前驱，除根结点外的所有结点有且仅有一个前驱。</li><li>树中所有结点可以有零个或多个后继。</li></ul><h4 id="2、相关术语"><a href="#2、相关术语" class="headerlink" title="2、相关术语"></a>2、相关术语</h4><p><img src="https://cdn.frankfang.cn/img/876f7bf5-d087-48fd-9809-1660187687e9.png" alt="树的逻辑结构"></p><ul><li><p><strong>结点</strong>：树中的一个独立单元。包含一个数据元素及若干指向其子树的分支，如上图 (b) 中的 A、B、C、D 等。</p></li><li><p><strong>结点的度</strong>：结点拥有的子树数称为结点的度。例如，A 的度为 3，C 的度为 1,， F 的度为 0。</p></li><li><p><strong>树的度</strong>：树的度是树内各结点度的最大值。上图 (b) 所示的树的度为 3。</p></li><li><p><strong>叶子</strong>：度为 0 的结点称为叶子或终端结点。结点 K、L、F、G、M、1、J 都是树的叶子。</p></li><li><p><strong>双亲和孩子</strong>：结点的子树的根称为该结点的孩子，相应地，该结点称为孩子的双亲。例如，B 的双亲为 A，B 的孩子有 E 和 F。</p></li><li><p><strong>兄弟</strong>：同一个双亲的孩子之间互称兄弟。例如，H、I 和 J 互为兄弟。</p></li><li><p><strong>层次</strong>：结点的层次从根开始定义起，根为第一层，根的孩子为第二层。树中任一结点的层次等于其双亲结点的层次加 1。</p></li><li><p><strong>堂兄弟</strong>：双亲在同一层的结点互为堂兄弟。例如，结点 G 与 E、F、H、I. J 互为堂兄弟。</p></li><li><p><strong>树的深度</strong>：树中结点的最大层次称为树的深度或高度。上图 (b) 所示的树的深度为 4。</p></li></ul><h4 id="3、树的性质"><a href="#3、树的性质" class="headerlink" title="3、树的性质"></a>3、树的性质</h4><p>树具有如下最基本的性质：</p><ul><li>树中的结点数等于所有结点的度数之和加 1。</li><li>度为 m 的树中第 i 层上至多有$m^{i-1}$个结点（i 大于等于 1）。</li><li>高度为 h 的 m 叉树至多有$(m^h - 1)/(m-1)$个结点。</li><li>具有 n 个结点的 m 叉树的最小高度为$\lceil log_m(n(m-1) + 1) \rceil$。</li></ul><h3 id="二、二叉树"><a href="#二、二叉树" class="headerlink" title="二、二叉树"></a>二、二叉树</h3><h4 id="1、基本定义-1"><a href="#1、基本定义-1" class="headerlink" title="1、基本定义"></a>1、基本定义</h4><p>二叉树（Binary Tree）是 n（n 大于等于 0）个结点的有限集。它或为空树（n = 0），或为非空树（n &gt; 0），对于非空树 T：</p><ul><li>有且仅有一个称之为根的结点。</li><li>除根结点以外的其余结点分别为两个互不相交的子集 T1 和 T2，分别称为 T 的左子树和右子树，且 T1 和 T2 本身又都是二叉树。</li></ul><p>换句话说，二叉树就是<strong>每个结点最多有两个子结点</strong>的树。下图将展示二叉树的几种基本形态：</p><p><img src="https://cdn.frankfang.cn/img/38a5d706-547d-465e-bf39-e4e4b5bca36e.png" alt="二叉树的几种基本形态"></p><h4 id="2、特殊的二叉树"><a href="#2、特殊的二叉树" class="headerlink" title="2、特殊的二叉树"></a>2、特殊的二叉树</h4><ul><li><p><strong>满二叉树</strong>：深度为 k 且含有 2^k-1 个结点的二叉树，即<strong>树中的每层都含有最多的结点</strong>。如下图所示是一棵深度为 4 的满二叉树。</p></li><li><p><strong>完全二叉树</strong>：高度为 h、有 n 个结点的二叉树，当且仅当其每个结点都与高度为 h 的满二叉树中编号为 1~n 的结点一一对应时，称为完全二叉树，如下图所示。</p><p><img src="https://cdn.frankfang.cn/img/aa96a3d8-d69c-4846-8a57-35af97dcd46d.png" alt="1615985010458.png"></p><p><img src="https://cdn.frankfang.cn/img/a30b4269-4ad7-417d-9d2b-c501d29edd61.png" alt="1616032775781.png"></p></li><li><p>二叉排序树：左子树上所有结点的关键字均小于根结点的关键字；右子树上的所有结点的关键字均大于根结点的关键字；左子树和右子树又各是一颗二叉排序树。</p></li><li><p>平衡二叉树：树上任一结点的左子树和右子树的深度之差不超过 1。</p></li></ul><h4 id="3、二叉树的性质"><a href="#3、二叉树的性质" class="headerlink" title="3、二叉树的性质"></a>3、二叉树的性质</h4><ul><li><p><strong>非空二叉树上的叶子结点数等于度为 2 的结点数加 1，即 n0 = n2 + 1</strong>。</p></li><li><p>非空二叉树上第 k 层上至多有$2^{k-1}$个结点（k≥1）。</p></li><li><p>高度为 h 的二叉树至多有$2^h - 1$个结点（h≥1）。</p></li><li><p>对完全二叉树有如下两条性质：</p><p><img src="https://cdn.frankfang.cn/img/3444b442-5033-46ec-b3f9-5149b05ffe90.png" alt="1616033122283.png"></p><p><img src="https://cdn.frankfang.cn/img/3c8bcef8-e374-4fcc-8269-9cb389d82c71.png" alt="1616033162594.png"></p></li></ul><h4 id="4、存储结构"><a href="#4、存储结构" class="headerlink" title="4、存储结构"></a>4、存储结构</h4><h5 id="（1）顺序存储"><a href="#（1）顺序存储" class="headerlink" title="（1）顺序存储"></a>（1）顺序存储</h5><p>顺序存储结构使用一组地址连续的存储单元来存储数据元素，为了能够在存储结构中反映出结点之间的逻辑关系，必须将二叉树中的结点依照一定的规律安排在这组单元中。</p><p>对于完全二叉树，只要从根起按层序存储即可，依次自上而下、自左至右存储结点元素，即将完全二叉树上编号为 i 的结点元素存储在如上定义的一维数组中下标为 i-1 的分量中。</p><p>对于一般二叉树，则应将其每个结点与完全二叉树上的结点相对照，存储在一维数组的相应分量中，以“0”表示不存在此结点。</p><p><img src="https://cdn.frankfang.cn/img/3f2a88be-8357-40fb-8624-3624fda0190b.png" alt="1616033393673.png"></p><p>由此可见，这种顺序存储结构仅适用于完全二叉树。因为，在最坏的情况下，一个深度为 k 且只有 k 个结点的单支树（树中不存在度为2的结点）却需要长度为 2^k - 1 的一维数组。这造成了存储空间的极大浪费，所以对于一般二叉树，更适合采取下面的链式存储结构。</p><h5 id="（2）链式存储"><a href="#（2）链式存储" class="headerlink" title="（2）链式存储"></a>（2）链式存储</h5><p>由于顺序存储的空间利用率较低，因此二叉树一般都采用链式存储，用链表结点来存储二叉树中的每个结点。在二叉树中，结点的结构通常包括若干数据域和若干指针域，二叉链表至少包含三个域：数据域、左指针域和右指针域。如下图所示：</p><p><img src="https://cdn.frankfang.cn/img/0e0ba14b-e251-4a01-bf0b-fe3794cf5553.png" alt="1616035289069.png"></p><p>用C语言可对二叉树进行如下定义：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">BiTNode</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    ElemType data; <span class="comment">// 数据域</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">BiTNode</span> *<span class="title">left</span>, *<span class="title">right</span>;</span> <span class="comment">// 左、右子结点指针</span></span><br><span class="line">&#125;BiTNode, *BiTree;</span><br></pre></td></tr></table></figure><h3 id="三、二叉树的遍历"><a href="#三、二叉树的遍历" class="headerlink" title="三、二叉树的遍历"></a>三、二叉树的遍历</h3><p><strong>遍历二叉树（traversing binary tree）</strong>是指按某条搜索路径巡访树中每个结点，使得每个结点均被访问一次，而且仅被访问一次。访问的含义很广，可以是对结点做各种处理，包括输出结点的信息，对结点进行运算和修改等。遍历二叉树是二叉树最基本的操作，也是二叉树其他各种操作的基础，遍历的实质是对二叉树进行线性化的过程，即遍历的结果是将非线性结构的树中结点排成一个线性序列。由于二叉树的每个结点都可能有两棵子树，因而需要寻找一种规律，以便使二叉树上的结点能排列在一个线性队列上，从而便于遍历。</p><p>遍历二叉树分为三种情况，分别是：<strong>先序遍历</strong>、<strong>中序遍历</strong>和<strong>后序遍历</strong>。例如这里有一个表达式 a + b * (c - d) - e / f 的二叉树：</p><p><img src="https://cdn.frankfang.cn/img/89412dea-8191-44e9-ba46-c9df5c0542f4.png" alt="1616036274804.png"></p><p>下面将分别介绍三种情况下对该二叉树的遍历结果。</p><h4 id="1、先序遍历"><a href="#1、先序遍历" class="headerlink" title="1、先序遍历"></a>1、先序遍历</h4><p>先序遍历算法的步骤为：</p><ul><li>访问根结点</li><li>先序遍历左子树</li><li>先序遍历右子树</li></ul><p>该算法的实现如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">PreOrder</span><span class="params">(BiTree T)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line"><span class="keyword">if</span> (T != <span class="literal">NULL</span>)</span><br><span class="line">&#123;</span><br><span class="line"><span class="comment">// 访问根节点</span></span><br><span class="line">visit(T);</span><br><span class="line"><span class="comment">// 递归遍历左子树</span></span><br><span class="line">PreOrder(T-&gt;left);</span><br><span class="line"><span class="comment">// 递归遍历右子树</span></span><br><span class="line">PreOrder(T-&gt;right);</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>采用先序遍历的结果为：- + a * b - c d / e f，该结果即为波兰式。</p><h4 id="2、中序遍历"><a href="#2、中序遍历" class="headerlink" title="2、中序遍历"></a>2、中序遍历</h4><p>中序遍历算法的步骤为：</p><ul><li>中序遍历左子树</li><li>访问根结点</li><li>中序遍历右子树</li></ul><p>该算法的实现如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">InOrder</span><span class="params">(BiTree T)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line"><span class="keyword">if</span> (T != <span class="literal">NULL</span>)</span><br><span class="line">&#123;</span><br><span class="line"><span class="comment">// 递归遍历左子树</span></span><br><span class="line">InOrder(T-&gt;left);</span><br><span class="line"><span class="comment">// 访问根结点</span></span><br><span class="line">visit(T);</span><br><span class="line"><span class="comment">// 递归遍历右子树</span></span><br><span class="line">InOrder(T-&gt;right);</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>采用中序遍历的结果为：a + b * c - d - e / f，该结果即为中缀表达式。</p><h4 id="3、后序遍历"><a href="#3、后序遍历" class="headerlink" title="3、后序遍历"></a>3、后序遍历</h4><p>后序遍历算法的步骤为：</p><ul><li>后序遍历左子树</li><li>后序遍历右子树</li><li>访问根结点</li></ul><p>该算法的实现如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">PostOrder</span><span class="params">(BiTree T)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line"><span class="keyword">if</span> (T != <span class="literal">NULL</span>)</span><br><span class="line">&#123;</span><br><span class="line"><span class="comment">// 递归遍历左子树</span></span><br><span class="line">PostOrder(T-&gt;left);</span><br><span class="line"><span class="comment">// 递归遍历右子树</span></span><br><span class="line">PostOrder(T-&gt;right);</span><br><span class="line"><span class="comment">// 访问根结点</span></span><br><span class="line">visit(T);</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>采用后序遍历的结果为：a b c d - * + e f / -，该结果即为逆波兰式。</p><h3 id="四、线索二叉树"><a href="#四、线索二叉树" class="headerlink" title="四、线索二叉树"></a>四、线索二叉树</h3><p>遍历二叉树是以一定规则将二叉树中的结点排列成-一个线性序列，得到二叉树中结点的先序序列、中序序列或后序序列。这实质上是对一个非线性结构进行线性化操作，使每个结点（除第一个和最后一个外）在这些线性序列中有且仅有一个直接前驱和直接后继（在不至于混淆的情况）。</p><p>但是，当以二叉链表作为存储结构时，只能找到结点的左、右孩子信息，而不能直接得到结点在任一序列中的前驱和后继信息，这种信息只有在遍历的动态过程中才能得到，为此引人线索二叉树来保存这些在动态过程中得到的有关前驱和后继的信息。</p><p>虽然可以在每个结点中增加两个指针域来存放在遍历时得到的有关前驱和后继信息，但这样做使得结构的存储密度大大降低。由于有 n 个结点的二叉链表中必定存在 n+1 个空链域，因此可以充分利用这些空链域来存放结点的前驱和后继信息。试做如下规定：若结点有左子树，则其左指针域指示其左孩子，否则令左指针域指示其前驱；若结点有右子树，则其右指针域指示其右孩子，否则令右指针域指示其后继。为了避免混淆，尚需改变结点结构，增加两个标志域，其结点形式如下图所示。</p><p><img src="https://cdn.frankfang.cn/img/6646a394-f662-42ac-9305-7bfa48e702ef.png" alt="1616042940102.png"></p><p><img src="https://cdn.frankfang.cn/img/530e135f-d66b-49ba-a84c-e020b976f5bd.png" alt="1616043022962.png"></p><p>以这种结点结构构成的二叉链表作为二叉树的存储结构，叫做线索链表，其中指向结点前驱和后继的指针，叫做线索。加上线索的二叉树称之为<strong>线索二叉树（Threaded Binary Tree）</strong>。对二叉树以某种次序遍历使其变为线索二叉树的过程叫做线索化。</p><h3 id="五、树和森林"><a href="#五、树和森林" class="headerlink" title="五、树和森林"></a>五、树和森林</h3><p><strong>森林是 m (m≥1) 棵互不相交的树的集合</strong>。利用孩子兄弟表示法（二叉树表示法）可以实现树和森林的互相转换。</p><p><img src="https://cdn.frankfang.cn/img/329ecc96-8d64-4388-8757-444ebc54272a.png" alt="1616043754107.png"></p><p>这个一一对应的关系说明森林或树与二叉树可以相互转换。</p>]]></content>
    
    
    <summary type="html">本文主要介绍了树和二叉树的基本概念</summary>
    
    
    
    <category term="数据结构" scheme="https://blog.frankfang.cn/categories/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    
    <category term="数据结构" scheme="https://blog.frankfang.cn/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    <category term="树" scheme="https://blog.frankfang.cn/tags/%E6%A0%91/"/>
    
    <category term="二叉树" scheme="https://blog.frankfang.cn/tags/%E4%BA%8C%E5%8F%89%E6%A0%91/"/>
    
  </entry>
  
  <entry>
    <title>栈和队列</title>
    <link href="https://blog.frankfang.cn/posts/202103384.html"/>
    <id>https://blog.frankfang.cn/posts/202103384.html</id>
    <published>2021-03-17T11:54:15.000Z</published>
    <updated>2021-03-17T11:54:15.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="一、栈"><a href="#一、栈" class="headerlink" title="一、栈"></a>一、栈</h3><h4 id="1、基本概念"><a href="#1、基本概念" class="headerlink" title="1、基本概念"></a>1、基本概念</h4><p><strong>栈（Stack）是只允许在表尾进行插入和删除操作的线性表</strong>。对栈来说，表尾端称为<strong>栈顶（top）</strong>，表头端称为<strong>栈底（Bottom）</strong>。不含元素的空表称为<strong>空栈</strong>。栈的示意图如下：</p><p><img src="https://cdn.frankfang.cn/img/610efd3e-c5f7-44ea-bc17-d65e9f0218ca.png" alt="栈的逻辑结构"></p><p>栈的修改是按后进先出的原则进行的，因此栈又称为<strong>后进先出（Last In First Out, LIFO）</strong>的线性表。</p><p>栈有如下的基本操作：</p><table><thead><tr><th>操作</th><th>说明</th></tr></thead><tbody><tr><td><code>init(&amp;S)</code></td><td>初始化一个空栈</td></tr><tr><td><code>isEmpty(S)</code></td><td>判断一个栈是否为空，若为空返回<code>true</code>，否则返回<code>false</code></td></tr><tr><td><code>push(&amp;S, x)</code></td><td>进栈</td></tr><tr><td><code>pop(&amp;S, &amp;x)</code></td><td>出栈</td></tr><tr><td><code>getTop(S, &amp;x)</code></td><td>读栈顶元素</td></tr><tr><td><code>destroy(&amp;S)</code></td><td>销毁栈</td></tr></tbody></table><h4 id="2、存储结构"><a href="#2、存储结构" class="headerlink" title="2、存储结构"></a>2、存储结构</h4><p>栈是一种操作受限的线性表，类似于线性表，它也有对应的两种存储方式。</p><h5 id="（1）顺序栈"><a href="#（1）顺序栈" class="headerlink" title="（1）顺序栈"></a>（1）顺序栈</h5><p>采用顺序存储的栈称为顺序栈。它利用一组地址连续的存储单元存放自栈底到栈顶的数据元素，同时附设一个指针指向当前栈顶元素的位置。用C语言可对顺序栈作如下定义：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">define</span> MAXSIZE 50 <span class="comment">// 定义栈中元素的最大个数</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    ElemType data[MAXSIZE]; <span class="comment">// 存放栈中元素</span></span><br><span class="line">    <span class="keyword">int</span> top; <span class="comment">// 栈顶指针</span></span><br><span class="line">&#125;SqStack;</span><br></pre></td></tr></table></figure><p>下面对栈的一些基本操作进行实现：</p><ul><li><strong>初始化</strong></li></ul><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">init</span><span class="params">(SqStack &amp;S)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">S.top = <span class="number">-1</span>; <span class="comment">// 初始化栈顶指针</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注：栈顶指针的初始值除了可以设置为 -1 外，还可以设置为 0，这相当于规定栈顶指针指向栈顶元素的下一个存储单元。</p><ul><li><strong>判断栈是否为空</strong></li></ul><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">isEmpty</span><span class="params">(SqStack &amp;S)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (S.top == <span class="number">-1</span>) <span class="comment">// 栈空</span></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    <span class="keyword">else</span> <span class="comment">// 非空</span></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><strong>进栈</strong></li></ul><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">push</span><span class="params">(SqStack &amp;S, ElemType x)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (S.top == MAXSIZE - <span class="number">1</span>) <span class="comment">// 栈满，报错</span></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    S.data[++S.top] = x; <span class="comment">// 指针先加1，再入栈</span></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注：如果初始化时 top 为 0，则进栈时应先入栈，再将 top 加1。</p><ul><li><strong>出栈</strong></li></ul><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">pop</span><span class="params">(SqStack &amp;S, ElemType &amp;x)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (S.top == <span class="number">-1</span>) <span class="comment">// 栈空，报错</span></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    x = S.data[S.top--]; <span class="comment">// 先出栈，指针再减1</span></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注：如果初始化时 top 为 0，则 top 需要先减1再进行出栈操作。</p><ul><li><strong>读栈顶元素</strong></li></ul><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">getTop</span><span class="params">(SqStack S, ElemType &amp;x)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (S.top == <span class="number">-1</span>)</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    x = S.data[S.top];</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注：如果初始化时 top 为 0，则读取 top -1 位置的数据元素。</p><h5 id="（2）链栈"><a href="#（2）链栈" class="headerlink" title="（2）链栈"></a>（2）链栈</h5><p>采用链式存储的栈称为链栈，链栈的优点是便于多个栈共享存储空间和提高效率，且不存在栈满上溢的情况，通常采用单链表实现。用C语言可对链栈作如下定义：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">LinkNode</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    ElemType data; <span class="comment">// 存放栈中元素</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">LinkNode</span> *<span class="title">next</span>;</span> <span class="comment">// 栈顶指针</span></span><br><span class="line">&#125;*LiStack;</span><br></pre></td></tr></table></figure><p>链栈的操作于链表相似，这里不再赘述。</p><h3 id="二、队列"><a href="#二、队列" class="headerlink" title="二、队列"></a>二、队列</h3><h4 id="1、基本概念-1"><a href="#1、基本概念-1" class="headerlink" title="1、基本概念"></a>1、基本概念</h4><p><strong>队列（Queue）是一种操作受限的线性表，只允许在表的一端进行插入，而在另一端进行删除</strong>。向队列中插入元素称为入队或进队，删除元素称为出队或离队。队列的示意图如下：</p><p><img src="https://cdn.frankfang.cn/img/7697002f-a45c-41dc-908f-18dcb6129575.png" alt="队列的逻辑结构"></p><p>队列是一种<strong>先进先出（First In First Out, FIFO）</strong>的线性表。</p><p>下面给出队列的一些基本操作：</p><table><thead><tr><th>操作</th><th>说明</th></tr></thead><tbody><tr><td><code>InitQueue</code></td><td>初始化队列</td></tr><tr><td><code>QueueEmpty</code></td><td>判断队列是否为空，若队列为空返回<code>true</code>，否则返回<code>false</code></td></tr><tr><td><code>EnQueue</code></td><td>入队</td></tr><tr><td><code>DeQueue</code></td><td>出队</td></tr><tr><td><code>GetHead</code></td><td>读队头元素</td></tr></tbody></table><h4 id="2、存储结构-1"><a href="#2、存储结构-1" class="headerlink" title="2、存储结构"></a>2、存储结构</h4><h5 id="（1）顺序存储"><a href="#（1）顺序存储" class="headerlink" title="（1）顺序存储"></a>（1）顺序存储</h5><p>队列的顺序存储是指分配一块连续的存储单元存放队列中的元素，并附设两个指针：队头指针 front 指向队头元素，队尾指针 rear 指向队尾元素的下一个位置（也可以指向队尾元素）。用C语言可对队列作如下定义：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">define</span> MAXSIZE 50</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    ElemType data[MAXSIZE]; <span class="comment">//存放队列元素</span></span><br><span class="line">    <span class="keyword">int</span> front, rear; <span class="comment">// 队头指针和队尾指针</span></span><br><span class="line">&#125;SqQueue;</span><br></pre></td></tr></table></figure><p>为了在C语言中描述方便起见，在此约定：初始化创建空队列时，令 front = rear = 0，每当插入新的队列尾元素时，尾指针 rear 加 1；每当删除队列头元素时，头指针 front 加 1。因此，在非空队列中，头指针始终指向队列头元素，而尾指针始终指向队列尾元素的下一个位置，如下图所示。</p><p><img src="https://cdn.frankfang.cn/img/ea48e2c5-0915-4746-863d-b10c55876530.png" alt="队列的顺序存储"></p><p>假设当前队列分配的最大空间为 6，则当队列处于上图 (d) 所示的状态时不可再继续插入新的队尾元素，否则会出现溢出现象，即因数组越界而导致程序的非法操作错误。事实上，此时队列的实际可用空间并未占满，所以这种现象称为“假溢出”。这是由“队尾人队，队头出队”这种受限制的操作造成的。怎样解决这种“假溢出”问题呢? 一个较巧妙的办法是将顺序队列变为一个环状的空间，如下图所示，称之为循环队列。</p><p><img src="https://cdn.frankfang.cn/img/bc15a614-652a-4de6-a131-f9f7b884a2e5.png" alt="循环队列"></p><p>头、尾指针以及队列元素之间的关系不变，只是<strong>在循环队列中，头、尾指针“依环状增 1” 的操作可用“模”运算来实现</strong>。通过取模，头指针和尾指针就可以在顺序表空间内以头尾衔接的方式“循环”移动。</p><p><img src="https://cdn.frankfang.cn/img/16a7c00d-dd0b-4597-8e16-9364e58e57b9.png" alt="循环队列的存储结构"></p><ul><li>初始时：<code>Q.front = Q.rear = 0</code></li><li>入队：<code>Q.rear = (Q.rear + 1) % MAXSIZE</code></li><li>出队：<code>Q.front = (Q.front + 1) % MAXSIZE</code></li><li>队列长度：<code>(Q.rear + MAXSIZE - Q.front) % MAXSIZE</code></li></ul><p>由上图可知，队空和队满时都有 <code>Qfront == Q.rear</code>，那么该如何判断队列是否为空呢？下面介绍几种常见的解决方法：</p><ul><li><strong>牺牲一个单元来区分队空和队满</strong>，入队时少用一个队列单元，以<strong>队头指针在队尾指针的下一位置</strong>作为队满的标志，如上图 (d) 所示。</li><li>类型中增设表示元素个数的数据成员。</li><li>类型中增设 tag 数据成员，以区分是队满还是队空。</li></ul><p>下面对队列的一些基本操作进行实现：</p><ul><li><strong>初始化</strong></li></ul><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">InitQueue</span><span class="params">(SqQueue &amp;Q)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    Q.rear = Q.front = <span class="number">0</span>; <span class="comment">// 初始化队首、队尾指针</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><strong>判断队列是否为空</strong></li></ul><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">isEmpty</span><span class="params">(SqQueue Q)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">return</span> Q.rear == Q.front; <span class="comment">// 队空条件</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><strong>入队</strong></li></ul><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">EnQueue</span><span class="params">(SqQueue &amp;Q, ElemType x)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> ((Q.rear + <span class="number">1</span>) % MAXSIZE == Q.front) <span class="comment">// 队满则报错</span></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    Q.data[Q.rear] = x;</span><br><span class="line">    Q.rear = (Q.rear + <span class="number">1</span>) % MAXSIZE; <span class="comment">// 队尾指针加 1 取模</span></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><strong>出队</strong></li></ul><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">DeQueue</span><span class="params">(SqQueue &amp;Q, ElemType &amp;x)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (Q.rear == Q.front) <span class="comment">// 队空则报错</span></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    x = Q.data[Q.front];</span><br><span class="line">    Q.front = (Q.front + <span class="number">1</span>) % MAXSIZE; <span class="comment">// 队头指针加 1 取模</span></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h5 id="（2）链式存储"><a href="#（2）链式存储" class="headerlink" title="（2）链式存储"></a>（2）链式存储</h5><p>队列的链式表示称为链队列，它实际上是一个同时带有队头指针和队尾指针的单链表。头指针指向队头结点，尾指针指向队尾结点，即单链表的最后一个结点。用C语言可对链队作如下定义：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    ElemType data;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">LinkNode</span> *<span class="title">next</span>;</span></span><br><span class="line">&#125;LinkNode; <span class="comment">// 链式存储结点</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    LinkNode *front, *rear; <span class="comment">// 队列的队头和队尾</span></span><br><span class="line">&#125;LinkQueue; <span class="comment">// 链式队列</span></span><br></pre></td></tr></table></figure><p>链式队列的基本操作本质上与顺序队列相同，只是具体实现不同，这里不再进行赘述。</p>]]></content>
    
    
    <summary type="html">本文主要介绍了栈和队列的基本概念</summary>
    
    
    
    <category term="数据结构" scheme="https://blog.frankfang.cn/categories/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    
    <category term="数据结构" scheme="https://blog.frankfang.cn/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    <category term="栈" scheme="https://blog.frankfang.cn/tags/%E6%A0%88/"/>
    
    <category term="队列" scheme="https://blog.frankfang.cn/tags/%E9%98%9F%E5%88%97/"/>
    
  </entry>
  
</feed>
