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

来源:互联网 发布:柯南帅气 知乎 编辑:程序博客网 时间: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为右括号,考察它能否与缓冲区中的左括号匹配。

这个匹配过程,是检查缓冲区最后出现的同类型左括号。
即:后进先出——栈

算法步骤:

从前向后扫描字符串:

  1. 遇到左括号x,就压栈x,也即栈中只存左括号;
  2. 遇到右括号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*+

  1. 若当前字符是操作数,则压栈
  2. 若当前字符是操作符,则弹出栈中的两个操作数,计算后仍然压入栈中
  3. 若某次操作,栈内无法弹出两个操作数,则表达式有误。
#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:

  1. 结点步数step[0…N-1]初始化为0。
    到每一个结点需要走的步数。

  2. 路径数目pathNum[0…N-1]初始化为0。
    到每一个结点可能会有多少种走法。

  3. 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

拓扑排序的方法:

  1. 从有向图中选择一个没有前驱(即入度为0)的顶点并且输出它;
  2. 从网中删去该顶点(出队列对头结点),并且删去从该顶点发出的全部有向边(与之相连的结点入度减一);
  3. 重复上述两步,直到剩余的网中不再存在没有前趋的顶点为止。
#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);            }        }    }}
原创粉丝点击