Turbo Pascal for DOS Tutorial by Glenn Grotzinger Part 18: Chained or Linked lists, the linked list sort copyright(c) 1995-96 by Glenn Grotzinger Here is a solution from last time... {$M 64000,0,655360} program part17; uses crt; type arptr = ^atype; atype = array[1..15000] of integer; var a: arptr; number: integer; c, i, j, PIVOT, t: integer; found: boolean; location: integer; outfile: text; procedure quicksort(L, R: integer); { nothing to say we couldn't sort the data... } begin if wherex = 79 then begin gotoxy(1, wherey); clreol; end else write(#254); if L < R then begin i := L + 1; j := R; PIVOT := A^[L]; repeat while a^[i] <= PIVOT do inc(i); while a^[j] > PIVOT do dec(j); if i < j then begin t := A^[i]; A^[i] := a^[j]; A^[j] := t; end; until i > j; a^[l] := A^[j]; a^[j] := PIVOT; quicksort(L, j-1); quicksort(i, R); end; end; procedure bsearch(number, lowend, highend: integer; var found: boolean); var midpoint: integer; begin if lowend > highend then found := false else begin midpoint := (lowend + highend) div 2; if number = a^[midpoint] then begin found := true; location := midpoint; end else if number < a^[midpoint] then bsearch(number, lowend, midpoint-1, found) else if number > a^[midpoint] then bsearch(number, midpoint+1, highend, found); end; end; begin if maxavail - sizeof(a) > 0 then new(a) else begin writeln('Out of memory!'); halt(1); end; randomize; assign(outfile, 'LOCAT2.TXT'); rewrite(outfile); for c := 1 to 15000 do a^[c] := random(25000) + 1; quicksort(1, 15000); for c := 1 to 15000 do begin number := random(25000) + 1; bsearch(number, 1, 15000, found); if found then writeln(outfile, c, ') ', number, ' was found at position ', location, '.'); end; dispose(a); close(outfile); end. Now we will discuss the idea of the linked list or chained list. Basically, there are 4 types of linked lists that we can discuss, the singularly linked linear list (SLLL), singularly linked circular list (SLCL), doubly linked linear list (DLLL), and the doubly linked circular list (DLCL). I will use the abbreviations I placed in the parentheses for any future references to these data types. These are basically the preferred ways to allocate large amounts of storage space on the heap. All linked lists are basically describable in the best analogy as a chain. They are record structures which have pointers that interconnect them. The method that these structures are connected distinguish the type of linked list it is. We will look at an example of the use of SLLL's, observe the advantages of linked lists through what we do with the example, and study the things to look out for on all 4 types. SLLL Concepts ============= This is the simplest type, in sense. It involves a record structure which is connected in a chain in a linear fashion with one link forward to the next link. A sample record structure for an SLLL follows below. type nodeptr = ^node; nodetype = record ourinfo: integer; nextnode: nodeptr; end; An example of an SLLL conceptually is something like this: NODE-->NODE-->NODE-->NODE-->NODE-->NODE-->NODE-->nil As we remember from earlier, nil is what we set a pointer to, if we do not want it to point to anything...In the use of an SLLL, it is also what we will use to indicate whether we are at the end of the list or not. We will see from the slll_demo program that there are several specialized issues we need to take into consideration with working with any linked or chained list. 1) We need to make a special case to insert or delete a node from the start of the list. 2) We need to be sure to maintain nil pointers in any insert or delete operation. 3) NEVER NEVER NEVER WORK DIRECTLY WITH THE HEAD TRACKING POINTER WE ORIGINALLY ALLOCATE UNLESS WE DESIGN OUR CODE COMPLETELY AROUND RECURSION. As a result, you will cause what is called a heap leak. This is where the pointer loses track of where the data it points to is stored. Logically, looking at the model above, if we disconnect one of the pointers, represented by the arrows, we lose track of the rest of the list, or chain. Work with a temporary pointer for each linked list function. What I say by recursion, you will see later in this document. 4) With regards to the example I wrote, I tried to demonstrate any and all functions that we might need with an SLLL. 5) Pointers that point at nil CAN NOT be deallocated. You will see this fact manifest itself by the memory statement at the end being 8 bytes smaller than it was at the start. SLLLs Demonstrated ================== Here is the SLLL_DEMO program. I will place stop notes in there, as well as comments. Advantages of linked lists: We will see here, that the data is not static, we can place data independently at different positions WITHOUT shuffling data, remove data in the same fashion, and definitely are capable of handling *A LOT* more data than 64KB, since we only have a 4 byte stub in that area. Take a good look at this program and seek to understand EXACTLY how it works. As you will remember from last time, a direct assignment to a pointer is making it point to something while a reference to the pointer (via ^) changes the contents of the data it points to. I recommend you draw out what is going on via pencil and paper, using boxes to represent the records and arrows representing pointers. It will help you VASTLY to do this in understanding what is going on. Remember a pointer can only point to one thing at a time. When you look at this program, seek to answer the following questions taking any "housekeeping functions" out of consideration: 1) Why is the insert code different than the build code? 2) Why is the delete code different than the cleanup code? 3) On the "divisible by 8" search, why is the NEXT node being searched for this and not the current node? 4) Why did I say to always use a temporary variable? Or Why does the statement p := list; always occur? 5) Observe methods of moving through the list. program slll_demo; uses crt; { Program written by Glenn Grotzinger for a demonstration of all of the functions/uses of a linked list that the author could think of. the variable used throughout called p, and sometimes t, are temporary variables. Note: This probably isn't completely optimized. } type nodeptr = ^nodetype; nodetype = record thenum: integer; nextnode: nodeptr; end; var list: nodeptr; procedure buildlist(var list: nodeptr); { This procedure builds up the list for us. } var p: nodeptr; i: integer; begin new(list); { This is creating the head of the list } list^.thenum := 1; p := list; { Set and move temporary pointer } for i := 2 to 18 do begin new(p^.nextnode); p^.nextnode^.thenum := i; p := p^.nextnode; { p := p^.nextnode advances the temporary pointer to the next node. this is a memory storage address or pointer and not a direct variable, referencing a node of the linked list. Anything, in reality does not become a pointer until the new function is used. } end; p^.nextnode := nil; { set last pointer to nothing } end; procedure writelist(list: nodeptr); { This procedure serves the function of writing out the list for us to the screen when called } var p: nodeptr; begin p := list; while p <> nil do { while we're not at the end of the list } begin write(p^.thenum:3); p := p^.nextnode; end; end; procedure insert(var list: nodeptr); { This procedure will serve to insert a node into the list either in the middle or the end. The logic can be done for the head of the list. } var p: nodeptr; begin new(p); p^.thenum := 20; p^.nextnode := list; if p^.nextnode = nil then { maintenance of the end of list marker } p^.nextnode^.nextnode := nil; list := p; end; procedure delete(var list: nodeptr); { This is a procedure that will serve to delete a node from the list, and consequently deallocate the memory. It is possible to remove the node without deallocating the memory, though it is a bad practice to do so } var p, t: nodeptr; begin p := list; t := p^.nextnode^.nextnode; dispose(p^.nextnode); p^.nextnode := t; end; procedure insertbythree(var list: nodeptr); { This procedure moves through the linked list and determines where the new nodes needs to be inserted, then calls the insert function written before } var p: nodeptr; i: integer; begin p := list; i := 1; while p <> nil do begin p := p^.nextnode; inc(i); if i mod 3 = 0 then insert(p^.nextnode); end; end; procedure findanddispose(var list: nodeptr); { This procedure finds and disposes the first number in the list divisible by 8. } var p, t: nodeptr; begin p := list; while (p^.nextnode <> nil) and (p^.nextnode^.thenum mod 8 <> 0) do p := p^.nextnode; delete(p); end; procedure cleanup(var list: nodeptr); { This procedure removes the list from memory. } var p, t: nodeptr; begin p := list; while p <> nil do begin t := p^.nextnode; dispose(p); p := t; end; end; begin clrscr; writeln;writeln; writeln('Free memory available: ', memavail, ' bytes.'); buildlist(list); writeln('Free memory available: ', memavail, ' bytes.'); write('The list is: '); writelist(list); writeln;writeln; writeln('Now we will insert a 20 in every third position'); insertbythree(list); writeln('Free memory available: ', memavail, ' bytes.'); write('The list is: '); writelist(list); writeln;writeln; write('Now we will search for and take the first # divisible by 8 '); writeln('out of the list.'); findanddispose(list); writeln('Free memory available: ', memavail, ' bytes.'); write('The list is: '); writelist(list); writeln;writeln; writeln('Now we will be good little programmers and clean up our list. :)'); cleanup(list); writeln('Free memory available: ', memavail, ' bytes.'); end. Hopefully, you can go through here, and follow the logic (actually, you will need to do that successfully to understand what is going on). Linked lists are very modular in nature. Therefore, a good understanding of what is going on here is essential. As a proof to be able to think through the logic of using pointers in linked structures, write out and logically explain on your sheet of paper how to perform the following (I will not provide a solution for this one -- code it up yourself and try and figure it out -- this is an important skill you will need to start developing as a programmer at this point, since you're at a pretty advanced level now (:))), and then code it up as a program: 1) Create 1000 nodes in an SLLL that consists of integers numbered from 1 to 1000. 2) Print this list to a text file. 3) Reverse the direction of the linked list. By doing this, I mean, instead of the linked list looking like this conceptually: NODE-->NODE-->NODE-->nil make it look like this: nil<--NODE<--NODE<--NODE DO NOT CREATE ANOTHER LINKED LIST IN MEMORY. USE THE CURRENT ONE YOU HAVE BUILT. 4) Print the new list to the same text file. Instead of it being from 1 to 1000 as the first printing was, it should be from 1000 to 1. CLUE: Think about how many temporary variables you will need (1? Maybe 2?, Possibly 3?). SLCL Concepts ============= This is essentially the same as an SLLL, except instead of being nil at the end of the list, the end of the list points at the beginning of the list. This type uses the same record format as the SLLL. Conceptually, an SLCL looks like this: NODE--->NODE--->NODE--->NODE--->NODE ^ | | V NODE NODE ^ | | V NODE<---NODE<---NODE<---NODE<---NODE As before with the SLLL, one of these nodes would be denoted as the head of the list. The only consideration that would differ that I could note, is that you would use a comparison of your temporary pointer with your head pointer in order to move through the list. So instead of while p <> nil, it would be while p <> list in the above example to make it that way. DLLL Concepts ============= This type of linked list uses a different kind of record format. It looks like this: type nodeptr = ^node; node = record ourinfo: integer; lastnode, nextnode: nodeptr; end; Conceptually, a DLLL looks like this: nil <-- <-- <-- <-- NODE NODE NODE NODE --> --> --> --> nil If you study up your logic from previously, this one shouldn't be too awfully bad to figure out. DLCL Concepts ============= This type of list uses the same record format as the DLLL. The conceptual diagram looks much like the SLCL diagram, but with double links much like the DLLL diagram, instead of single links. Final Thoughts on Linked Lists ============================== I did not provide examples of SLCLs, DLLLs, and DLCLs , merely for space, and also by the fact that I have never had reason to use the other three types. I am presenting their basics here, merely for people's study, and learning. Using the knowledge learned from doing those logic problems presented in the SLLLs, and references (though I find those to be VERY sparse on the types other than SLLL), you should be able to come up with the code to do the other three types pretty readily. Always remember that the best thing to do to work out the logic of what to do with the pointers is to draw it out using the boxes and the arrows. An Idea on Sorting Data Using Linked Lists ========================================== Here, I will now present the reasoning behind my "recursion" statement, plus an idea of sorting data upon build. I don't have any stats on this being more or less efficient than using one of the array sorts, but if you can't use an array to sort in memory, you would have to resort to this. Here is a little code/pseudocode (with a bent toward sorting names alphabetically) For purposes of the recursion, we will call the function INSERT: IF WE ARE TO PUT NODE HERE GET DATA TO PUT INTO NODE (read data from file, or elsewhere) WHILE DATA IS NOT DONE DO BEGIN IF NIL LIST THEN PUT NODE HERE ELSE PUT NODE HERE = NEWNAME <= NODE^.NAME IF WE ARE TO PUT NODE HERE THEN BEGIN new(p); SET DATA TO NODE p^.nextnode := LIST; if p^.nextnode = nil then p^.nextnode^.nextnode = nil; list := p; END ELSE INSERT(LIST^.NEXTNODE); IF WE ARE TO PUT NODE HERE THEN BEGIN GET INFO. DO NOT PUT NODE HERE (boolean variable set to false). END. END. This general code does work for a high capacity. I have used this code to sort a maximum of an 86KB list of 150 char items per line alphabetically using memory alone, no disk swapping. For practice: Do things as I have suggested throughout this document. No real practice problem. Next Time ========= We will talk about binary trees. Be sure to send comments to ggrotz@2sprint.net. I will say again that I apologize for the long period of time it took to get this out. I also apologize for the length this document has become. Be sure to please comment on how this part is.