带哨兵节点和不带哨兵节点的单链表操作的对比
哨兵节点:哨兵节点(sentinel)是一个哑元节点(dummy node),可以简化边界条件。是一个附加的链表节点,该节点作为第一个节点,它的值域中并不存储任何东西,只是为了操作的方便而引入的。如果一个链表有哨兵节点的话,那么线性表的第一个元素应该是链表的第二个节点。
很多情况下,需要处理当前节点的前驱节点,如果是没有哨兵节点的链表,对第一个节点,即头节点,没有前驱节点。如果不作特殊处理,就可能出错;如果对它特别对待,就会增加代码复杂性,还会降低程序效率。而如果有哨兵节点的话, 线性表的每个位置的节点都有前驱节点,因此可以统一处理。
当链表为空时,没有哨兵节点的链表的头节点为NULL,处理起来也和其他情况不同。带哨兵节点的链表,当其为一个空链表时,仅含哨兵节点,哨兵节点的指针域为空,和其他情况的表尾是一样的。
定义链表的节点:
typedef struct _node{ int data; struct _node *next; }node, *pNode;
定义链表结构体的数据类型。
创建一个链表。
没有哨兵节点的情况:
pNode createList(void) { pNode temp; pNode prev; int input; pNode head = NULL; scanf("%d", &input); while(input != -1){ temp = (pNode)malloc(sizeof(node)); temp->data = input; temp->next = NULL; //需要考虑链表为空的情况 if(head == NULL){ head = temp; prev = temp; }else{ prev->next = temp; //链 prev = temp; } scanf("%d", &input); } return head; }
有哨兵节点的情况:
pNode createListWithSentinel(void) { pNode temp; pNode prev; int input; pNode head; head = (pNode)malloc(sizeof(node)); head->next = NULL; prev = head; scanf("%d", &input); while(input != -1){ temp = (pNode)malloc(sizeof(node)); temp->data = input; temp->next = NULL; prev->next = temp; //链 prev = temp; scanf("%d", &input); } return head; }
没有哨兵节点时,添加一个节点要先判断是否是第一个节点,并单独保留第一个节点的指针,以便于返回整个链表的头指针。有哨兵节点时,链表头是固定的,不可能为空,后续的节点都是链接在前一个节点的,不需要单独判断是否为头节点。
遍历输出链表。
没有哨兵节点:
void printList(const pNode head) { pNode temp = head; while(temp){ printf("%d, ", temp->data); temp = temp->next; } printf("\n"); }
有哨兵节点:
void printListWithSentinel(const pNode head) { pNode temp = head; while(temp->next){ printf("%d, ", temp->next->data); temp = temp->next; } printf("\n"); }
差别不大。
在指定的位置前插入一个节点。第一个位置为0
没有哨兵节点:
pNode insertNodeN(pNode head, int pos, int value) { int count; pNode temp; pNode prev = head; temp = (pNode)malloc(sizeof(node)); temp->data = value; if(head == NULL || pos == 0){ temp->next = head; return temp; } for(count = 1; count < pos && prev->next != NULL; count++){ prev = prev->next; } temp->next = prev->next; prev->next = temp; //链 return head; }
有哨兵节点:
void insertNodeWithSentinelN(const pNode head, int pos, int value) { int count; pNode temp; pNode prev = head; temp = (pNode)malloc(sizeof(node)); temp->data = value; for(count = 0; count < pos && prev->next != NULL; count++){ prev = prev->next; } temp->next = prev->next; prev->next = temp; //链 }
有哨兵节点时,不需要判断链表为空和插入点在第一个位置节点的情况。
删除指定位置的节点。
没有哨兵节点:
pNode deleteNodeN(pNode head, int pos) { int count; pNode temp; pNode prev = head; /*空表的情况*/ if(head == NULL){ return head; } /*删除第一个节点,即删除的是头节点的情况*/ if(pos == 0){ temp = head; head = head->next; free(temp); return head; } for(count = 1; count < pos && prev->next != NULL; count++){ prev = prev->next; } temp = prev->next; if(temp != NULL){ prev->next = temp->next; //还没有到表尾 } free(temp); return head; }
有哨兵节点:
void deleteNodeWithSentinelN(const pNode head, int pos) { int count; pNode temp; pNode prev = head; for(count = 0; count < pos && prev->next != NULL; count++){ prev = prev->next; } temp = prev->next; if(temp != NULL){ prev->next = temp->next; //还没有到表尾 } free(temp); }
有哨兵节点时,不需要判断链表为空和删除第一个位置节点的情况。
总结:
带哨兵节点的链表,需要额外的一个节点,但插入和删除等操作不需要额外的判断;不带哨兵节点,在处理链表为空时,和其他情况不一样,需要单独判断一次。
带哨兵节点的链表,插入或删除时,不论操作的位置,表头都不变,不需要额外的判断;不带哨兵节点的链表,插入或删除操作发生在第一个节点时,表头指针都要变化,需要额外的处理。