主要内容来自百度百科和百度文库
一、数学基础
一、对数
对数公式是数学中的一种常见公式,如果a^x=N(a>0,且a≠1),则x叫做以a为底N的对数,记做x=log(a)(N),其中a要写于log右下。其中a叫做对数的底,N叫做真数。通常我们将以10为底的对数叫做常用对数,以e为底的对数称为自然对数。
-
基本性质:
- l o g a 1 = 0 log_a^1 = 0 loga1=0
- l o g a a = 1 log_a^a=1 logaa=1
- 负 数 与 零 无 对 数 负数与零无对数 负数与零无对数
- l o g a b ∗ l o g b a = 1 log_a^b * log_b^a = 1 logab∗logba=1
- − l o g c a / b = l o g c b / a -log_c^{a/b} = log_c^{b/a} −logca/b=logcb/a
- a l o g a N = N ( a > 0 , a ≠ 1 ) a^{log_a^N} = N (a>0,a≠1) alogaN=N(a>0,a̸=1)
-
运算法则:
- l o g a M N = l o g a M + l o g a N log_a^{MN} = log_a^M + log_a^N logaMN=logaM+logaN
- l o g a M / N = l o g a M − l o g a N log_a^{M/N} = log_a^M - log_a^N logaM/N=logaM−logaN
- l o g a M N = N l o g a M ( M , N ∈ R ) log_a^{M^N} = Nlog_a^M (M,N ∈R) logaMN=NlogaM(M,N∈R)
-
换底公式:
证 明 : l o g a b = l o g c b / l o g c a 证明: log_a^b = log_c^b / log_c^a 证明:logab=logcb/logca
令 l o g a b = t . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ( 1 ) 令 log_a^b=t ................................(1) 令logab=t................................(1)
即 a t = b , 两 边 取 以 c ( c > 0 , c ≠ 1 ) 的 对 数 即 a^t=b,两边取以 c(c>0,c≠1)的对数 即at=b,两边取以c(c>0,c̸=1)的对数
即 l o g c a t = l o g c b , 即 t l o g c a = l o g c b 即 log_c^{a^t}=log_c^b,即 t log_c^a = log_c^b 即logcat=logcb,即tlogca=logcb
故 由 a ≠ 1 , 即 l o g c a ≠ 0 故由 a≠1,即log_c^a ≠ 0 故由a̸=1,即logca̸=0
即 t = l o g c b / l o g c a . . . . . . . . . . . . . . ( 2 ) 即 t=log_c^b / log_c^a ..............(2) 即t=logcb/logca..............(2)
由 ( 1 ) 与 ( 2 ) 知 l o g a b = l o g c b / l o g c a 由(1)与(2)知 log_a^b = log_c^b / log_c^a 由(1)与(2)知logab=logcb/logca
二、序列
-
数列An与Sn的关系
- n=1时,An=Sn
- n>1时,An=S(n+1)-Sn
-
等差等比数列
-
常见数列通项公式求法
二、 算法
一、算法的特征
-
有穷性(Finiteness)
算法的有穷性是指算法必须能在执行有限个步骤之后终止;
-
确切性 (Definiteness)
算法的每一步骤必须有确切的定义;
-
输入项 (Input)
一个算法有0个或多个输入,以刻画运算对象的初始情况,所谓0个输入是指算法本身定出了初始条件;
-
输出项 (Output)
一个算法有一个或多个输出,以反映对输入数据加工后的结果。没有输出的算法是毫无意义的;
-
可行性 (Effectiveness)
算法中执行的任何计算步骤都是可以被分解为基本的可执行的操作步,即每个计算步都可以在有限时间内完成(也称之为有效性)。
二、算法要素
- 数据对象的运算和操作:
- 算术运算:加减乘除等运算
- 逻辑运算:或、且、非等运算
- 关系运算:大于、小于、等于、不等于等运算
- 数据传输:输入、输出、赋值等运算
- 算法的控制结构:
一个算法的功能结构不仅取决于所选用的操作,而且还与各操作之间的执行顺序有关。
三、算法的评定
1. 正确性
算法的正确性是评价一个算法优劣的最重要的标准。
2. 可读性
算法的可读性是指一个算法可供人们阅读的容易程度。 [1]
3. 健壮性
健壮性是指一个算法对不合理数据输入的反应能力和处理能力,也称为容错性。
4. 算法的优劣:
同一问题可用不同算法解决,而一个算法的质量优劣将影响到算法乃至程序的效率。算法分析的目的在于选择合适算法和改进算法。一个算法的评价主要从 时间复杂度 和 空间复杂度 来考虑。
1、时间复杂度(Time complexity)
-
时间频度 T(n):
一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。算法的时间复杂度是指执行算法所需要的计算工作量。
-
时间复杂度:
在刚才提到的时间频度中,n称为问题的规模,当n不断变化时,时间频度T(n)也会不断变化。但有时我们想知道它变化时呈现什么规律。为此,我们引入时间复杂度概念。
一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),存在一个正常数c使得fn*c>=T(n)恒成立。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。
在各种不同算法中,若算法中语句执行次数为一个常数,则时间复杂度为O(1),另外,在时间频度不相同时,时间复杂度有可能相同,如T(n)=n2+3n+4与T(n)=4n2+2n+1它们的频度不同,但时间复杂度相同,都为O(n^2)。
-
按数量级递增排列,常见的时间复杂度有:
常数阶O(1),对数阶O(log2n)(以2为底n的对数,下同),线性阶O(n),
线性对数阶O(nlog2n),平方阶O(n2),立方阶O(n3),…,
k次方阶O(nk),指数阶O(2n)。随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。
-
如何推导出时间复杂度呢?有如下几个原则:
- 如果运行时间是常数量级,用常数1表示;
- 只保留时间函数中的最高阶项;
- 如果最高阶项存在,则省去最高阶项前面的系数
-
常见时间复杂度排序
O ( 1 ) < O ( l o g n ) < O ( n ) < O ( n 2 ) O(1)< O(log^n)< O(n)< O(n^2) O(1)<O(logn)<O(n)<O(n2)
2、空间复杂度(Space Complexity)
一个程序的空间复杂度是指运行完一个程序所需内存的大小,利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。程序执行时所需存储空间包括以下两部分
- 固定部分:这部分空间的大小与输入/输出的数据的个数多少、数值无关,主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间,这部分属于静态空间。
- 可变空间:这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等,这部分的空间大小与算法有关。一个算法所需的存储空间用f(n)表示。S(n)=O(f(n)),其中n为问题的规模,S(n)表示空间复杂度。
- 空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。
- 一个算法在计算机上占用的内存包括:程序代码所占用的空间、输入输出数据所占用的空间、辅助变量所占用的空间这三个方面。程序代码所占用的空间取决于算法本身的长短,输入输出数据所占用的空间取决于要解决的问题,是通过参数表调用函数传递而来,只有辅助变量是算法运行过程中临时占用的存储空间,与空间复杂度相关。
- 通常来说,只要算法不涉及到动态分配的空间以及递归、栈所需的空间,空间复杂度通常为0(1)。
- 算法的空间复杂度并不是计算实际占用的空间,而是计算整个算法的辅助空间单元的个数,与问题的规模没有关系。
三、常见排序算法
一、插入排序
-
时间复杂度 O ( n 2 ) O(n^2) O(n2)
-
空间复杂度 O ( 1 ) O(1) O(1)
-
是否稳定:稳定排序
-
代码
public static void insertSort(int arr[]) { int len = arr.length; if (arr == null || len < 2) { return; } /** 从第二个元素开始,与前边的元素比较,将该元素插入到合适的位置 * 每次排序后前i个元素为有序数列 */ for (int i = 1; i < len; i++) { int min = arr[i]; int beginIndex = i; while (beginIndex > 0 && min < arr[beginIndex - 1]) { arr[beginIndex] = arr[beginIndex - 1]; beginIndex--; } arr[beginIndex] = min; } }
二、冒泡排序
-
时间复杂度 O ( n 2 ) O(n^2) O(n2)
-
空间复杂度 O ( 1 ) O(1) O(1)
-
是否稳定:稳定排序
-
代码
public static void bubbleSort(int[] arr) { int len = arr.length; if (arr == null || len < 2) { return ; } int temp = 0; // 用来交换的临时数 // 要遍历的次数 for (int i=0; i < len-1; i++) { // 从后向前依次的比较相邻两个数的大小,遍历一次后,把数组中第i小的数放在第i个位置上 for (int j=len-1; j>i; j--) { // 比较相邻的元素,如果前面的数大于后面的数,则交换,稳定算法 if (arr[j-1] > arr[j]) { temp = arr[j-1]; arr[j-1] = arr[j]; arr[j] = temp; } } } }
三、快速排序
-
时间复杂度
平 均 情 况 : O ( n l o g 2 n ) , 最 坏 情 况 O ( n 2 ) , 最 好 情 况 O ( n l o g 2 n ) 平均情况:O(nlog_2 ^n),最坏情况O(n^2) ,最好情况O(nlog_2 ^n) 平均情况:O(nlog2n),最坏情况O(n2),最好情况O(nlog2n)
-
空间复杂度
O ( n l o g 2 n ) O(nlog_2^n) O(nlog2n)
-
是否稳定:不稳定排序
-
代码
-
挖坑法
/** * @param arr 待排序数组 * @param start 数组起始坐标 * @param end 数组结束坐标 */ @SuppressWarnings("unused") private static int[] quickSortDigging(int arr[], int start, int end) { int pivot = arr[start]; int i = start; int j = end; while (i < j) { while ((i < j) && (arr[j] > pivot)) { j--; } while ((i < j) && (arr[i] < pivot)) { i++; } if ((arr[i] == arr[j]) && (i < j)) { i++; } else { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } if (i - 1 > start) arr = quickSortDigging(arr, start, i - 1); if (j + 1 < end) arr = quickSortDigging(arr, j + 1, end); return (arr); }
-
指针交换法
/** * @param arr 待排序数组 * @param startIndex 数组起始坐标 * @param endIndex 数组结束坐标 */ public static void quickSortPointerExchange(int[] arr, int startIndex, int endIndex){ // 递归结束条件:startIndex大等于endIndex的时候 if (startIndex >= endIndex) return; // 得到基准元素位置 int pivotIndex = partition(arr, startIndex, endIndex); // 根据基准元素,分成两部分递归排序 quickSortPointerExchange(arr, startIndex, pivotIndex - 1); quickSortPointerExchange(arr, pivotIndex + 1, endIndex); } /** * 快速排序数组划分 * @param arr 待划分的数组 * @param startIndex 起始坐标 * @param endIndex 结束坐标 * @return 划分后基准元素的指针 */ private static int partition(int[] arr, int startIndex, int endIndex) { // 取第一个位置的元素作为基准元素 int pivot = arr[startIndex]; int left = startIndex; int right = endIndex; while (left != right) { // 控制right指针比较并左移 while (left < right && arr[right] > pivot) { right--; } // 控制right指针比较并右移 while (left < right && arr[left] <= pivot) { left++; } // 交换left和right指向的元素 if (left < right) { int p = arr[left]; arr[left] = arr[right]; arr[right] = p; } } // pivot和指针重合点交换 arr[startIndex] = arr[left]; arr[left] = pivot; return left; }
-
非递归实现法
/** * @param arr 待排序数组 * @param startIndex 数组起始坐标 * @param endIndex 数组结束坐标 */ public static void quickSortNonRecursive(int[] arr, int startIndex, int endIndex) { // 用一个集合栈来代替递归的函数栈 Stack<Map<String, Integer>> quickSortStack = new Stack<Map<String, Integer>>(); // 整个数列的起止下标,以哈希的形式入栈 Map rootParam = new HashMap(); rootParam.put("startIndex", startIndex); rootParam.put("endIndex", endIndex); quickSortStack.push(rootParam); // 循环结束条件:栈为空时结束 while (!quickSortStack.isEmpty()) { // 栈顶元素出栈,得到起止下标 Map<String, Integer> param = quickSortStack.pop(); // 得到基准元素位置 int pivotIndex = partition(arr, param.get("startIndex"), param.get("endIndex")); // 根据基准元素分成两部分, 把每一部分的起止下标入栈 if (param.get("startIndex") < pivotIndex - 1) { Map<String, Integer> leftParam = new HashMap<String, Integer>(); leftParam.put("startIndex", param.get("startIndex")); leftParam.put("endIndex", pivotIndex - 1); quickSortStack.push(leftParam); } if (pivotIndex + 1 < param.get("endIndex")) { Map<String, Integer> rightParam = new HashMap<String, Integer>(); rightParam.put("startIndex", pivotIndex + 1); rightParam.put("endIndex", param.get("endIndex")); quickSortStack.push(rightParam); } } } /** * 快速排序数组划分 * @param arr 待划分的数组 * @param startIndex 起始坐标 * @param endIndex 结束坐标 * @return 划分后基准元素的指针 */ private static int partition(int[] arr, int startIndex, int endIndex) { // 取第一个位置的元素作为基准元素 int pivot = arr[startIndex]; int left = startIndex; int right = endIndex; while (left != right) { // 控制right指针比较并左移 while (left < right && arr[right] > pivot) { right--; } // 控制right指针比较并右移 while (left < right && arr[left] <= pivot) { left++; } // 交换left和right指向的元素 if (left < right) { int p = arr[left]; arr[left] = arr[right]; arr[right] = p; } } // pivot和指针重合点交换 arr[startIndex] = arr[left]; arr[left] = pivot; return left; }
-
四、树
本部分参考:作者:MrHorse1992 链接:https://www.jianshu.com/p/bf73c8d50dc2 来源:简书
一、树的基础知识
树状图是一种数据结构,它是由n(n>=1)个有限结点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
- 每个结点有零个或多个子结点;
- 没有父结点的结点称为根结点;
- 每一个非根结点有且只有一个父结点;
- 除了根结点外,每个子结点可以分为多个不相交的子树;
-
定义
树(tree)是包含 n(n>=0)个结点的有穷集,其中:
(1)每个元素称为结点(node);
(2)有一个特定的结点被称为根结点或树根(root)。
(3)除根结点之外的其余数据元素被分为 m(m≥0)个互不相交的集合T1,T2,……Tm-1,其中每一个集合Ti(1<=i<=m)本身也是一棵树,被称作原树的子树(subtree)。
-
相关术语
- 节点的度:一个节点含有的子树的个数称为该节点的度;
- 叶节点或终端节点:度为0的节点称为叶节点;
- 非终端节点或分支节点:度不为0的节点;
- 双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;
- 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;
- 兄弟节点:具有相同父节点的节点互称为兄弟节点;
- 树的度:一棵树中,最大的节点的度称为树的度;
- 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
- 树的高度或深度:树中节点的最大层次;
- 堂兄弟节点:双亲在同一层的节点互为堂兄弟;
- 节点的祖先:从根到该节点所经分支上的所有节点;
- 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。
- 森林:由m(m>=0)棵互不相交的树的集合称为森林;
-
种类
- 无序树:树中任意节点的子结点之间没有顺序关系,这种树称为无序树,也称为自由树;
- 有序树:树中任意节点的子结点之间有顺序关系,这种树称为有序树;
- 二叉树:每个节点最多含有两个子树的树称为二叉树;
- 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。
- 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。
- 霍夫曼树:带权路径最短的二叉树称为哈夫曼树或最优二叉树;
-
符号表达法
用括号先将根结点放入一对圆括号中,然后把它的子树由左至右的顺序放入括号中,而对子树也采用同样的方法处理;同层子树与它的根结点用圆括号括起来,同层子树之间用逗号隔开,最后用闭括号括起来。如前文树形表示法可以表示为:(1(2(5(9,10)),3(6,7),4(8)))
二 、 二叉树
一、二叉树的基础知识
-
定义
二叉树是由n(n>=0)个结点组成的有序集合,集合或者为空,或者是由一个根节点加上两棵分别称为左子树和右子树的、互不相交的二叉树组成。
二叉树的五种形态:
-
特性
1)每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点。
2)左子树和右子树是有顺序的,次序不能任意颠倒。
3)即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。 -
性质
1)在二叉树的第i层上最多有2i-1 个节点 。(i>=1)
2)二叉树中如果深度为k,那么最多有2k-1个节点。(k>=1)
3)n0=n2+1 n0表示度数为0的节点数,n2表示度数为2的节点数。
4)在完全二叉树中,具有n个节点的完全二叉树的深度为[log2n]+1,其中[log2n]是向下取整。
5)若对含 n 个结点的完全二叉树从上到下且从左至右进行 1 至 n 的编号,则对完全二叉树中任意一个编号为 i 的结点有如下特性: (1) 若 i=1,则该结点是二叉树的根,无双亲, 否则,编号为 [i/2] 的结点为其双亲结点;
(2) 若 2i>n,则该结点无左孩子, 否则,编号为 2i 的结点为其左孩子结点;
(3) 若 2i+1>n,则该结点无右孩子结点, 否则,编号为2i+1 的结点为其右孩子结点。 -
斜树
所有的结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫右斜树。这两者统称为斜树。
-
满二叉树
在一棵二叉树中。如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。 满二叉树的特点有:
1)叶子只能出现在最下一层。出现在其它层就不可能达成平衡。
2)非叶子结点的度一定是2。
3)在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。 -
完全二叉树
对一颗具有n个结点的二叉树按层编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。完全二叉树有如下特点:
1)叶子结点只能出现在最下层和次下层。
2)最下层的叶子结点集中在树的左部。
3)倒数第二层若存在叶子结点,一定在右部连续位置。
4)如果结点度为1,则该结点只有左孩子,即没有右子树。
5)同样结点数目的二叉树,完全二叉树深度最小。
注:满二叉树一定是完全二叉树,但反过来不一定成立
一、 二叉树的存储
-
顺序存储
二叉树的顺序存储结构就是使用一维数组存储二叉树中的结点,并且结点的存储位置,就是数组的下标索引。
-
二叉链表
既然顺序存储不能满足二叉树的存储需求,那么考虑采用链式存储。由二叉树定义可知,二叉树的每个结点最多有两个孩子。因此,可以将结点数据结构定义为一个数据和两个指针域。
左孩子指针 数据域 右孩子指正 lchild data rchild
二、二叉树的遍历
-
定义
二叉树的遍历是指从二叉树的根结点出发,按照某种次序依次访问二叉树中的所有结点,使得每个结点被访问一次,且仅被访问一次。
-
前序遍历
前序遍历通俗的说就是从二叉树的根结点出发,当第一次到达结点时就输出结点数据,按照先向左在向右的方向访问。
遍历上图所示二叉树:
- 从根结点出发,则第一次到达结点A,故输出A;
- 继续向左访问,第一次访问结点B,故输出B;
- 按照同样规则,输出D,输出H;
- 当到达叶子结点H,返回到D,此时已经是第二次到达D,故不在输出D,进而向D右子树访问,D右子树不为空,则访问至I,第一次到达I,则输出I;
- I为叶子结点,则返回到D,D左右子树已经访问完毕,则返回到B,进而到B右子树,第一次到达E,故输出E;
- 向E左子树,故输出J;
- 照同样的访问规则,继续输出C、F、G;
上图所示二叉树的前序遍历输出为:ABDHIEJCFG
-
中序遍历
中序遍历就是从二叉树的根结点出发,当第二次到达结点时就输出结点数据,按照先向左在向右的方向访问。
遍历上图所示二叉树:
- 从根结点出发,则第一次到达结点A,不输出A,继续向左访问,第一次访问结点B,不输出B;
- 继续到达D,H;
- 到达H,H左子树为空,则返回到H,此时第二次访问H,故输出H;
- H右子树为空,则返回至D,此时第二次到达D,故输出D;
- 由D返回至B,第二次到达B,故输出B;
- 按照同样规则继续访问,输出J、E、A、F、C、G;
上图所示二叉树的前序遍历输出为:: HDIBJEAFCG
-
后序遍历
后序遍历就是从二叉树的根结点出发,当第三次到达结点时就输出结点数据,按照先向左在向右的方向访问。
遍历上图所示二叉树:
- 从根结点出发,则第一次到达结点A,不输出A,继续向左访问,第一次访问结点B,不输出B;
- 继续到达D,H;
- 到达H,H左子树为空,则返回到H,此时第二次访问H,不输出H;
- H右子树为空,则返回至H,此时第三次到达H,故输出H;
- 由H返回至D,第二次到达D,不输出D;
- 继续访问至I,I左右子树均为空,故第三次访问I时,输出I;
- 返回至D,此时第三次到达D,故输出D;
- 按照同样规则继续访问,输出J、E、B、F、G、C,A;
上图所示二叉树的前序遍历输出为:: H I D J E B F G C A
虽然二叉树的遍历过程看似繁琐,但是由于二叉树是一种递归定义的结构,故采用递归方式遍历二叉树的代码十分简单。
*二叉树的前序遍历递归算法*/ void PreOrderTraverse(BiTree T) { if(T==NULL) return; printf("%c", T->data); /*显示结点数据,可以更改为其他对结点操作*/ PreOrderTraverse(T->lchild); /*再先序遍历左子树*/ PreOrderTraverse(T->rchild); /*最后先序遍历右子树*/ } /*二叉树的中序遍历递归算法*/ void InOrderTraverse(BiTree T) { if(T==NULL) return; InOrderTraverse(T->lchild); /*中序遍历左子树*/ printf("%c", T->data); /*显示结点数据,可以更改为其他对结点操作*/ InOrderTraverse(T->rchild); /*最后中序遍历右子树*/ } /*二叉树的后序遍历递归算法*/ void PostOrderTraverse(BiTree T) { if(T==NULL) return; PostOrderTraverse(T->lchild); /*先后序遍历左子树*/ PostOrderTraverse(T->rchild); /*再后续遍历右子树*/ printf("%c", T->data); /*显示结点数据,可以更改为其他对结点操作*/ }
-
层次遍历
层次遍历就是按照树的层次自上而下的遍历二叉树.
上图所示二叉树的前序遍历输出为:ABCDEFGHIJ