1.算法基础

1. 从【插入排序】说起

插入排序(insert-sort)在生活中的一个应用场景就是玩扑克时,右手(不考虑左撇子)不断从桌上拿起一张扑克,按大小插入到左手的扑克序列的相应位置中。在结束时,左手的扑克是排好序的。

算法很简单,下面直接给出java实现代码:

/**
     * 插入排序
     *
     * @param a 需要排序的数组
     */
    public static void insertSort(int[] a) {
        for (int i = 1; i < a.length; i++) {
            int j = i - 1;
            // curr 表示要插入的扑克
            int curr = a[i];
            // 要插入的扑克从左手的扑克序列最右端不断向左移动
            // 直到到了尽头或者要插入的扑克比左手某一扑克大时,就停止移动
            while (j > -1 && curr < a[j]) {
                a[j + 1] = a[j];
                j--;
            }
            a[j + 1] = curr;
        }
    }

2. 理论证明

  • 循环不变试

在以上程序中,每轮循环结束(外层for循环)时,a[0~i](表示a数组的0~i位)都是排好序的,我们把a[0~i]的这一性质形式地表示为一个循环不变式

    循环不变式可用来帮助我们理解算法的正确性:

  • 初始化

循环第一次迭代之前,它为true;

  • 保持

如果循环迭代式某次迭代之前它为true,那么下次迭代之前它也为true;

  • 终止

在循环终止时,不变式为我们提供一个有用的性质,该性质有助于证明算法是正确的;

应用以上理论来验证以上的算法是否正确:

  • 初始化:在第一次迭代之前,子数组只有一位a[0],显然成立;
  • 保持:假设在第i次迭代之前,不变式成立,即子数组a[0~i-1]是排好序的;在进行第i次迭代时,根据算法,a[i]从子数组a[0~i-1]的最右端不断向左移动,直到找到自己合适的位置,此时子数组由a[0~i-1]扩充为a[0~i]。因此在进行第i+1次迭代之前,子数组a[0~i]也是排好序的。
  • 终止:外层for循环终止的条件是,i = a.length-1。根据保持性,此时子数组扩充为a[0~a.length-1],且是排好序的。

因此算法是正确的。

3. 算法分析

我们假定一种通用的单处理器计算模型——随机访问机(random-access machine,RAM)来作为实现技术的模型。在该模型中,一些常用的计算机指令,包括算术指令(加、减、乘、除、取余、向上取整、向下取整)、数据移动指令(装入、储存、复制)和控制指令(条件与无条件转移、子程序调用与返回),它们执行所需的时间为常量。

还是以上面的插入排序(insert-sort)为例来说明:

//(c, n),第一个参数表示该步执行一次的时间,第二个参数表示该步执行的次数。
        for (int i = 1; i < a.length; i++) { // (c1, n)
            int j = i - 1; //(c2, n-1)
            int curr = a[i]; // (c3, n-1)
            while (j > -1 && curr < a[j]) { // (c4, t2+t3+...+tn)
                a[j + 1] = a[j];// (c5, (t2-1)+(t3-1)+...+(tn-1))
                j--;// (c6, (t2-1)+(t3-1)+...+(tn-1))
            }
            a[j + 1] = curr;// (c7, n-1)
        }

计算总时间:

最佳情况:T(n) =c1 * n  + c2 * (n - 1) + c3 * (n - 1) + c4 * (n - 1) + c7 * (n - 1)= (c1 + c2 + c3 + c4 + c7) * n – (c2 + c3 + c4 + c7)

我们把T(n)表示为an+b,它是n的线性函数。

最坏情况:T(n) = c1 * n + (c2 + c3 + c7) * (n - 1) + c4 * [n(1 + n) / 2 - 1] + (c5 + c6) * [n(1 + n) / 2] = (c4 / 2 + c5 / 2 + c6 / 2) * n² + (c1 + c2 + c3 + c7 - c4 / 2 - c5 / 2 - c6 / 2) * n – (c2 + c3 + c7 + c4)

我们把T(n)表示为an^2 + bn + c,它是n的二次函数。

做更近一步的简化抽象,我们真正感兴趣的是运算时间的增长率增长量级)。因此忽略低阶项和最高阶项的常系数。记插入排序(insert-sort)的最佳情况运行时间为:θ(n),最坏情况运行时间为:θ(n²)。

对于插入排序(insert-sort),我们更应该考虑的是最坏情况,因为:① 最坏情况给出了一个上界,可以确保该算法不会超过某一时间;② 最坏情况往往经常出现;③ “平均情况”和最坏情况大致一样差。

4. 算法设计

插入排序(insert-sort)采用了增量方法,将a[i]插入子数组a[0~i-1]中,子数组增长为:a[0~i]。

下面介绍一种叫做分治法(Divide and Conquer)的设计方法:将原问题分解为几个规模较小的但类似于原问题的子问题,递归的解决这些子问题,然后合并这些子问题的解来建立原问题的解。

归并排序(merge-sort)完全遵循分治法(Divide and Conquer),操作步骤如下:

  • 分解 将待排序的具有n个元素的序列分成2个具有n / 2个元素的子序列;
  • 解决 使用归并排序递归的解决2个子序列;
  • 合并 合并已排序的2个子序列得到答案;

下面给出归并排序(merge-sort)的java实现代码:

/**
     * 将a[p~r]排序
     *
     * @param a
     * @param p
     * @param r
     */
    public static void mergeSort(int[] a, int p, int r) {
        if (p < r) {
            int q = (r + p) / 2;
            mergeSort(a, p, q);
            mergeSort(a, q + 1, r);
            merge(a, p, q, r);
        }
    }

    /**
     * 合并2个已排序的序列(a[p ~ q]和a[q+1 ~ r])
     *
     * @param a
     * @param p q >= p
     * @param q
     * @param r
     */
    public static void merge(int[] a, int p, int q, int r) {
        int[] a1 = new int[q - p + 2];
        int[] a2 = new int[r - q + 1];
        for (int i = 0; i < a1.length - 1; i++) {
            a1[i] = a[p + i];
        }
        a1[a1.length - 1] = Integer.MAX_VALUE;
        for (int i = 0; i < a2.length - 1; i++) {
            a2[i] = a[q + i + 1];
        }
        a2[a2.length - 1] = Integer.MAX_VALUE;
        int m = 0, n = 0;
        for (int i = p; i < r + 1; i++) {
            if (a1[m] < a2[n]) {
                a[i] = a1[m];
                m++;
            } else {
                a[i] = a2[n];
                n++;
            }
        }
    }

下面分析分治法(Divide and Conquer)

设T(n)为规模为n的问题运用分治法所需的运行时间。若问题规模足够小,如对于某个常量c,n <= c,则直接求解需要常量时间,记为:θ(1);否则,把问题分解成a个子问题,每个子问题的规模是原来的1 / b,则T(n) = a * T(n / b),如果分解为子问题所需时间为D(n) (可记为:θ(1)),合并子问题所需的时间为C(n)(可记为:θ(n)),那么T(n)递归式为:

特别的,对于归并排序(merge-sort)

可以求得,当n-> ∞时,T(n) = c * n * lg n + cn,记为:θ(n * lg n)。

可见当n较大时,在最坏情况下,归并排序(merge-sort)优于插入排序(insert-sort)

ps:以上内容均摘自《算法导论》中文译本。本人只是提取出文中个人认为比较重要的点,加入了一些个人理解,仅供参考。有些句子和词由于是翻译过来的,所以可能比较突兀,会意就好。

时间: 2024-10-19 02:45:07

1.算法基础的相关文章

python小白-day4递归和算法基础

递归&算法基础 一.递归 递归函数的优点是定义简单,逻辑清晰.理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰. 使用递归函数需要注意防止栈溢出.在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧.由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出. 1 2 3 4 5 6 7 8 def calc(n):     print(n)     if n/2>1:         r

算法——基础篇——快速排序

快速排序是一个经常使用的算法,由于每次用的时候,都感觉没有理解清楚,特写一篇文章记录一下. 算法介绍 快速排序有点类似有冒泡排序,冒泡排序从相邻的两个元素比较,小的在左边,大的在右边,这个算法很容易理解.而快速排序它相当于是在一头一尾两边分别排序比较,比较的对象是当前元素值,和一个选定的key值,主题的思想就是通过跟key值比较,把大于key的值放在右边,小于的放在左边这样就完成了一次排序,接着在对key值左边的序列进行同样的操作,右边也是,最后便能将所有的元素给排好序,由于它每次排序,都会分成

算法——基础篇——二分查找

     二分查找又称折半查找,优点是比较次数少,查找速度快,平均性能好:其缺点是要求待查表为有序表,且插入删除困难.因此,折半查找方法适用于不经常变动而查找频繁的有序列表.     首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功:否则利用中间位置记录将表分成前.后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表.重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功

第二章 算法基础 思考题2-1

package chap02; import static org.junit.Assert.*; import java.util.Arrays; import java.util.Random; import org.junit.Test; /*** * 在归并排序中对小数组采用插入排序 * * @author xiaojintao * */ public class ques2_1 { /** * 归并排序算法 * * @param a * @return */ static void m

[算法学习笔记]算法基础知识

算法基础知识 算法的五大要素 有穷性:算法必须能够在有限个步骤内完成. 确定性:算法的每一步必须有确定的定义. 输入 输出 可行性:算法的每个步骤都必须能分解为基本的可执行操作,每个步骤都必须能在有限时间内完成 循环不变式 循环中的循环不变式可以帮助我们理解算法的正确性.为了证明算法的正确,必须证明循环不变式的三个性质: 1. 初始化:循环不变式在循环开始之前是正确的. 2. 保持:循环不变式在循环的每一次迭代开始之前是正确的. 3. 终止:在循环结束时,不变式会给出一个可以对判断算法是否正确有

01.数据结构概念与算法基础

数据结构概念与算法基础 一.数据结构概念 1.数据:是描述客观事务的符号,是计算机中可以操作的对象,是能被计算机识别,并输入给计算机处理的符号集合.数据不仅仅包括整型.实型等数值类型,还包括字符及声音.图像.视频等非数值类型. 2.数据元素:是组成数据的.有一定意义的基本单位,在计算机中通常作为整体处理,也被成为记录.比如畜类中,牛.马.羊都属于数据元素. 3.数据项:一个数据元素可以由若干个数据项组成,数据项是数据不可分割的最小单位.比如人这样的数据元素,可以有眼.耳.鼻等数据项. 4.数据对

编程算法基础-一刀切法

1.5一刀切法 编程和编筐一样,处理边缘问题相同重要. 程序逻辑 開始 中间 结束 一刀切法上式 用空格或逗号分隔的串,提取为各个部分 abc ttt,kmd,uuu xyz 切分成每一部分. /* * "abc ttt,kmd,uuu xyz" 用逗号或者空格隔开字符串 分解为各个部分 */ package OneCut; public class SplitString { public static void main(String[] args) { // 一刀切(上式:事先补

编程算法基础-2.7作业-通讯编码-格式检查

作业 通信编码 如果通信的物理设备仅仅能表示1和0两种状态. 1和0状态都不能持续太久,否则物理设备会出现问题.因而人们设计出一种变通的方法: 多个0后人为地补入一个1 多个1后人为地补入一个0 当然,在解码的时候,要对应处理. 以下我们用串来模拟这个算法. 如果有须要通信的串: String s = "1010100100100001011110100010101010100001010101111"; 连续的3个0,后须要插入一个1 连续的3个1,后须要输入一个0 10101001

第二章 算法基础 思考题2-4(逆序对)

1 package chap02; 2 3 import static org.junit.Assert.*; 4 5 import java.util.Arrays; 6 7 import org.junit.Test; 8 9 public class ques2_4 { 10 /** 11 * 逆序对,将一个序列中的所有逆序对打印输出 12 * 13 * @author xiaojintao 14 * 15 */ 16 static void printReverseOrder(int[]

编程算法基础3.3-测试驱动风格

cocos2d-x升级到3.0后变化不小,除了API的变化(主要是函数和类名称变化,以及使用了C++11的不少特性,function/bind, lamda, std::thread-),创建和编译工程也做了一些简化调整.本文主要讨论一下cocos2d-x3.0 在android平台开发的环境设置及工程创建编译流程. 1.   初始设置 除了2.x所需要的python,jdk, android sdk和ndk之外,还需要部署apache-ant. 1)      在path中设置好java环境变