您的位置:首页 > 其它

算法基础-->链表,堆栈,队列

2017-08-21 17:44 507 查看
从这篇博文开始,我将总结一些常用的传统算法的思想核心。本篇博文主要总结链表,堆栈,队列。

链表

链表相加

给定两个链表,分别表示两个非负整数。它们的数字逆序存储 在链表中,且每个结点只存储一个数字,计算两个数的和,并且返回和的链表头指针。

如:输入: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

)
{
int step
;//每个结点第几步到达
int pathNumber
;//到每个结点有几种走法
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

存储边权
//用indegree
存储每个结点入度

const int n = 16;
void topologic(int* toposort, int indegree
, int graph

)
{
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);

}
}
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: