数据结构与算法(三)

双向链表

链表由单向的链变成双向链,使用这种数据结构,我们不再拘束于单链表的单向创建于遍历等操作。

img

在单链表中,有一个数据域,还有一个指针域,数据域用来存储相关数据,而指针域负责链表之间的“联系”。在双向链表中,需要有两个指针域,一个负责向后连接,一个负责向前连接

//单链表的结构
struct List{
    int data;	//数据域
    struct List *next;	//指针域
};

//双向链表的结构
typedef struct List{
    int data;		//数据域
    struct List *next;	//向后的指针
    struct Lsit *front;		//向前的指针
};

typedef struct List* pElem;
typedef struct List eElem;

同单链表一样,对双向链表的操作也有创建,插入,遍历,删除,销毁。

双向链表的创建

在创建双向链表的时候,需要初始化两个指针。同单链表一样,需要一个头指针来标志链表的信息。可以写出该函数的定义:

pElem CreateList(){
    pElem head = (pElem)malloc( sizeof(eElem) );
    assert(head != NULL );		//包含于标准库<assert.h>
    head -> next = head -> front = NULL;		//初始化链表,指针置空
    return head;
}

双向链表的插入

在单向链表的头插法中,最主要的语句就是tmp->next = head->next,而在双向链表中,多了一个向前的指针。

void InsertElem( pElem head, int data ){
    if ( head == NULL ){
        printf("The list is empty.\n");		//头结点为空,无法插入
        return;
    }
    
    pElem tmpHead = head;	//创建一个临时的头结点指针
    if( tmpHead->next ==NULL ){
        //当双向链表只有一个头结点时
		pElem addition = (pElem)malloc( sizeof(eElem) );
		assert( addition != NULL );
		addition->data = data;		//数据域赋值
		addition->next = tmpHead->next;	//后向指针连接
		tmpHead->next = addition;
		addition->front = tmpHead;		//将插入结点的front指向头结点
    }
	else{
		//当双向链表不只一个头结点时
		pElem addition = (pElem)malloc( sizeof(eElem) );
		assert( addition != NULL );
		addition -> data = data;		//数据域赋值
		tmpHead->next->front = addition;		//头结点的下一个结点的front指针
		addition->front = tmpHead;		//插入的结点的front指针指向头结点
		addition->next = tmpHead->next;		//插入结点的next 指针指向原本头指针的下个结点
		tmpHead->next = addition;		//将头结点的next指针指向插入结点
	}
}

img

创建一个新的结点addition,此时表中只有头结点,因此执行if中的程序,执行完毕后就变成了:

img

双向链表的遍历

在遍历中将实现next的向后遍历,也会实现front的向前遍历

void IlluList( pElem head ){
	printf("-----------------------------\n");
	if( head == NULL ){
		printf("The list is empty.\n");
		return;
	}
	
	pElem tmpHead = head;
	while( tmpHead->next != NULL ){
		tmpHead = tmpHead->next;	//头结点数据为空,因此直接从头结点的下一结点开始遍历
		printf("%d\n", tmpHead->data);
	}
	//此时tmpHead的地址在链表的最后一个结点处
	while( tmpHead->front->front != NULL ){
		printf("%d\n", tmpHead->data);		//最后一个结点要输出
		tmpHead = tmpHead->front;
	}
	printf("%d\n", tmpHead->data);
	return;
}

当向后遍历完成之后,此时tmpHead的地址是链表的最后一个结点的地址,因此使用front来进行向前的遍历,如果判断条件是tmpHead->front !=NULL的话,会将头结点的数据域也输出,然而头结点的数据域未使用,因此会输出一个不确定的值。正因此判断条件改为tmpHead->front->front !=NULL

删除一个结点

当删除一个结点的时候,我们要判断在链表中是否存在与之匹配的结点,有的话删除成功,否则失败。

img

就像这幅图一样,当删除addition结点时,先讲addition的下一个结点与head相连,而下一个结点是NULL,因此可以得出函数为:

void DeleteElem( pElem head, int data ){
	if( head == NULL ){
		printf("The list is empty.\n");
		return;
	}
	pElem tmpHead = head;
	while( tmpHead->next != NULL ){
		tmpHead = tmpHead->next;
		if( tmpHead->data == data ){
			tmpHead->front->next = tmpHead->next;	//将被删结点的上一个结点的next指针指向被删结点的下一个结点
			tmpHead->next->front = tmpHead->front;	//将被删结点的下一个结点front指针指向被删结点的上一个结点
			break;
		}
	}
	free(tmpHead);		//释放内存
}

销毁链表

销毁一个双向链表的操作同单链表的相似。指针不断向后运动,每运动一个结点,释放上一个结点。

void DestroyList( pElem head ){
    pElem tmp;
    while( head->next != NULL ){
        tmp = head;		//指针不断后移
        head = head->next;
        free(tmp);
    }
    free(head);
}

img

此时,tmp的地址就是head的地址,但当执行一个之后,就变成这样。

img

上一次执行完之后,头结点已经被释放,此时头结点就在第一个数据结点处。以此往后,最后循环结束,释放最后一个结点。

课堂演示题目

要求实现用户输入一个数使得26个字母的排列发生变化,例如用户输入3,输出结果:

  • DEFGHIJKLMNOPQRSTUVWXYZABC

同时需要支持负数,例如用户输入-3,输出结构:

  • XYZABCDEFGHIJKLMNOPQRSTUVW

    #include <stdio.h>
    #include <stdlib.h>
    
    #define OK 1
    #define ERROR 0
    
    typedef char ElemType;
    typedef int Status;
    
    typedef struct DualNode
    {
        ElemType data;
        struct DualNode *prior;
        struct DualNode *next;
    }DualNode, *DuLinkList;
    
    Status InitList(DuLinkList *L)
    {
        DualNode *p, *q;
        int i;
    
        *L = (DuLinkList)malloc(sizeof(DualNode));
        if( !(*L) )
        {
            return ERROR;
        }
    
        (*L)->next = (*L)->prior = NULL;
        p = (*L);
    
        for( i=0; i < 26; i++ )
        {
            q = (DualNode *)malloc(sizeof(DualNode));
            if( !q )
            {
                return ERROR;
            }
    
            q -> data = 'A'+i;
            q -> prior = p;
            q -> next = p -> next;
            p -> next = q;
    
            p = q;
        }
    
        p->next = (*L)->next;
        (*L)->next->prior = p;
    
        return OK;
    }
    
    void Caesar(DuLinkList *L, int i)
    {
        if( i > 0 )
        {
            do
            {
                (*L) = (*L)->next;
            }while( --i );
        }
    
        if( i < 0 )
        {
            do
            {
                (*L) = (*L)->next;
            }while( ++i );
        }
    }
    
    int main()
    {
        DuLinkList L;
        int i, n;
    
        InitList(&L);
    
        printf("请输入一个整数:");
        scanf("%d", &n);
        printf("\n");
        Caesar(&L, n);
    
        for( i=0; i < 26; i++ )
        {
            L = L->next;
            printf("%c", L->data);
        }
    	printf("\n");
        return 0;
    }
    

栈和队列

栈:(Stack)是一个后进先出(Last in first out ,LIFO)的线性表,它要求只在表尾进行删除和插入操作。

对于栈来说,表尾称为栈的栈顶(top),相应的表头称为栈底(bottom)。

image-20200403152457218

栈的顺序存储结构

typedef struct
{
    ElemType *base;
    ElemType *top;
    int stackSize;
}sqStack;

base是指向栈底的指针变量,top是指向栈顶的指针变量,stackSize指示栈的当前可使用的最大容量。

创建一个栈

#define STACK_INIT_SIZE 100
initStack(sqStack *s)
{
    s -> base = (ElemType *)malloc(STACK_INIT_SIZE * sizeof(ElemType) );
    if( !s->base )
        exit(0);
    s -> top = s -> base;	//最开始,栈顶就是栈底
    s -> stackSize = STACK_INIT_SIZE;
}

入栈操作

  • 入栈操作也叫压栈操作,就是向栈中存放数据
  • 入栈操作要在栈顶进行,每次向栈中压入一个数据,top指针就要+1,直到栈满为止。
#define STACKINCREMENT 10

Push(sqStack *s, ElemType e)
{
    //如果栈满,追加空间
    if( s->top - s->base >= s->stackSize )
    {
        s->base = (ElemType *)realloc(s->base, (s->stackSize + STACKINCREMENT) * sizeof(ElemType));
        if( !s->base )
            exit(0);
        
        s->top = s->base + s->stackSize;		//设置栈顶
        s->stackSize = s->stackSize + STACKINCREMENT;	//设置栈的最大容量
    }   
    
    *(s->top) = e;
    s->top++;
}

出栈操作

  • 出栈操作就是在栈顶取出数据,栈顶指针随之下移的操作

  • 每当从栈内弹出一个数据,栈的当前容量就-1

    Pop(sqStack *s, ElemType *e)
    {
        if( s->top == s->base )	//栈已空
            return;
        *e = *--(s->top);
    }
    

清空一个栈

  • 将栈中的元素全部作废,栈本身的物理空间并不发生改变。

  • 只要将s->top的内容赋值为s->base即可,这样s->base等于s->top,也就表明这个栈是空的。

    ClearStack(sqStack *s){
        s->top = s->base;
    }
    

销毁一个栈

  • 销毁一个栈与清空一个栈不同,销毁一个栈是要释放掉该栈所占据的物理内存空间。

    DestroyStack(sqStack *s){
        int i, len;
        len = s->stackSize;
        for( i=0; i < len; i++ ){
            free( s->base );
            s->base++;
        }
        s->base = s->top = NULL;
        s->stackSize = 0;
    }
    

计算栈的当前容量

  • 计算栈的当前容量也就是计算栈中元素的个数,因此只要返回s.top - s.base即可

  • 注:栈的最大容量是指该栈占据内存空间的大小,其值时s.stackSize, 它与栈的当前容量不是一个概念。

    int StackLen(sqStack s)
    {
        return (s.top - s.base );
    }
    

实例分析

  • 题目:利用栈的数据结构特点,将二进制转换为十进制数。

    #include <stdio.h>
    #include <stdlib.h>
    #include <math.h>
    
    #define STACK_INIT_SIZE 20
    #define STACKINCREMENT 10
    
    typedef char ElemType;
    typedef struct
    {
        ElemType *base;
        ElemType *top;
        int stackSize;
    }sqStack;
    
    void InitStack(sqStack *s)
    {
        s->base = (ElemType *)malloc(STACK_INIT_SIZE * sizeof(ElemType));
        if( !s->base )
        {
            exit(0);
        }
        
        s->top = s->base;
        s->stackSize = STACK_INIT_SIZE;
    }
    
    void Push(sqStack *s, ElemType e)
    {
        if( s->top - s->base >= s->stackSize )
        {
            s->base = (ElemType *)realloc(s->base, (s->stackSize + STACKINCREMENT) * sizeof(ElemType) );
                if( !s->base )
                {
                    exit(0);
                }
        }
        
        *(s->top) = e;
        s->top++;
    }
    
    void Pop(sqStack *s, ElemType *e)
    {
        if( s->top == s->base )
        {
            return;
        }
        *e = *--(s->top);
    }
    
    int StackLen(sqStack s)
    {
        return (s.top - s.base);
    }
    
    int main()
    {
        ElemType c;
        sqStack s;
        int len, i, sum = 0;
        
        InitStack(&s);
        
        printf("请输入二进制数,输入#符号表示结束!\n");
        scanf("%c", &c);
        while( c != '#' )
        {
            Push(&s, c);
            scanf("%c", &c);
        }
        
        getchar();	//把'\n'从缓冲区去掉
        
        len = StackLen(s);
        printf("栈的当前容量是:%d\n", len);
        
        for(i = 0; i < len; i++ )
        {
            Pop(&s, &c);
            sum = sum + (c-48) * pow(2, i);
        }
        
        printf("转化为十进制数是:%d\n", sum);
        
        return 0;
    }
    

栈的链式存储结构

  • 栈因为只是栈顶来做插入和删除操作,所以比较好的方法就是将栈顶放在单链表的头部,栈顶指针和单链表的头指针合二为一。

进栈操作

对于栈链的Push操作,假设元素值为e的新结点是s,top为栈顶指针

Status Push(LinkStack *s, ElemType e)
{
    LinkStackPtr p = (LinkStackPtr) malloc (sizeof(StackNode));
    p->data = e;
    p->next = s->top;
    s->top = p;
    s->count++;
    return OK;
}

出栈操作

假设变量p用来存储要删除的栈顶结点,将栈顶指针下移一位,最后释放p即可

Status Pop(LinkStack *s, ElemType *e )
{
    LinkStackPtr p;
    if( StackEmpty(*s))		//判断是否为空栈
        return ERROR;
    *e = s->top->data;
    p = s->top;
    s->top = s->top->next;
    free(p);
    s->count--;
    return OK;
}

(重点)逆波兰计算器

  • 实现对逆波兰输入的表达式进行计算
  • 支持带小数点的数据
  • 正常表达式----------->逆波兰表达式
    • a+b-------------->a b +
    • a+(b-c) --------------------> a b c - +
    • a+(b-c)*d --------------------> a b c - d * +
    • a+d*(b-c)-----------------> a d b c - * +
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>

#define STACK_INIT_SIZE 20
#define STACKINCREMENT  10
#define MAXBUFFER       10

typedef double ElemType;
typedef struct
{
    ElemType *base;
    ElemType *top;
    int stackSize;
}sqStack;

InitStack(sqStack *s)
{
    s->base = (ElemType *)malloc(STACK_INIT_SIZE * sizeof(ElemType));
    if( !s->base )
        exit(0);

    s->top = s->base;
    s->stackSize = STACK_INIT_SIZE;
}

Push(sqStack *s, ElemType e)
{
    // 栈满,追加空间,鱼油必须懂!
    if( s->top - s->base >= s->stackSize )
    {
        s->base = (ElemType *)realloc(s->base, (s->stackSize + STACKINCREMENT) * sizeof(ElemType));
        if( !s->base )
            exit(0);

        s->top = s->base + s->stackSize;
        s->stackSize = s->stackSize + STACKINCREMENT;
    }

    *(s->top) = e;      // 存放数据
    s->top++;
}

Pop(sqStack *s, ElemType *e)
{
    if( s->top == s->base )
        return;

    *e = *--(s->top);   // 将栈顶元素弹出并修改栈顶指针
}

int StackLen(sqStack s)
{
    return (s.top - s.base);
}

int main()
{
    sqStack s;
    char c;
    double d, e;
    char str[MAXBUFFER];
    int i = 0;

    InitStack( &s );

    printf("请按逆波兰表达式输入待计算数据,数据与运算符之间用空格隔开,以#作为结束标志: \n");
    scanf("%c", &c);

    while( c != '#' )
    {
        while( isdigit(c) || c=='.' )  // 用于过滤数字
        {
            str[i++] = c;
            str[i] = '\0';
            if( i >= 10 )
            {
                printf("出错:输入的单个数据过大!\n");
                return -1;
            }
            scanf("%c", &c);
            if( c == ' ' )
            {
                d = atof(str);
                Push(&s, d);
                i = 0;
                break;
            }
        }

        switch( c )
        {
            case '+':
                Pop(&s, &e);
                Pop(&s, &d);
                Push(&s, d+e);
                break;
            case '-':
                Pop(&s, &e);
                Pop(&s, &d);
                Push(&s, d-e);
                break;
            case '*':
                Pop(&s, &e);
                Pop(&s, &d);
                Push(&s, d*e);
                break;
            case '/':
                Pop(&s, &e);
                Pop(&s, &d);
                if( e != 0 )
                {
                    Push(&s, d/e);
                }
                else
                {
                    printf("\n出错:除数为零!\n");
                    return -1;
                }
                break;
        }

        scanf("%c", &c);
    }

    Pop(&s, &d);
    printf("\n最终的计算结果为:%f\n", d);

    return 0;
}
posted @ 2021-02-18 16:37  zonkidd  阅读(52)  评论(0编辑  收藏  举报