链表笔试题

来源:互联网 发布:知天下帐号 编辑:程序博客网 时间:2024/04/29 23:31
链表:
1、注意是否有带头结点。
2、单链表的建立:顺序建表(尾插法)、逆序建表(头插法)。单链表插入、删除操作需要寻找前驱结点。
3、双向链表和单向链表相比,多了一个前驱指针,单向链表在删除结点时候要遍历链表,双向链表在删除不需要遍历。

双向循环链表的插入和删除:
1.p之后插入ss->next = p->next;p->next = s;s->prior = p;s->next->prior = s;2.p之前插入ss->prior= p->prior;p->prior = s;s->next = p;s->prior->next = s;3.删除p之后继ss = p->next;p->next = s->next;p->next->prior = p;4.删除pp->prior->next= p->next;p->next->prior= p->prior;

一、判断两个链表是否相交:(假设两个链表都没有环)

方法:

 1、判断第一个链表的每个节点是否在第二个链表中
 2、把第二个链表连接到第一个后面,判断得到的链表是否有环,有环则相交
 3、如果两个链表有一个公共结点,那么该公共结点之后的所有结点都是重合的,那么,它们的最后一个结点必然是重合的。
先遍历第一个链表,记住最后一个节点,再遍历第二个链表,得到最后一个节点时和第一个链表的最后一个节点做比较,如果相同,则相交。
如何判断一个单链表是有环的?
设置两个指针(fast, slow),初始值都指向头,slow每次前进一步,fast每次前进二步,如果链表存在环,则fast必定先进入环,而slow后进入环,两个指针必定相遇。(当然,fast先行头到尾部为NULL,则为无环链表)程序如下:
bool IsExitsLoop(ListNode *head){    ListNode *slow = head, *fast = head;    while ( fast && fast->next )     {        slow = slow->next;        fast = fast->next->next;        if ( slow == fast ) break;    }    return !(fast == NULL || fast->next == NULL);}


寻找到环的入口点

当fast若与slow相遇时,slow肯定没有走遍历完链表,而fast已经在环内循环了n圈(1<=n)。假设slow走了s步,则fast走了2s步(fast步数还等于s 加上在环上多转的n圈),设环长为r,则:
2s = s + nr
s= nr
环入口点与相遇点距离为x,起点到环入口点的距离为a,则:
a + x = s

a + x = nr

a = nr - x

从链表头到环入口点等于n循环内环减去环入口点到相遇点距离,现让一指针p1从链表起点处开始遍历,指针p2从相遇点处开始遍历,且p1和p2移动步长均为1。则当p1移动距离a时到达环的入口点。由于p2是从相遇点处开始移动,故p2移动nr步是移回到了相遇点处,再退x步则是到了环的入口点,也即移动了(nr-x)的距离,当p1移动a第一次到达环的入口点时,p2也恰好到达了该入口点。程序描述如下:
ListNode* FindLoopPort(ListNode *head){    ListNode *slow = head, *fast = head;    while(fast && fast->next)     {        slow = slow->next;        fast = fast->next->next;        if (slow == fast) break;    }    if(fast == NULL || fast->next == NULL)        return NULL;    slow = head;    while(slow != fast)    {         slow = slow->next;         fast = fast->next;    }    return slow;}

扩展1:如果链表可能有环,则如何判断两个链表是否相交

思路:
1.先判断带不带环
2.如果都不带环,就判断尾节点是否相等
3.如果都带环,判断一链表上俩指针相遇的那个节点,在不在另一条链表上,如果在,则相交,如果不在,则不相交。

//1.先判断带不带环  //判断是否有环,返回bool,如果有环,返回环里的节点  //思路:用两个指针,一个指针步长为1,一个指针步长为2,判断链表是否有环  bool isCircle(Node * head, Node *& circleNode, Node *& lastNode)  {      Node * fast = head->next;      Node * slow = head;      while(fast != slow && fast && slow)      {          if(fast->next != NULL)              fast = fast->next;                    if(fast->next == NULL)              lastNode = fast;          if(slow->next == NULL)              lastNode = slow;                    fast = fast->next;          slow = slow->next;                }      if(fast == slow && fast && slow)      {          circleNode = fast;          return true;      }      else          return false;  }
如果都不带环,就判断尾节点是否相等,如果都带环,判断一链表上俩指针相遇的那个节点,在不在另一条链表上。下面是综合解决这个问题的代码:
//判断带环不带环时链表是否相交  //2.如果都不带环,就判断尾节点是否相等  //3.如果都带环,判断一链表上俩指针相遇的那个节点,在不在另一条链表上。  bool detect(Node * head1, Node * head2)  {      Node * circleNode1;      Node * circleNode2;      Node * lastNode1;      Node * lastNode2;            bool isCircle1 = isCircle(head1,circleNode1, lastNode1);      bool isCircle2 = isCircle(head2,circleNode2, lastNode2);            //一个有环,一个无环      if(isCircle1 != isCircle2)          return false;      //两个都无环,判断最后一个节点是否相等      else if(!isCircle1 && !isCircle2)      {          return lastNode1 == lastNode2;      }      //两个都有环,判断环里的节点是否能到达另一个链表环里的节点      else      {          Node * temp = circleNode1->next;        while(temp != circleNode1)            {              if(temp == circleNode2)                  return true;              temp = temp->next;          }          return false;      }            return false;  } 


扩展2:求两个链表相交的第一个节点
思路:如果两个尾结点是一样的,说明它们有重合;否则两个链表没有公共的结点。
在上面的思路中,顺序遍历两个链表到尾结点的时候,我们不能保证在两个链表上同时到达尾结点,这是因为两个链表不一定长度一样。但如果假设一个链表比另一个长L个结点,我们先在长的链表上遍历L个结点,之后再同步遍历,这个时候我们就能保证同时到达最后一个结点了。由于两个链表从第一个公共结点开始到链表的尾结点,这一部分是重合的。因此,它们肯定也是同时到达第一公共结点的。于是在遍历中,第一个相同的结点就是第一个公共的结点。
在这个思路中,我们先要分别遍历两个链表得到它们的长度,并求出两个长度之差。在长的链表上先遍历若干次之后,再同步遍历两个链表,直到找到相同的结点,或者一直到链表结束。此时,如果第一个链表的长度为m,第二个链表的长度为n,该方法的时间复杂度为O(m+n)。
ListNode* FindFirstCommonNode( ListNode *pHead1, ListNode *pHead2){      // Get the length of two lists      unsigned int nLength1 = ListLength(pHead1);      unsigned int nLength2 = ListLength(pHead2);      int nLengthDif = nLength1 - nLength2;      // Get the longer list      ListNode *pListHeadLong = pHead1;      ListNode *pListHeadShort = pHead2;      if(nLength2 > nLength1)      {            pListHeadLong = pHead2;            pListHeadShort = pHead1;            nLengthDif = nLength2 - nLength1;      }      // Move on the longer list      for(int i = 0; i < nLengthDif; ++ i)            pListHeadLong = pListHeadLong->m_pNext;      // Move on both lists      while((pListHeadLong != NULL) && (pListHeadShort != NULL) && (pListHeadLong != pListHeadShort))      {            pListHeadLong = pListHeadLong->m_pNext;            pListHeadShort = pListHeadShort->m_pNext;      }      // Get the first common node in two lists      ListNode *pFisrtCommonNode = NULL;      if(pListHeadLong == pListHeadShort)            pFisrtCommonNode = pListHeadLong;      return pFisrtCommonNode;}unsigned int ListLength(ListNode* pHead){      unsigned int nLength = 0;      ListNode* pNode = pHead;      while(pNode != NULL)      {            ++ nLength;            pNode = pNode->m_pNext;      }      return nLength;}

二、题目:给定链表的头指针和一个结点指针,在O(1)时间删除该结点。

分析:之所以需要从头结点开始查找要删除的结点,是因为我们需要得到要删除的结点的前面一个结点。换一种思路,我们可以从给定的结点得到它的下一个结点。这个时候我们实际删除的是它的下一个结点,由于我们已经得到实际删除的结点的前面一个结点,因此完全是可以实现的。当然,在删除之前,我们需要需要把给定的结点的下一个结点的数据拷贝到给定的结点中。此时,时间复杂度为O(1)。
void DeleteNode(ListNode* pListHead, ListNode* pToBeDeleted){      if(!pListHead || !pToBeDeleted)            return;      // if pToBeDeleted is not the last node in the list      if(pToBeDeleted->m_pNext != NULL)      {            // copy data from the node next to pToBeDeleted            ListNode* pNext = pToBeDeleted->m_pNext;            pToBeDeleted->m_nKey = pNext->m_nKey;            pToBeDeleted->m_pNext = pNext->m_pNext;             // delete the node next to the pToBeDeleted            delete pNext;            pNext = NULL;      }      // if pToBeDeleted is the last node in the list      else      {            // get the node prior to pToBeDeleted            ListNode* pNode = pListHead;            while(pNode->m_pNext != pToBeDeleted)            {                  pNode = pNode->m_pNext;                         }            // deleted pToBeDeleted            pNode->m_pNext = NULL;            delete pToBeDeleted;            pToBeDeleted = NULL;      }} 
值得注意的是,为了让代码看起来简洁一些,上面的代码基于两个假设:(1)给定的结点的确在链表中;(2)给定的要删除的结点不是链表的头结点。不考虑第一个假设对代码的健壮性是有影响的。至于第二个假设,当整个列表只有一个结点时,代码会有问题。但这个假设不算很过分,因为在有些链表的实现中,会创建一个虚拟的链表头,并不是一个实际的链表结点。这样要删除的结点就不可能是链表的头结点了。

扩展:一个没有头指针的单链表。一个指针指向此单链表中间的一个结点(不是第一个,也不是最后一个),将该结点删除。
删除一个结点需要其前驱结点,但是此题无法得到前驱结点,所以将要删除结点后面的那个结点的值赋值过来,再将后面结点删除。
void DeleteRandomNode(node* pCurrent){    assert(pCurrent != NULL)    node* pNext = pCurrent->next;    if(pNext != NULL)    {        pCurrent->next = pNext->next;        pCurrent->data = pNext->data;        delete pNext;    }}


三、删除一个单项链表的最中间的元素,要求时间尽可能短

void DeleteMiddleNode(ListNode *head){    if(head == NULL)   return;    else if(head->next == NULL)    {        delete head;        return;    }    else    {        ListNode *low = head;        ListNode *fast = head->next;        while(fast != NULL && fast->next != NULL)        {               fast = fast->next->next;            if(fast == NULL)  break;            low = low->next;        }        ListNode *temp = low->next;        low->next = low->next->next;        delete temp;    }}

 

三、寻找倒数第k个结点

题目:输入一个单向链表,输出该链表中倒数第k个结点。链表的倒数第0个结点为链表的尾指针。

解法一:假设整个链表有n个结点,那么倒数第k个结点是从头结点开始的第n-k-1个结点(从0开始计数)。
这种思路的时间复杂度是O(n),但需要遍历链表两次。第一次得到链表中结点个数n,第二次得到从头结点开始的第n-k-1个结点即倒数第k个结点。如果链表的结点数不多,这是一种很好的方法。

ListNode* FindKthToTail(ListNode* head, int k){    if(head == NULL)  return NULL;    ListNode *p = head;    unsigned int n = 0;    while(p->next != NULL)    {        p = p->next;        n ++;    }    if(n < k)  return NULL;    p = head;    for(int i = 0; i < n-k; ++i)        p = p->next;    return p;}

解法二:如果我们在遍历时维持两个指针,第一个指针从链表的头指针开始遍历,在第k-1步之前,第二个指针保持不动;在第k-1步开始,第二个指针也开始从链表的头指针开始遍历。由于两个指针的距离保持在k-1,当第一个(走在前面的)指针到达链表的尾结点时,第二个指针(走在后面的)指针正好是倒数第k个结点。
这种思路只需要遍历链表一次。对于很长的链表,只需要把每个结点从硬盘导入到内存一次。因此这一方法的时间效率前面的方法要高。

ListNode *FindMToLastElement(ListNode *head, int m) {     ListNode *p1, *p2;     p1 = head;     for(int i = 0; i < m; ++i)     {          if(p1->next)             p1 = p1->next;          else             return NULL;     }     p2 = head;     while(p1->next)     {         p1 = p1->next;         p2 = p2->next;     }     return p2;}

 

四、从尾到头遍历链表

题目:输入一个链表的头结点,从尾到头反过来输出每个结点的值。
1、反转链表,再从头到尾输出

void ReverseList(ListNode* head){    ListNode* p = head->next;  // 原链表    head->next = NULL; // 新表(空表)    while(p != NULL)    {        ListNode* q = p->next;  // 保存下个结点q        p->next = head->next;        head->next = p;        p = q;    }}

2、从头到尾遍历链表,每经过一个结点的时候,把该结点放到一个栈中。当遍历完整个链表后,再从栈顶开始输出结点的值,此时输出的结点的顺序已经反转过来了。该方法需要维护一个额外的栈,实现起来比较麻烦。
3、既然想到了栈来实现这个函数,而递归本质上就是一个栈结构。于是很自然的又想到了用递归来实现。要实现反过来输出链表,我们每访问到一个结点的时候,先递归输出它后面的结点,再输出该结点自身,这样链表的输出结果就反过来了。

void PrintListReversely(ListNode* head){    if(head != NULL)    {        // Print the next node first        if (head->next != NULL)        {            PrintListReversely(head->next);        }        // Print this node        printf("%d", head->data);    }}


五、对链表进行排序

typedef ListNode * List;//采用插入法将单链表中的元素排序。void InsertionSort(List & L){    List h=L->next;  // 原链表    L->next=NULL;  // 新空表    List s=NULL,p=NULL;    while(h)    {        // 从原链表中取下结点s        s=h;  h=h->next;        // 在新表中查找插入位置        p=L;        while (p->next && p->next->data<=s->data)        p=p->next;        // 在p之后插入s        s->next=p->next;        p->next=s;    }}//采用选择法将单链表中的元素排序。void SelectionSort(List & L){    List p=L;    List q=NULL,s=NULL,m=NULL;    while (p->next)    {        // 选择最小(从p->next至表尾)        q=p; // 最小元素的前驱q        s=p;        while(s->next)        {            if(s->next->data<q->next->data)  q=s;            s=s->next;        }        m=q->next; // 找到最小m        // 最小元素m插入有序序列末尾(p之后)        if (q!=p)        {            q->next=m->next;  // 解下最小m            m->next=p->next;  // 插入p之后            p->next=m;        }        p=p->next; // L->next至p为有序序列    }}