算法基础-->链表,堆栈,队列
来源:互联网 发布:柯南帅气 知乎 编辑:程序博客网 时间:2024/06/05 05:18
从这篇博文开始,我将总结一些常用的传统算法的思想核心。本篇博文主要总结链表,堆栈,队列。
链表
链表相加
给定两个链表,分别表示两个非负整数。它们的数字逆序存储 在链表中,且每个结点只存储一个数字,计算两个数的和,并且返回和的链表头指针。
如:输入:2→4→3、5→6→4,输出:7→0→8
问题分析:因为两个数都是逆序存储,正好可以从头向后依次相加,完成“两个数的竖式计算”。
#include<stdio.h>#include <stdlib.h>using namespace std;typedef struct tagSNode{ int value; tagSNode* pNext; tagSNode(int v):value(v), pNext(NULL){};}SNode;SNode* Add(SNode* pHead1, SNode* pHead2){ SNode* pSum = new SNode(0); SNode* pTail = pSum; SNode* p1 = pHead1->pNext; SNode* p2 = pHead2->pNext; SNode* pCur; int carray = 0; int value; while (p1&& p2) { value = p1->value + p2->value + carray; carray = value / 10; value %= 10; pCur = new SNode(value); pTail->pNext = pCur; pTail = pCur; p1 = p1->pNext; p2 = p2->pNext; } //处理较长的连 SNode* p = p1 ? p1 : p2; while (p) { value = p->value + carray; carray = value / 10; value %= 10; pCur = new SNode(value); pTail->pNext = pCur; pTail = pCur; p = p->pNext; } //处理可能存在的进位 if (carray != 0) pTail->pNext = new SNode(carray); return pSum;}void Print(SNode* pHead){ pHead = pHead->pNext; printf("%d", pHead->value); pHead = pHead->pNext; while (pHead->pNext) { printf("->%d",pHead->value); pHead = pHead->pNext; } printf("\r\n");}void Destroy(SNode* p){ SNode* next; while (p) { next = p->pNext; delete p; p = next; }}int main(){ SNode* pHead1 = new SNode(0); int i; for (i = 0; i < 6; i++) { SNode* p = new SNode(rand() % 10); p->pNext = pHead1->pNext;//注意链表有个数值为0的头结点 pHead1->pNext = p; } SNode* pHead2 = new SNode(0); for (i = 0; i < 9; i++) { SNode* p = new SNode(rand() % 10); p->pNext = pHead2->pNext; pHead2->pNext = p; } Print(pHead1); Print(pHead2); SNode* pSum = Add(pHead1, pHead2); Print(pSum); Destroy(pHead1); Destroy(pHead2); Destroy(pSum); return 0;}
链表部分翻转
给定一个链表,翻转该链表从m到n的位置。要求直接翻转而非申请新空间。
如:给定1→2→3→4→5,m=2,n=4,返回 1→4→3→2→5。
假定给出的参数满足:1≤m≤n≤链表长度。
问题分析:空转m-1次,找到第m-1个结点,即开始翻转的第一个结点的前驱,记做head;以head为起始结点遍历n-m次,将第i次时,将找到的结点插入到head的next中即可。即头插法。
我们以:给定1→2→3→4→5,m=2,n=4,返回 1→4→3→2→5。为例,画图显示每一步的转换结果。
这里面思想并不是很难,难就难在coding时一定要注意每次在头插法时,其指针指向要正确改变。
由上图我们可知在coding时,需要标记以下几个结点:
在遍历n-m个结点时,在第i次时:
1. 第m-1个结点pPre,因为总是在它后面进行插入。
2. 对于第m+i个结点pCur,将pCur结点插入到pPre结点后。
3. 对于刚开始的第m个结点pFst,因为在每次插入后,pFst结点都要和pCur结点的后一个结点相连。
#include<stdio.h>#include <stdlib.h>using namespace std;typedef struct tagSNode{ int value; tagSNode* pNext; tagSNode(int v) :value(v), pNext(NULL){};}SNode;void Print(SNode* pHead){ pHead = pHead->pNext; printf("%d", pHead->value); pHead = pHead->pNext; while (pHead->pNext) { printf("->%d", pHead->value); pHead = pHead->pNext; } printf("\r\n");}void Destroy(SNode* p){ SNode* next; while (p) { next = p->pNext; delete p; p = next; }}void Reverse(SNode* pHead, int from, int to){ SNode* pCur = pHead->pNext; int i; SNode* pPre = pHead; for (i = 0; i < from - 1; i++) { pPre = pCur; pCur = pCur->pNext; } SNode* pFst = pCur; pCur = pCur->pNext; SNode* pNext; to--; for (; i < to; i++) { pNext = pCur->pNext; pCur->pNext = pPre->pNext; pPre->pNext = pCur; pFst->pNext = pNext; pCur = pNext; }}int main(){ SNode* pHead = new SNode(0); int i = 0; for (i = 0; i < 10; i++) { SNode*p = new SNode(rand() % 100); p->pNext = pHead->pNext; pHead->pNext = p; } Print(pHead); Reverse(pHead, 4, 8); Print(pHead); Destroy(pHead); return 0;}
链表划分
给定一个链表和一个值x,将链表划分成两部分,使得划分后小于x的结点在前,大于等于x的结点在后。在这两部分中要保持原链表中的出现顺序。
如:给定链表1→4→3→2→5→2和x = 3,返回1→2→2→4→3→5。
问题分析:分别申请两个指针p1和p2,小于x的添加到p1中,大于等于x的添加到p2中;最后,将p2链接到p1的末端即可。
#include<stdio.h>#include <stdlib.h>using namespace std;typedef struct tagSNode{ int value; tagSNode* pNext; tagSNode(int v) :value(v), pNext(NULL){};}SNode;void Print(SNode* pHead){ pHead = pHead->pNext; printf("%d", pHead->value); pHead = pHead->pNext; while (pHead->pNext) { printf("->%d", pHead->value); pHead = pHead->pNext; } printf("\r\n");}void Destroy(SNode* p){ SNode* next; while (p) { next = p->pNext; delete p; p = next; }}void Partition(SNode* pHead, int pivotKey){ SNode* LeftHead = new SNode(0); SNode* RightHead = new SNode(0); SNode* pCur = pHead->pNext; SNode* LeftTail = LeftHead; SNode* RightTail = RightHead; while (pCur) { if (pCur->value <= pivotKey) { LeftTail->pNext = pCur; LeftTail = LeftTail->pNext; } else { RightTail->pNext = pCur; RightTail = RightTail->pNext; } pCur = pCur->pNext; } pHead->pNext = LeftHead->pNext; LeftTail->pNext = RightHead->pNext; RightTail->pNext = NULL;//这一句很重要,如果没有这句代码,在输出链表时会源源不断的输出。 delete LeftHead; delete RightHead;}int main(){ SNode* pHead = new SNode(0); int i = 0; for (i = 0; i < 10; i++) { SNode*p = new SNode(rand() % 100); p->pNext = pHead->pNext; pHead->pNext = p; } Print(pHead); Partition(pHead, 50); Print(pHead); Destroy(pHead); return 0;}
时间复杂度是O(N),空间复杂度为O(1)
排序链表中去重
给定排序的链表,删除重复元素,只保留重复元素第一次出现的结点。
给定:2→3→3→5→7→8→8→8→9→9→10
返回:2→3→5→7→8→9→10
解法一:
问题分析:若p->next的值和p的值相等,则将p->next->next赋值给p,删除p->next;重复上述过程,直至链表尾端。
#include<stdio.h>#include <stdlib.h>using namespace std;typedef struct tagSNode{ int value; tagSNode* pNext; tagSNode(int v) :value(v), pNext(NULL){};}SNode;void Print(SNode* pHead){ pHead = pHead->pNext; printf("%d", pHead->value); pHead = pHead->pNext; while (pHead->pNext) { printf("->%d", pHead->value); pHead = pHead->pNext; } printf("\r\n");}void Destroy(SNode* p){ SNode* next; while (p) { next = p->pNext; delete p; p = next; }}void DeleteDuplicateNode(SNode* pHead){ SNode* pPre = pHead->pNext; SNode* pCur; while (pPre) { pCur = pPre->pNext; if (pCur && (pCur->value == pPre->value)) { pPre->pNext = pCur->pNext; delete pCur; } else { pPre = pCur; } }}int main(){ SNode* pHead = new SNode(0); int data[] = { 2, 3, 3, 5, 7, 8, 8, 8, 9, 9, 30 }; int size = sizeof(data) / sizeof(int); //当操作数具有数组类型时,其结果是数组的总字节数,而sizeof(int)=4 for (int i = size-1; i >= 0; i--) { SNode*p = new SNode(data[i]); p->pNext = pHead->pNext; pHead->pNext = p; } Print(pHead); DeleteDuplicateNode3(pHead); Print(pHead); Destroy(pHead); return 0;}
解法二:
void DeleteDuplicateNode2(SNode* pHead){ SNode* pPre = pHead; SNode* pCur = pPre->pNext; SNode* pNext; while (pCur) { pNext = pCur->pNext; while (pNext&&(pCur->value==pNext->value)) { pPre->pNext = pNext; delete pCur; pCur = pNext; pNext = pCur->pNext; } pPre = pCur; pCur = pNext; }}
咱们来比较下上面两种链表去重的解法,解法一很自然的在链表选取连续 两个结点,从左到右不停的滑动,每次滑动都会比较这连续两个结点的value值是否相等,如果相等则去掉后面一个结点。修改相关指针继续滑动;解法二中是从链表中选取三个结点,其中第二第三个结点相当于解法一中的两个结点,不断的滑动比较,第一分结点始终在这两个结点的前面一个结点。
相比较而言,解法二扩展性更好。
若题目变成:若发现重复元素,则重复元素全部删除,代码应该怎么实现呢?
给定:2→3→3→5→7→8→8→8→9→9→10
返回:2→5→7→10
对于这个变种题,上面的解法二稍微改下就可以解决这道题。因为解法二中标记了重复结点之前的一个结点。
void DeleteDuplicateNode3(SNode* pHead){ SNode* pPre = pHead; SNode* pCur = pPre->pNext; SNode* pNext; bool bDup; while (pCur) { pNext = pCur->pNext; bDup = false; while (pNext&&(pCur->value==pNext->value)) { pPre->pNext = pNext; delete pCur; pCur = pNext; pNext = pCur->pNext; bDup = true; } if (bDup)//如果此时pCur与原数据重复,删之 { pPre->pNext = pNext; delete pCur; } else //pCur未发现重复,则pPre后移 { pPre = pCur; } pCur = pNext; }}
链表总结:可以发现,纯链表的题目,往往不难,但需要需要扎实的Coding基本功,在实现过程中,要特别小心next的指向,此外,删除结点时,一定要确保该结点不再需要。
stack
堆栈是一种特殊的线性表,只允许在表的顶端top进行插入或者删除操作,是一种操作受限制的线性表。
栈元素服从后进先出原则: LIFO——Last In First Out
括号匹配问题
给定字符串,仅由”()[]{}”六个字符组成。设计算法,判断该字符串是否有效。括号必须以正确的顺序配对,如:“()”、“()[]”是有效的,但“([)]”无效。
括号匹配问题是堆栈里面一个经典应用。
算法分析:
在考察第i位字符c与前面的括号是否匹配时:
- 如果c为左括号,开辟缓冲区记录下来,希望c能够与后面出现的同类型最近右括号匹配。
- 如果c为右括号,考察它能否与缓冲区中的左括号匹配。
这个匹配过程,是检查缓冲区最后出现的同类型左括号。
即:后进先出——栈
算法步骤:
从前向后扫描字符串:
- 遇到左括号x,就压栈x,也即栈中只存左括号;
- 遇到右括号y:如果发现栈顶元素x和该括号y匹配,则栈顶元素出栈。
如果扫描到一个右括号则:
- 如果栈顶元素x和该右括号y不匹配,则返回结果字符串不匹配,退出程序;
- 如果栈为空,则返回结果字符串不匹配,退出程序;
- 扫描完成后,如果栈恰好为空,则字符串匹配,否则,字符串不匹配,退出程序。
#include<stdio.h>#include <stdlib.h>#include<stack>using namespace std;bool isLeft(char c){ return c == '(' || c == '[' || c == '{';}bool isMatch(char c, char d){ if (c == '(') return d == ')'; else if (c == '[') return d == ']'; else if (c == '{') return d == '}'; else { return false; }}bool Match(const char* p){ stack<char> s; char cur; while (*p) { cur = *p; if (isLeft(cur)) { s.push(cur); } else { if (s.empty() || !isMatch(s.top(), cur)) { return false;//return既有返回结果功能也有退出程序作用。 } s.pop();//匹配的话弹出栈顶元素。 } p++; } return s.empty();}int main(){ char *p = "(([])[]))[()]"; bool match = Match(p); if (match) printf("匹配!\n"); else { printf("不匹配!\n"); }}
最长括号匹配问题
给定字符串,仅包含左括号‘(’和右括号‘)’,它可能不是括号匹配的,设计算法,找出最长匹配的括号子串,返回该子串的长度。
如:
(():2
()():4
()(()):6
(()()):6
这里需要注意:是找找出最长匹配的括号子串,子串是字符串内某一段连续 的子字符串。所以这个匹配的字符串必须是连续,不能说前面有一对匹配,隔了几个字符后面又有一对或几对匹配,不能把这几个不连续的匹配求和作为结果。例如“()((())”最长匹配子串长度为4而不是6。
算法分析:
记起始匹配位置start=-1;最大匹配长度ml=0:
考察第 i(i从0开始)位字符c:
如果c为左括号,压栈;
如果c为右括号,它一定与栈顶左括号匹配;
- 如果栈为空,表示没有匹配的左括号,start=i,为下一次可能的匹配做准备。
如果栈不空,出栈(因为和c匹配了);
如果栈为空,i-start即为当前找到的匹配长度,检查i-start是否比ml更大,使得ml得以更新;
如果栈不空,则当前栈顶元素t是上次匹配的最后位置,检查i-t是否比ml更大,使得ml得以更新。
注:因为入栈的一定是左括号,显然没有必要将它们本身入栈,应该入栈的是该字符在字符串中的索引。
#include<stdio.h>#include <stdlib.h>#include<stack>using namespace std;int getLongestMatch(char* p){ int n = (int)strlen(p); int start = -1; int ml = 0; stack<int> s; for (int i = 0; i < n; i++) { if (p[i] == '(') { s.push(i); } else { if (s.empty()) { start = i; } else { s.pop(); if (s.empty()) { ml = ml>i - start ? ml : i - start; } else { ml = ml > i - s.top() ? ml : i - s.top(); } } } } return ml;}
堆栈和逆波兰表达式RPN
逆波兰表达式Reverse Polish Notation,又叫后缀表达式。
习惯上,二元运算符总是置于与之相关的两个运算对象之间,即中缀表达方法。波兰逻辑学家J.Lukasiewicz于1929年提出了运算符都置于其运算对象之后,故称为后缀表示。
如:
- 中缀表达式:a+(b-c)*d
- 后缀表达式:abc-d*+
从上面也可以看出:逆波兰表达式不需要带括号,自适应了优先级。
事实上,二元运算的前提下,中缀表达式可以对应一颗二叉树;逆波兰表达式即该二叉树后序遍历的结果。
计算给定的逆波兰表达式的值。有效操作只有+-*/,每个操作数都是整数。
如:
- “2”, “1”, “+”, “3”, “*”:9——(2+1)*3
- “4”, “13”, “5”, “/”, “+”:6——4+(13/5)
算法分析:
例如:abc-d*+
- 若当前字符是操作数,则压栈
- 若当前字符是操作符,则弹出栈中的两个操作数,计算后仍然压入栈中
- 若某次操作,栈内无法弹出两个操作数,则表达式有误。
#include<stdio.h>#include <stdlib.h>#include<stack>#include<string>using namespace std;bool istoken(const char* c){ return (c[0] == '+' || c[0] == '-' || c[0] == '*' || c[0] == '/');}int RPN(const char* p[],int n){ stack<int> s; int a, b; const char *c; for (int i = 0; i < n; i++) { c = p[i]; if (!istoken(c)) { s.push(atoi(c));//字符串转int数字 } else { a = s.top(); s.pop(); b = s.top(); s.pop(); if (c[0] == '+') s.push(a + b); else if (c[0] == '-') s.push(a - b); else if (c[0] == '/') s.push(a / b); else if (c[0] == '*') { s.push(a * b); } } } return s.top();}int main(){ const char* p[] = { "4", "13", "5", "/", "+" };//因为有两位数的数字,那么必须这样定义 int n = sizeof(p) / sizeof(const char*); int ml = RPN(p,n); printf("%d ", ml);}
queue(队列)
队列是一种特殊的线性表,只允许在表的前端front进行删除操作,在表的后端rear进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
队列元素服从先进先出原则: FIFO——First In First Out
队列与广度优先遍历:最短路径条数问题
给定如图所示的无向连通图,假定图中所有边的权值都为1,显然,从源点A到终点T的最短路径有多条,求不同的最短路径的数目。
通常涉及到最短路径问题,往往是一种广度优先搜索的应用。
算法分析:
权值相同的最短路径问题,则单源点Dijkstra算法 退化成BFS广度优先搜索,假定起点为0,终点为N:
结点步数step[0…N-1]初始化为0。
到每一个结点需要走的步数。路径数目pathNum[0…N-1]初始化为0。
到每一个结点可能会有多少种走法。pathNum[0] = 1。
最开始的结点走法即自己走到自己为1。
若从当前结点i扩展到邻接点 j 时,也即是结点 i 和结点 j 连通 情况下:
若step[ j ]为0,则
step[ j ]=step[ i ]+1,pathN[ j ] = pathN[ i ]若step[ j ]==step[ i ]+1,则
pathN[ j ] += pathN[ i ]
可考虑一旦扩展到结点N,则提前终止算法。
#include<stdio.h>#include <stdlib.h>#include<queue>using namespace std;const int N = 16;int Calc(int G[N][N]){ int step[N];//每个结点第几步到达 int pathNumber[N];//到每个结点有几种走法 memset(step, 0, sizeof(int)*N); memset(pathNumber, 0, sizeof(int)*N); pathNumber[0] = 1; queue<int> q;//当前搜索的结点,这里队列的作用就是从起点开始,然后不断寻找与其相连的结点,依次的循环, //依次的将这些结点push到队列中。然后又按照先进先出顺序不断的出队列。 q.push(0);//按照上图例子中,起点序号为0,终点序号15,故首先将起点结点加入队列,然后依次找相连结点。 int from, i, s; while (!q.empty()) { from = q.front(); q.pop(); s = step[from] + 1; for (i = 1; i < N; i++)//0是起点不遍历,每次弹出队列头部元素,与其他所有元素判断是否连通,这是典型的广度遍历。 { if (G[from][i] = 1)//连通 { //i尚未可达或发现更快的路 if (step[i] == 0 || step[i] > s) { step[i] = s; pathNumber[i] = pathNumber[from]; q.push(i);//因为第 i 个结点step=0,它应该没在队列中存在过,所以这里把它放进队列中。 } else if (step[i] == s) { pathNumber[i] += pathNumber[from]; } } } } return pathNumber[N - 1];}
队列与拓扑排序
对一个有向无环图(Directed Acyclic Graph,DAG)G进行拓扑排序,是将G中所有顶点排成线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。
一种可能的拓扑排序结果2->8->0->3->7->1->5->6->9->4->11->10->12
拓扑排序的方法:
- 从有向图中选择一个没有前驱(即入度为0)的顶点并且输出它;
- 从网中删去该顶点(出队列对头结点),并且删去从该顶点发出的全部有向边(与之相连的结点入度减一);
- 重复上述两步,直到剩余的网中不再存在没有前趋的顶点为止。
#include<stdio.h>#include <stdlib.h>#include<queue>using namespace std;//结点数为n,用邻接矩阵graph[n][n]存储边权//用indegree[n]存储每个结点入度const int n = 16;void topologic(int* toposort, int indegree[n], int graph[n][n]){ int cnt = 0; queue<int> q; for (int i = 0; i < n; i++) { if (!indegree[i]) q.push(i);//队列首先存储那些入度为0的结点。 } int cur; while (!q.empty()) { cur = q.front();//出队列获得头部第一个结点。 q.pop(); toposort[cnt++] = cur;//按出队列的顺序存进数组,因为是先进先出,所以先存的是有向边头头结点,自然先出的也是有向边头结点。这就保证拓扑排序存储顺序。 for (int i = 0; i < n; i++) { if (graph[cur][i])//每次出队列头结点,然后把这个头结点与其他所有结点进行判断是否连通,典型的广度遍历。 { indegree[i]--;//如果连通,则i这个结点的入度减一 if (toposort[i] == 0) q.push(i); } } }}
- 算法基础-->链表,堆栈,队列
- 【算法学习笔记】06.数据结构基础 队列与堆栈初步
- java算法-栈/队列/堆栈
- 链表,队列,堆栈
- 链表 堆栈 队列
- 停车场(队列堆栈基础练习)
- 数据结构与算法01--堆栈 & 队列
- 链表,队列,堆栈的区别
- 链表,队列,堆栈的区别
- 数组、链表、堆栈和队列
- 链表、堆栈、队列的区别
- 数组、链表、堆栈和队列
- 数组、链表、堆栈和队列
- 数组、链表、堆栈和队列
- 数组、链表、堆栈和队列
- 链表,队列,堆栈中的练习
- 数组、链表、堆栈和队列
- 链表、堆栈、队列的区别
- Day06 创建用户
- 多核CPU配合负载均衡可以这样用,为老板省点钱
- Executor框架
- [Elasticsearch in Action读书笔记]第一章 Elasticsearch介绍
- 拟阵基础与贪心算法
- 算法基础-->链表,堆栈,队列
- A
- 在线小说网站的设计与实现(附源码)
- 项目使用druid连接池
- 源码阅读分析
- 使用XIB, 如何让文字随心所欲的换行
- QT学习记录四
- Linux——终端快捷键
- 多种方法搞定反向查找