[Beauty of Data Structure and Algorithm] 06-linked list (below): how to easily write the correct linked list code

  The last blog summarized the basic theory of the linked list, but it may not be enough for us to easily write the code of the linked list. Most people know the theory of linked lists. But in the interview, less than 10% of the people were able to write the "link list reversed" lines of code correctly.

  This blog will organize some techniques for writing linked list code, in order to better grasp the linked list from a practical perspective.


1. Understand the meaning of pointers or references

  The structure of the linked list itself is not difficult to understand, the difficulty lies mainly in the operation of the pointer. To write code for a linked list, we must first understand the pointer.

How to understand pointers

  Some languages ​​have the concept of "pointers", such as the C language. Some languages ​​do not have pointers, and are replaced by "references", such as Java and Python. Whether it is a "pointer" or "reference", in fact, they all represent the memory address where the pointed object is stored. In order to facilitate the description, we will call them pointers in the following text and describe them in C language. It doesn't matter if you use Java or other languages ​​without pointers, just understand it as a "reference".

  In fact, the essence of a pointer is two sentences:

  • Assigning a variable to a pointer actually assigns the address of this variable to the pointer;
  • The memory address where this variable is stored in the pointer can be found through the pointer.

Common pointer codes in linked lists

  When writing linked list code, you often see code similar to  p-> next = q  . Its meaning is: the next pointer in node p stores the memory address of node q.

  There is also a form that is often used when writing linked list code: p-> next = p-> next-> next . Its meaning is: the next pointer of the p node stores the memory address of the next node of the p node.

 

Second, beware of pointer loss and memory leaks

  When writing the linked list code, the pointer moves around, it is easy to not know where to point. Therefore, when writing linked list code, you must not lose pointers. Let's take the insertion operation of a singly linked list as an example to see how the pointer is lost:

  As shown in the figure, we want to insert node x between node a and the adjacent node b, assuming that the current pointer p points to node a. If we change the code implementation to the following, pointer loss and memory leak will occur.

1 p-> next = x;         // point the next pointer of p to the x node; 
2 x-> next = p-> next;   // point the next pointer of the x node to the b node;

  Beginners often make mistakes here. After completing the first operation, the p-> next pointer no longer points to node b, but points to node x. The second line of code is equivalent to assigning x to x-> next, pointing yourself. Therefore, the entire linked list is broken in half, and all nodes from node b onwards cannot be accessed.

  For some languages, such as the C language, the memory management is the responsibility of the programmer. If the memory space corresponding to the node is not manually released, a memory leak will occur. Therefore, when we insert a node, we must pay attention to the order of operations. We must first point the next pointer of node x to node b, and then point the next pointer of node a to node x, so as not to lose the pointer, resulting Memory leak. Therefore, for the inserted code just now, we only need to reverse the order of the first and second lines of code.

  Similarly, when deleting a linked list node, you must also remember to manually release the memory space, otherwise, there will be a memory leak problem. Of course, for a programming language like Java that automatically manages memory, there is no need to think so much.

 

Third, use the sentinel to simplify the difficulty of realization

Single-link list ordinary node insertion

  First, let's review the insertion and deletion operations of singly linked lists. If we insert a new node after node p, only two lines of code below are needed.

1 new_node->next = p->next;
2 p->next = new_node;

Singly linked list inserted into the first node

  When inserting the first node into an empty linked list, the above logic can no longer be used. The processing method at this time is as follows, where head represents the head node of the linked list. As can be seen from the following code, for a single linked list, the insertion logic of the first node and other nodes are different.

1 if (head == null)
2 {
3     head = new_node;
4 }

Singly linked list common node deletion

  Let's look at the deletion of singly linked list nodes. If we want to delete the successor node of node p, we only need one line of code to get it.

1 p->next = p->next->next;

Singly linked list deletes the last node

  If there is only one node in the singly linked list, p-> next  is null, the above logic is naturally not applicable. This situation also requires special treatment. code show as below:

1 if (head->next == null)
2 {
3     head = null;
4 }

  From the previous analysis, we can see that the insertion of the first node in the linked list and the deletion of the last node require special treatment. This code will be very cumbersome to implement, and it is also easy to make mistakes because of incomplete consideration. We can use sentinels to solve this problem. As the name implies, sentinels solve "boundary problems" and do not directly participate in business logic.

  How did we represent empty linked lists before? head = null means there are no more nodes in the linked list. Where head represents the head node pointer, pointing to the first node in the linked list.

  Next we introduce the sentinel node. Regardless of whether the linked list is empty, the head pointer always points to this sentinel node. We also call this list with sentinel nodes the lead list . Conversely, a linked list without a sentinel node is called a leaderless list .

  I drew a lead list, you can find that the sentinel node does not store data . Because the sentinel node always exists, inserting the first node and inserting other nodes, deleting the last node and deleting other nodes can all be unified into the same code to achieve logic.

  In fact, this technique of using Sentinel to simplify programming difficulty is used in many code implementations, such as insertion sort, merge sort, and dynamic programming. We will talk about these contents later. Now I will give you a very simple example in order to make you feel deeper. The code is implemented in C language. It does not involve the advanced syntax of the language. It is easy to understand, and you can compare it to the language you are familiar with.

  Code one:

1  // Find the key in the array a and return the position where the key is located
 2  // Where, n represents the length of the array a 
3  int find ( char * a, int n, char key)
 4  {
 5      // Boundary condition processing, If a is empty, or n <= 0, it means there is no data in the array, so you do n’t need a while loop to compare 
6      if (a == null || n <= 0 )
 7      {
 8          return - 1 ;
 9      }
 10  
11      int i = 0 ;
 12      
13      // There are two comparison operations: i <n and a [i] == key. 
14     while (i < n)
15     {
16         if (a[i] == key)
17         {
18             return i;
19         }
20         ++i;
21     }
22 
23     return -1;
24 }

  Code two:

1  // In array a, find the key and return the position where the key is located
 2  // where n represents the length of the array a
 3  // I will give 2 examples, you can take the example and take a look at the code
 4  // a = {4 , 2, 3, 5, 9, 6} n = 6 key = 7
 5  // a = {4, 2, 3, 5, 9, 6} n = 6 key = 6 
6  int find ( char * a, int n, char key)
 7  {
 8      if (a == null || n <= 0 )
 9      {
 10          return - 1 ;
 11      }
 12  
13      //Here, because the value of a [n-1] is to be replaced by key, this value must be specially treated. 
14      if (a [n- 1 ] == key)
 15      {
 16          return n- 1 ;
 17      }
 18  
19      // put The value of a [n-1] is temporarily saved in the variable tmp for later restoration. tmp = 6.
20      // The purpose of doing this is: hope find () code does not change the contents of a array 
21      char tmp = a [n- 1 ];
 22  
23      // put the value of key in a [n-1] In this case, a = {4, 2, 3, 5, 9, 7} 
24      a [n- 1 ] = key;
 25  
26      int i = 0 ;
 27  
28     // Compared to code one, the while loop lacks i <n this comparison operation 
29      while (a [i]! = key)
 30      {
 31          ++ i;
 32      }
 33  
34      // Restore the original a [n-1] Value, at this time a = {4, 2, 3, 5, 9, 6} 
35      a [n- 1 ] = tmp;
 36  
37      if (i == n- 1 )
 38      {
 39          // if i == n -1 means that there is no key between 0 ... n-2, so return -1 
40          return - 1 ;
 41      }
 42      else 
43      {
 44          //Otherwise, return i, which is the index of the element equal to the key value 
45          return i;
 46      }
 47 }

  Comparing the two pieces of code, when the string a is very long, such as tens of thousands, hundreds of thousands, which piece of code do you think runs faster? The answer is code two, because the maximum number of executions in the two pieces of code is the part of the while loop. In the second piece of code, we passed a sentry a [n-1] = key, successfully saved a comparison statement i <n, do n’t underestimate this statement, when the cumulative execution of tens of thousands of times The time is obvious.

  Of course, this is just to illustrate the role of the sentinel. When you write the code, do n’t write the second paragraph of code, because the readability is too bad. In most cases, we do not need to pursue such extreme performance.

 

Fourth, pay attention to the handling of boundary conditions

  In software development, the code is most likely to produce bugs under some boundary or abnormal conditions. To realize the linked list code without bugs, it is necessary to check whether the boundary conditions are considered comprehensively and whether the code can run correctly under the boundary conditions during and after the writing.

  There are several boundary conditions that are often used to check whether the linked list code is correct:

  • If the linked list is empty, will the code work properly?
  • If the linked list contains only one node, can the code work properly?
  • If the linked list contains only two nodes, can the code work properly?
  • Does the code logic work properly when dealing with head and tail nodes?

  In addition to verifying whether the common node works for the linked list code, pay attention to verify the above four boundary conditions. If there are no problems under these boundary conditions, it can basically be considered as no problem.

  Of course, there are more than these boundary conditions. For different scenes, there may be specific boundary conditions, this need depends on the situation, but the routines are the same.

  In fact, when writing any code, don't just implement the normal functions of the business. Be sure to think about what boundary conditions or anomalies you might encounter. Encountered how to deal with, so that the code written is strong enough!

 

5. Draw pictures with examples to aid thinking

  For a slightly more complicated linked list operation, such as the singly linked list inversion we mentioned earlier, the pointer points to this for a while, and then to the other, and it is stunned. In this case, the example method and drawing method are used.

  You can find a specific example and draw it on paper, so that you will feel a lot clearer. For example, to insert a node into a singly linked list, you can give an example of each situation, and draw the change of the linked list before and after insertion, as shown in the figure:

  Looking at the picture and writing the code is much simpler. After writing the code, you can also give a few examples, draw it on paper, and follow the code again, it is easy to find the bugs in the code.

 

Six, write more and practice more

  The following are 5 common linked list operations. As long as you write these operations proficiently, you will no longer be afraid to write linked list code.

  • Singly linked list reversal
  • Detection of rings in linked list
  • Two ordered linked lists merge
  • Delete the nth last node of the linked list
  • Find the middle node of the linked list

 

Seven, content summary

  In this blog, you mainly talked about six tips for writing correct linked list code:

  • Understand the meaning of pointers or references;
  • Be wary of pointer loss and memory leaks;
  • Use Sentinel to simplify the realization of difficulty;
  • Pay attention to the handling of boundary conditions;
  • Draw pictures for example;
  • Write more and practice more.

  Writing linked list code is the most testing logical thinking ability. Because linked list code pointers operate frequently, and need to consider the handling of boundary conditions, a little carelessness can easily lead to bugs. The code of the linked list is good or bad. It can be seen whether a person writes the code carefully enough to consider whether the problem is comprehensive and whether the thinking is meticulous. So, this is why many interviewers like to let people hand-write the linked list code.

Thinking questions

  Today we talked about using sentinels to simplify coding implementation. Can you think of other scenarios where using sentinels can greatly simplify the coding difficulty?

 

Guess you like

Origin www.cnblogs.com/murongmochen/p/12720129.html