一:算法分析
1:时间复杂度计算
T(n)=函数执行的最大次数。执行次数与输入规模有关。
原则:
1)去除常系数
2)同等数量级并且并列的则用 + 连接,并列的但数量级不同的取大者
3)嵌套的相乘
所以,我们先用 T(n)=K*t(f1(n))+M*t(f2(n))... 计算出算法执行的最大次数,然后运行化简法则化简:去常系数,和式取大,嵌套相乘,即可得到复杂度
2:空间代价
如果需要用到容器存放处理过程中的数据,那么我们可以根据输入规模n计算出空间开销代价与n的关系式。
二:树
1:遍历二叉树
前序:根左右
中序:左根右
后序:左右根
2:链表建立二叉树
每个结点一个node,有左右指针指向儿子
3:数组实现完全二叉树
n为结点总数,r为当前结点下标,则:
父节点为:floor[(r-1)/2]
左兄弟:r-1(r为偶数)
右兄弟:r+1(r为奇数)
左儿子:2r+1
右儿子:2r+2
4:遍历二叉树
5:由中序+前/后序遍历重建二叉树
6:BST的建立与使用
7:最大/最小堆的建立
8:哈夫曼编码树的建立及使用
9:计算树的高度
10:统计叶子数目
三:排序
三种复杂度为O(n^2)的排序算法:
1:插入排序
每次将一个待排序的数据,插入到前面已经排好序的序列之中,直到全部数据插入完成。i从0开始往后遍历,i之前有序,每次把下标i的元素前溯插入适当位置。时间复杂度:从前往后遍历i花费n,i从后往前遍历插入花费n。所以是O(n^2)。
2:冒泡排序
用两个下标i和j。i从前往后遍历,i前面有序。j从n-1向i+1遍历,每次比较j和j-1的值,把小的交换在前面,直到交换到i,此时则把无序区最小的元素交换到了有序区最后一位。由于排序过程是不停地把无序区小值往前传递,像泡泡一样,所以叫冒泡排序。时间复杂度:i从前往后遍历n次,j每次从后往前交换,嵌套的用相乘所以是O(n^2)
3:选择排序
选择排序是对冒泡排序的改进,冒泡排序在选择下一个最小值时需要不停交换到前面。选择排序则是先从n-1到i+1遍历一次,找到最小值的下标。然后再根据下标交换i和最小值。这样也是i从前到后遍历,j从后往前遍历,但是只交换一次。
更好的排序:
1:shell希尔排序
void shellsort3(int a[], int n) { int i, j, gap; for (gap = n / 2; gap > 0; gap /= 2) //循环到间距为1 for (i = gap; i < n; i++) //每次从gap开始,外层循环执行数据指向,移动一位相当于拿出一个新数据来插入 //用i实现不同分组的访问 for (j = i - gap; j >= 0 && a[j] > a[j + gap]; j -= gap) //从i-gap开始,由于i++,所以j是由0++开始,由j-=gap实现分组,相当于一个新数执行冒泡插入,每次与[j-gap]比较,交换,直到合适位置。 //用j实现一个分组内的跳跃访问 Swap(a[j], a[j + gap]); }
2:归并排序
归并排序,其的基本思路就是将数组分成二组A,B,如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序。如何让这二组组内数据有序了?
可以将A,B组各自再分成二组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的二个小组就可以了。这样通过先递归的分解数列,再合并数列就完成了归并排序。归并排序经过两步:划分(数组不停除以二,需要O(logn))然后合并(并列n个元素),故归并排序时间复杂度为:O(nlogn)
//将有二个有序数列a[first...mid]和a[mid...last]合并。 void mergearray(int a[], int first, int mid, int last, int temp[]) //实现比较后的“交换”,排序{ int i = first, j = mid + 1; int m = mid, n = last; int k = 0; while (i <= m && j <= n) { if (a[i] <= a[j]) //此处3个while实现比较并交换顺序。实则从first逐个与移动的mid比较,交换位置,一次移动mid一次排序完毕,最终实现左,右两边有序 temp[k++] = a[i++]; else temp[k++] = a[j++]; } while (i <= m) temp[k++] = a[i++]; while (j <= n) temp[k++] = a[j++]; for (i = 0; i < k; i++) //比较后结果覆盖到原数组,实现了相邻的两个元素排序 a[first + i] = temp[i]; } void mergesort(int a[], int first, int last, int temp[]) { if (first < last) { int mid = (first + last) / 2; //设立终止递归的flag mergesort(a, first, mid, temp); //左边有序 //不断递归修改first,last,mid,直到相邻比较,当相邻比较时,此句不执行,不再递归。由于此处的递归修改last,有mid+1=last,所以下一句不执行。 mergesort(a, mid + 1, last, temp); //右边有序 //同理,此句修改first,递归时不执行上一句 mergearray(a, first, mid, last, temp); //再将二个有序数列合并 //递归尽头的相邻的两个逐个与mid比较合并或最终数组的两边的逐个比较,合并 } }
3:快速排序
该方法的基本思想是:挖坑填数+分治法
1.先从数列中取出一个数作为基准数。
2.分区,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3.再对左右区间重复第一二步,直到各区间只有一个数。
对挖坑填数进行总结
1.i =L; j = R; 将基准数(第一个,最后一个,中间一个或者随机一个都可以)挖出(临时存放于temp)形成第一个坑a[i]。
2.j--由后向前找比temp小的数,找到后挖出此数填前一个坑a[i]中,形成新的坑。
3.i++由前向后找比temp大的数,找到后也挖出此数填到前一个坑a[j]中,形成新的坑。
4.再重复执行2,3二步,直到i==j,将基准数填入a[i]中,所有坑填完。
在最差情况下(选择了最大、最小值作为基准数,则i右前向后遍历、j每次都由后向前遍历一次,复杂度O(n^2));如果每次都是取到当前处理序列的中值,则只需递归logn次,每次交换时i、j共遍历n次,复杂度为O(nlogn)。
//快速排序 void quick_sort(int s[], int l, int r) { if (l < r) //执行分治的判断式,当l=r时,即说明不能再划分,不再递归 { int i = l, j = r, x = s[l]; //i,j分别从数组的左,右开始向中间递进,X作为暂时储存器存放基准数,此处把第一个元素作为基准数 while (i < j) //挖坑填数过程,当i==j时说明已全部遍历,停止循环 { while(i < j && s[j] >= x) // 从右向左找第一个小于x的数 j--; if(i < j) //当上述循环退出时,说明找到了小于基准数的元素,判断此时下标,若在i的右边,说明未遍历过,则填到[i]处 s[i++] = s[j]; while(i < j && s[i] < x) // 从左向右找第一个大于等于x的数 i++; if(i < j) //当上述循环退出时,说明找到了大于等于基准数的元素,判断此时下标,若在j的左边,说明未遍历过,则填到[j]处 s[j--] = s[i]; } s[i] = x; //当i==j时退出循环,此处填入中间数(基准数) quick_sort(s, l, i - 1); // 递归调用,对数组左右划分,直到相邻两元素比较交换,实现全部有序 注意:此处是先执行排序交换再递归的,而不是递归到底层再返回上来,函数没有return语句 quick_sort(s, i + 1, r); } }
4:堆排序
首先,根据原数组建立最大堆;
移除堆头,得到数组最大值,然后对剩下的堆重新建立一次形成新的最大堆;
重复第二步,依次得到次大值,......直到最小值。
总得来说堆排序就是:建立最大堆——移除堆头——重新建堆——移除堆头......
时间复杂度:建堆需要O(n)时间,每次取了堆头后堆的重塑需要O(logn)时间,n个结点所以共需要O(nlogn)时间。由化简原则,我们取最大值O(nlogn)。
四:检索
1:位图检索法
对于数值为元素的情况,检查一个数字是否属于某规则的集合,我们可以用位图来存储。创建一个二进制数组,如果元素n属于该规则的集合,我们就把bit[n]=1,否则设为0.在检索集合中的元素时直接遍历bit[]即可,为1则对应下标是集合的元素。
2:散列方法
散列:通过散列函数,把键值映射到数组中某个位置。由于object类自带hashCode()方法,所以每个key值都会有相对应的哈希值,映射到数组中的相应位置。
散列表:用于存放value值的数组成为散列表,每一个位置称为一个“槽”。我们由key经过散列函数得到哈希值就是key对应的value在散列表中的槽位下标。
冲突:当有两个不同的key的哈希值相同时,也就是说映射到散列表的同一个槽时,就发生了冲突。解决冲突有两种方法:
方法一:开散列方法:把散列表数组的每一个元素定义为一个链表的头,每个散列到该槽的哈希值作为一个结点插入到该槽指的链表处。
方法二:闭散列方法:分三种。
桶式散列:把散列数组分成N个桶,每个桶有相同数量的槽。我们由key经过散列函数得到的不是槽位而是桶位,依次把值插入桶中;
线性查探:key的映射位=hash(key)+P(key,i),如果经过hash(key)得到基槽为空,则放入基槽;如果已被占用,则使用 hash(key)+P(key,i)(i从1开始递增) 求下一个空槽,找到就插入,找不到就 ++i ,继续使用自定义的查探函数P(key,i)查找空槽。
双散列方法:当hash(key)找到的基槽被占用时,使用 P(key,i)=i*hash(key)(i从1自增) 依次查探空位,有则插入,无则 ++i,继续查探。
五:索引
1:2-3树
一个结点包含 一或两个关键码值
每个结点拥有两到三个子节点:如果当前结点有1个关键码值,则有两个子节点;如果有两个关键码值,则有三个子节点;
左子节点的关键码值 小于父节点的左关键码;中子结点的关键码值 介于父节点的左关键码值~右关键码值之间;右子节点的关键码值大于父节点的右关键码值;
2-3树的使用类似于二叉搜索树,每次与当前结点的关键码值比较大小然后决定左/中/右递归。
2:B树
一棵m阶B树为:
根要么是一个叶子结点,要么至少有两个子节点;
除了根结点,每个内部结点都有 m/2 ~ m个结点;
所有叶子在同一层,树的高度平衡;
子节点们的顺序参照2-3树,也是根据子节点内值的大小与父节点的值的大小比较来决定位置的。
1阶B树就是二叉搜索树:
3:B+树
B+树是基于B树的改进:只在叶结点存储数据,内部结点值只是用于引导检索的(决定往左还是往右递归检索)。而每一个叶结点都是一个有序数组序列。
六:图
1:图的相邻矩阵表示法
用一个n*n二维数组表示图,A[i][j]表示顶点i与顶点j直接有无边连接,有则填1,无则填0.
2:图的邻接表 表示法
一个链表数组,每个元素存放一个链表头,A[i]表示顶点i连接的结点们组成的链表。
3:图的遍历
DFS:用递归实现的深度优先搜索
BFS:用队列实现的宽度优先搜索,每次遍历该顶点的所有临接顶点;
4:图的最短路径
Dijkstra算法:从一个顶点开始,每次选择当前顶点最短的邻接边走,直到目标顶点
5:最小生成树(最小支撑树)
Prim算法:从一个顶点开始,每次选择当前顶点所以邻接边中权重最小的边来加入,逐步建立起一棵连接所有顶点的无环的树;
Kruskal算法:把顶点们分为V个等价类(V为顶点数),然后每次取连接两个等价类的最小权重边来连接两个等价类使其合并为一个等价类;依次逐步选边——合并,直到重构出一棵连接所有顶点的树。