算法准备
1. 排序小结
1.1 插入排序
插入排序从前往后遍历,默认前面都是已经排好序了,找到没排好序的那位数字往前查找,如果找到一个比它大的就往后挪一位,直到某一位不符合条件了,就把这一位后面的那个数的值置为该数。
- 代码实现:
//插入排序
void insert(vector<int> &v) {
int len = v.size();
for (int i=1; i<len; i++) {
int temp = v[i], j=i-1;
while (j>=0 && temp<v[j]) {
v[j+1]=v[j];
j--;
}
v[j+1]=temp;
}
}
1.2 shell排序
shell排序是插排的一种改进,是通过找寻一个shell增量(默认是长度的两倍)开始,从该希尔增量往后不断进行插排,不过比较的不再是相邻的数,而是与该数相差一个希尔增量的前一个数,直到最左端为之。思路还是插排的思路,不过插排算法平均复杂度为O(n^2),而shell排序为O(nlogn),下面是代码实现;
- 代码实现
//希尔排序
void shell(vector<int> &v) {
int len = v.size();
int gap=len/2,temp;
while (gap>0) {
for (int i=gap; i<len; i++) {
temp = v[i];
int j=i-gap;
while (j>=0 && temp<v[j]) {
v[j+gap] = v[j];
j-=gap;
}
v[j+gap]=temp;
}
gap/=2;
}
}
1.3 归并排序
归并排序核心思想是分治法,每次都将数组分为两半,先排前面一半,再拍后面一半,最后将已经排好序的两部分再通过一个第三者数组过渡一下,排好序之再复制回原来的数组中,完成整体的排序。时间复杂度也是O(nlogn);
- 代码如下
//归并排序
void merge(vector<int> &v, int l, int r) {
if (l>=r) return;
int mid=l+(r-l)/2;
merge(v, l, mid);
merge(v, mid+1, r);
vector<int> temp;
int m=l, n=mid+1;
for (int i=0; i<r-l+1; i++) {
if (m>mid) temp.push_back(v[n++]);
else if(n>r) temp.push_back(v[m++]);
else if (v[m]<v[n]) temp.push_back(v[m++]);
else temp.push_back(v[n++]);
}
for (int i=0; i<temp.size(); i++)
v[i+l]=temp[i];
}
- 其实归并有两种,一种是自上而下的(递归),还有种是自下而上的(非递归),本处只体现了一种递归写法。
1.4 快速排序
快速排序实在太出名了,在这里也不再过多强调了。比如jdk中的Arrays.sort(T[])中就有采用快排(当然是在某一个阈值以上才采用,而且是双轴双向快排),快排的思想要深入理解了才能以不变应万变,比如理解单轴单向快排和单轴双向的区别,以及双轴双向快排的思想,以及面对链表如何进行快速排序?是值传递还是指针替换,都必须基于此。下面是几种几种实现方式:
- 代码实现
//快速排序,基于算导上的标准写法
void quicksort1(vector<int> &v, int l, int r) {
if (l>=r) return;
int privot=v[r], index=l-1;
for (int i=l; i<r; i++) {
if (v[i] < privot) {
int temp = v[index+1];
v[++index] = v[i];
v[i] = temp;
}
}
v[r]=v[++index];
v[index]=privot;
quicksort1(v, l, index-1);
quicksort1(v, index+1, r);
}
//单轴双向快排
void quicksort(vector<int> &v, int left, int right)
{
if(left < right)//false则递归结束
{
int key=v[left];//基数赋值
int low = left;
int high = right;
while(low < high) //当low=high时,表示一轮分割结束
{
while(low < high && v[high] >= key)//胜利[low]为基数,从后向前与基数比较
{
high--;
}
swap(v[low],v[high]);
while(low < high && v[low] <= key)//胜利[high]为基数,从前向后与基数比较
{
low++;
}
swap(v[low],v[high]);
}
//分割后,对每一分段重复上述操作
quicksort(v, left,low-1);
quicksort(v, low+1,right);
}
}
以及如果面对是链表时候的快排序,下面是交换数值的方法,还有不改变node的val属性,而且整个node都进行迁移,那个算法比较复杂,这里并没实现;
private static void quickSort(Node head) {
quickSortMain(head, null);
}
private static void quickSortMain(Node head, Node tail) {
if (head != tail) {
Node mid = getPartition(head, tail);
quickSortMain(head, mid);
quickSortMain(mid.next, tail);
}
}
private static Node getPartition(Node head, Node tail) {
int key = head.val;
Node p = head, q = p.next;
while (q != tail) {
if (q.val < key) {
p = p.next;
int temp = q.val;
q.val = p.val;
p.val = temp;
}
q = q.next;
}
int temp = p.val;
p.val = head.val;
head.val = temp;
return p;
}
1.5 堆排序算法
堆排序就是通过构建最大/小堆来进行排序,首先得理解最大/小堆的概念,然后知道如何构建,最大/小堆的调整算法,然后就是不断取值不断调整了。实现代码如下:
//堆排序
void heapsort(vector<int> &v) {
int len=v.size();
buildheap(v);
while(len>0) {
int temp=v[len-1];
v[len-1]=v[0];
v[0]=temp;
len--;
adjust(v, 0);
}
}
//建堆
void buildheap(vector<int> &v) {
for (int i=(v.size()-2)/2; i>=0; i--) {
adjust(v, i);
}
}
//调整算法
void adjust(vector<int> &v, int i) {
int maxIndex = i;
if (2*i < v.size() && v[2*i]<v[i]) maxIndex=2*i;
if (2*i+1<v.size() && v[2*i+1]<v[maxIndex]) maxIndex=2*i+1;
if (maxIndex != i) {
int temp = v[maxIndex];
v[maxIndex]=v[i];
v[i]=temp;
adjust(v, maxIndex);
}
}
- 首先最大堆或者最小堆算法,在jdk中就有应用,如PriorityQueue中优先队列就是基于最大/最小堆来实现的,里面的插入/取值之后都会进行不断调整,保证始终都可以获取到最大/小值。
- 堆排序常用来求算最大/小的k个数,建立最小/大根堆,然后遍历所有元素时如果大于/小于堆中的最小/大元素时就插入到堆中,等到插入元素个数到达k时,再插入一个就得吐出一个最值元素。保持堆中k个元素。
2. KMP算法
KMP算法是用来查找模式字符串pattern在目标字符串str中的位置(返回一个int数值)的一个高效算法,用过正则表达式表达式来匹配字符串的知道,通常要达到这个目的,str(长度n),pattern(长度m),最普遍的做法是从头开始查找,一旦不匹配str的下标重新返回到和pattern匹配的第一个位置,pattern同理,继续查找,总耗时O(n*m),但其实这时str可以保持不动,pattern字符只需要右移到某个位置即可,具体解释参考这几篇博客;如此一来的str字符串是保持一直往前匹配,pattern偶尔回溯,总耗时O(m+n),效率大大提高。
- 代码实现
//获取next数组的函数
void getNext(vector<int> &next, string &p) {
int len = p.size(), k=-1;
next.push_back(-1);
for (int i=1; i<p.size(); i++) {
while (k>-1 && p[k+1]!=p[i]) k=next[k];
if (p[k+1]==p[i]) k++;
next.push_back(k);
}
}
//KMP主函数
int kmp(string &str, string &p){
int i=0,j=0,slen=str.size(),plen=p.size();
vector<int> next;
getNext(next, p);
while (i<slen && j<plen) {
if (str[i]==p[j]) {
i ++;
j ++;
} else {
if (next[j] == -1) {
j == 0;
i++;
} else
j = next[j];
}
if (j==plen) return i-j;
}
return -1;
}
其实核心还是理解了一个字符串的最长前缀和最长后缀相同是如何求算的,也即getNext()函数。
3. 图算法
几乎在所有的图问题中,都会有两种方式的思路来解决存储图元素,然后基于此再进行下一步的操作。图元素包括点,边信息。
输入 n个顶点,m条边;
struct edge { int u, v; //两个顶点 int val; //边的长度 } int a[n][n]; //邻接矩阵 vector<edge> v[n]; //邻接表
- 邻接矩阵:
将边u-v的权值放入a[u][v]中,a[u][v]为0,而如果u,v之间无直达边,设为无穷大;- 邻接表:
仅仅存储所有存在的边信息,存为一个edge对象,放入v[u]中,如果是无向边,也放入v[v]中。
main函数如下:
#define maxn 9999 struct edge { int u, v; int val; }; vector<edge> v; void dfs(int,int, vector<int> &v); int a[100][100] = {maxn}; int main() { int n, m, u, v, val; scanf("%d%d", &n, &m); for (int i=1; i<=n; i++) { fill(a[i], a[i]+n+1, maxn); a[i][i]=0; } for (int i=0; i<m; i++) { scanf("%d%d%d", &u, &v, &val); a[u][v]=val; a[v][u]=val; } for (int i=1; i<=n; i++) { for (int j=1; j<=n; j++) { printf("%04d ", a[i][j]); } printf("\n"); } vector<int> b; b.resize(n+1, 0); dfs(1, n, b); fill(b.begin(), b.end(), 0); bfs(1, n, b); }
3.1 深度优先遍历
深度优先遍历,其实就是从起点开始遍历,从遍历第一个邻接点开始,每遍历到一个点,就依次遍历它的所有邻接点,没有则返回。
- 实现代码
void dfs(int s, int n, vector<int> &b) {
b[s]=1;
printf("深搜:遍历到了第%d个点\n", s);
for (int i=1; i<=n; i++) {
if (a[s][i]<maxn && !b[i])
dfs(i, n, b);
}
}
3.2 广度优先遍历
广搜的思路就是搜索一个点,就先把其所有邻接点都遍历完,接着按顺序再进行邻接点自身的邻接点的遍历;
void bfs(int s, int n, vector<int> &b) {
queue<int> q;
q.push(s);
b[s]=1;
while (!q.empty()) {
int d = q.front();
q.pop();
printf("广搜:遍历到了第%d个点\n", d);
for (int i=1; i<=n; i++) {
if (a[d][i]<maxn && !b[i]) {
q.push(i);
b[i] = 1;
}
}
}
}
3.3 拓扑排序
拓扑排序,就是找出一个所有顶点排布的顺序,使得按照在有向无环图中,不存在有任何一条边比如u->v,或者u-> …->v,有v排布顺序出现在u前面的情况出现;这个时候我们叫拓扑有序了。
拓扑排序的思想就是,先计算图中所有的点入度,然后找出入度为0的点,排在前面,每遍历一个入度为0的点,就删除该点,然后将他所有邻接点的入度减一处理。继续添加所有入度为0的点进队列中即可;
//拓扑排序时必须是有向无环图
void topology(int n) {
vector<int> degree;
degree.resize(n+1, 0);
//先初始化所有顶点的入度
for (int i=1; i<=n; i++) {
for (int j=1; j<=n; j++) {
if (a[i][j]<maxn && a[i][j]>0) degree[j]++;
}
}
queue<int> q;
//先找出所有的度为0的点加入队列
for (int i=1; i<=n; i++) {
if (!degree[i]) q.push(i);
}
while (!q.empty()) {
int t=q.front();
q.pop();
printf("拓扑排序:%d个顶点", t);
for (int i=1; i<=n; i++) {
if (a[t][i]<maxn && a[t][i]>0) {
degree[i]--;
if(!degree[i]) q.push(i);
}
}
}
}
- 拓扑排序必须保证图是有向无环图,因为无向图任何一条边已经成环,而加入有向图中存在环,那么拓扑排序得出的点数是小于总体的顶点数的,可以因此来检测是否存在环。
拓扑排序还存在两种拓展(或变形);
- 深搜中实现拓扑排序:
- 利用堆栈数组实现巧妙的拓扑排序:
3.4 弗洛伊德最短路径算法
最短路径算法,是用于解决一些实际问题而延申出来的。弗洛伊德最短路径算法,是用于求算,任意两点之间的最带路径距离的算法(路径不包含负权值),算法思路可以这样来概述:对于图中的任意两个点d1, d2之间的距离a[d1][d2],如果可以在经过第三方点d3进行过渡,使得满足a[d1][d3]+a[d3][d2]<a[d1][d2],那么我们就说点d3可以进行放缩,此时的d1和d2之间的距离可以进一步缩小。
因此要求出任意两点的最短距离,就得对每一个顶点都进行放缩处理。
- 下面是代码实现:
void floyd(int n) {
for (int i=1; i<=n; i++) {
for (int u=1; u<=n; u++) {
for (int v=1; v<=n; v++) {
if (a[u][i]+a[i][v]<a[u][v])
a[u][v] = a[u][i]+a[i][v];
}
}
}
}
- 只有短短五行代码,可以说是非常简便了,这样的话就可以根据a[u][v]二维数组得出任意两个顶点的最短距离来了。
- 该算法时间复杂度是O(n^3);
3.5 Dijkstra最短路径算法
如果说floyd算法是用来求算任意两个点之间的最短路径算法的话,那么Dijkstra算法就是用来求算单源最短路径的所有最短路径,即起点s到图中任意一个点的最短距离。算法的核心思想是通过求算一个以s为起点的最短路径树,并不断扩展到所有的顶点去为止;维持一个距离数组,保存的都是每个点到起点的距离。每次都找数组中还没加入到最短路径树中最距离起点最短的那个点加入到最短距离树中,接着以它为松弛点,对dis数组中所有的顶点到起点的距离进行松弛。
- 代码实现
void dijkstra(int s, int n) {
vector<int> dis;
dis.resize(n+1, maxn);
dis[s]=0;
vector<bool> b;
b.resize(n+1, false);
for (int i=1; i<=n; i++) {
int u=-1, min=maxn;
for (int j=1; j<=n; j++) {
if (!b[j] && dis[j]<min) {
min=dis[j];
u=j;
}
}
if (u==-1) printf("图并非连通...");
b[u]=true;
for (int j=1; j<=n; j++) {
if (dis[u]+a[u][j]<dis[j]) dis[j]=dis[u]+a[u][j];
}
}
}
- 其实dijkstra算法,可以算是floyd算法的一种拓展;都是基于点的松弛技术,不过floyd算法,是针对每一个点,对图中的n*n条边都进行松弛,松弛的顺序是乱序的,但是最终得到的结果必定是最短的结果。而对于单源的最短路径来说,采用floyd算法时,必须确定点松弛的顺序,这个顺序确保,就是找到离这颗最短路径树最近的点即可。
- Dijkstra算法无法处理负边;
3.6 最小生成树算法
4. 树算法
4.1 哈夫曼树编码算法
public class HuffmanTree {
public static class Node implements Comparable<Node>{
int weight;
String str;
String code;
Node left;
Node right;
public Node(String str, int weight) {
this.str = str;
this.weight = weight;
}
@Override
public int compareTo(Node o) {
return weight - o.weight;
}
}
public static Node createHuffmanTree(String[] strs, int[] weights) {
PriorityQueue<Node> queue = new PriorityQueue<>();
for (int i = 0; i < strs.length; i++) {
queue.offer(new Node(strs[i], weights[i]));
}
while (queue.size() >= 2) {
Node left = queue.poll();
Node right = queue.poll();
Node root = new Node("", left.weight + right.weight);
root.left = left;
root.right = right;
queue.offer(root);
}
return queue.peek();
}
public static Map<String, String> getCode(Node root) {
Map<String, String> map = new HashMap<>();
LinkedList<Node> deque = new LinkedList<>();
deque.addLast(root);
root.code = "";
Stack<String> stack = new Stack<>();
while (!deque.isEmpty()) {
root = deque.removeFirst();
if (!root.str.equals("")) {
stack.push(root.str);
}
if (root.left != null) {
root.left.code = root.code + "0";
deque.addLast(root.left);
}
if (root.right != null) {
root.right.code = root.code + "1";
deque.addLast(root.right);
}
}
return map;
}
public static void main(String[] args) {
String[] strs = {"A", "B", "C", "D", "E"};
int[] weights = {5, 4, 3, 2, 1};
Node root = createHuffmanTree(strs, weights);
Map<String, String> map = getCode(root);
for (String str : strs) {
System.out.println(str + " : " + map.get(str));
}
}
}