home *** CD-ROM | disk | FTP | other *** search
/ Programming Tool Box / SIMS_2.iso / bp_6_93 / bonus / winer / chap8.txt < prev    next >
Text File  |  1994-09-01  |  118KB  |  2,877 lines

  1.                                 CHAPTER 8
  2.  
  3.                           SORTING AND SEARCHING
  4.  
  5.  
  6. Two fundamental operations required of many applications are searching and
  7. sorting the data they operate on.  Many different types of data are
  8. commonly sorted, such as customer names, payment due dates, or even a list
  9. of file names displayed in a file selection menu.  If you are writing a
  10. programmer's cross reference utility, you may need to sort a list of
  11. variable names without regard to capitalization.  In some cases, you may
  12. want to sort several pieces of related information based on the contents
  13. of only one of them.  One example of that is a list of names and addresses
  14. sorted in ascending zip code order.
  15.    Searching is equally important; for example, to locate a customer name
  16. in an array or disk file.  In some cases you may wish to search for a
  17. complete match, while in others a partial match is needed.  If you are
  18. searching a list of names for, say, Leonard, you probably would want to
  19. ignore Leonardo.  But when searching a list of zip codes you may need to
  20. locate all that begin with the digits 068.  There are many different ways
  21. sorting and searching can be accomplished, and the subject is by no means
  22. a simple one.
  23.    Most programmers are familiar with the Bubble Sort, because it is the
  24. simplest to understand.  Each adjacent pair of items is compared, and then
  25. exchanged if they are out of order.  This process is repeated over and
  26. over, until the entire list has been examined as many times as there are
  27. items.  Unfortunately, these repeated comparisons make the Bubble Sort an
  28. extremely poor performer.  Similarly, code to perform a linear search that
  29. simply examines each item in succession for a match is easy to grasp, but
  30. it will be painfully slow when there are many items.
  31.    In this chapter you will learn how sophisticated algorithms that handle
  32. these important programming chores operate.  You will also learn how to
  33. sort data on more than one key.  Often, it is not sufficient to merely sort
  34. a list of customers by their last name.  For example, you may be expected
  35. to sort first by last name, then by first name, and finally by balance due. 
  36. That is, all of the last names would first be sorted.  Then within all of
  37. the Smiths you would sort again by first name, and for all of the John
  38. Smiths sort that subgroup based on how much money is owed.
  39.    For completeness I will start each section by introducing sorting and
  40. searching methods that are easy to understand, and then progress to the
  41. more complex algorithms that are much more effective.  Specifically, I will
  42. show the Quick Sort and Binary Search algorithms.  When there are many
  43. thousands of data items, a good algorithm can make the difference between
  44. a sort routine that takes ten minutes to complete, and one that needs only
  45. a few seconds.
  46.    Finally, I will discuss both BASIC and assembly language sort routines. 
  47. As important as the right algorithm is for good performance, an assembly
  48. language implementation will be even faster.  Chapter 12 describes how
  49. assembly language routines are written and how they work, and in this
  50. chapter I will merely show how to use the routines included with this book.
  51.  
  52.  
  53. SORTING FUNDAMENTALS
  54. ====================
  55.  
  56. Although there are many different ways to sort an array, the simplest
  57. sorting algorithm is the Bubble Sort.  The name Bubble is used because a
  58. FOR/NEXT loop repeatedly examines each adjacent pair of elements in the
  59. array, and those that have higher values rise to the top like bubbles in
  60. a bathtub.  The most common type of sort is ascending, which means that "A"
  61. comes before "B", which comes before "C", and so forth.  Figure 8-1 shows
  62. how the name Zorba ascends to the top of a five-item list of first names.
  63.  
  64. Initial array contents:
  65.  
  66.   Element 4    Kathy
  67.   Element 3    Barbara
  68.   Element 2    Cathy
  69.   Element 1    Zorba <
  70.  
  71.  
  72.       After 1 pass:
  73.  
  74.         Element 4    Kathy
  75.         Element 3    Barbara
  76.         Element 2    Zorba <
  77.         Element 1    Cathy
  78.  
  79.  
  80.             After 2 passes:
  81.  
  82.               Element 4    Kathy
  83.               Element 3    Zorba <
  84.               Element 2    Barbara
  85.               Element 1    Cathy
  86.  
  87.  
  88.                 After 3 passes:
  89.  
  90.                   Element 4    Zorba <
  91.                   Element 3    Kathy
  92.                   Element 2    Barbara
  93.                   Element 1    Cathy
  94.  
  95. Figure 8.1: Data ascending a list during a bubble sort.
  96.  
  97. The Bubble Sort routine that follows uses a FOR/NEXT loop to repeatedly
  98. examine an array and exchange elements as necessary, until all of the items
  99. are in the correct order.
  100.  
  101. DEFINT A-Z
  102. DECLARE SUB BubbleSort (Array$())
  103.  
  104. CONST NumItems% = 20
  105. CONST False% = 0
  106. CONST True% = -1
  107.  
  108. DIM Array$(1 TO NumItems%)
  109. FOR X = 1 TO NumItems%
  110.   READ Array$(X)
  111. NEXT
  112.  
  113. CALL BubbleSort(Array$())
  114.  
  115. CLS
  116. FOR X = 1 TO NumItems%
  117.   PRINT Array$(X)
  118. NEXT
  119.  
  120. DATA Zorba, Cathy, Barbara, Kathy, Josephine
  121. DATA Joseph, Joe, Peter, Arnold, Glen
  122. DATA Ralph, Elli, Lucky, Rocky, Louis
  123. DATA Paula, Paul, Mary Lou, Marilyn, Keith
  124. END
  125.  
  126. SUB BubbleSort (Array$()) STATIC
  127.  
  128. DO
  129.   OutOfOrder = False%                 'assume it's sorted
  130.   FOR X = 1 TO UBOUND(Array$) - 1
  131.     IF Array$(X) > Array$(X + 1) THEN
  132.       SWAP Array$(X), Array$(X + 1)   'if we had to swap
  133.       OutOfOrder = True%              'we may not be done
  134.     END IF
  135.   NEXT
  136. LOOP WHILE OutOfOrder
  137.  
  138. END SUB
  139.  
  140. This routine is simple enough to be self-explanatory, and only a few things
  141. warrant discussing.  One is the OutOfOrder flag variable.  When the array
  142. is nearly sorted to begin with, fewer passes through the loop are needed. 
  143. The OutOfOrder variable determines when no more passes are necessary.  It
  144. is cleared at the start of each loop, and set each time two elements are
  145. exchanged.  If, after examining all of the elements in one pass no
  146. exchanges were required, then the sorting is done and there's no need for
  147. the DO loop to continue.
  148.    The other item worth mentioning is that the FOR/NEXT loop is set to
  149. consider one element less than the array actually holds.  This is necessary
  150. because each element is compared to the one above it.  If the last element
  151. were included in the loop, then BASIC would issue a "Subscript out of
  152. range" error on the statement that examines Array$(X + 1).
  153.    There are a number of features you can add to this Bubble Sort routine. 
  154. For example, you could sort without regard to capitalization.  In that case
  155. "adams" would come before "BAKER", even though the lowercase letter "a" has
  156. a higher ASCII value than the uppercase letter "B".  To add that capability
  157. simply use BASIC's UCASE$ (or LCASE$) function as part of the comparisons:
  158.  
  159.    IF UCASE$(Array$(X)) > UCASE$(Array$(X + 1)) THEN
  160.  
  161. And to sort based on the eight-character portion that starts six bytes
  162. into each string you would use this:
  163.  
  164.    IF MID$(Array$(X), 5, 8) > MID$(Array$(X + 1), 5, 8) THEN
  165.  
  166. Although the comparisons in this example are based on just a portion of
  167. each string, the SWAP statement must exchange the entire elements.  This
  168. opens up many possibilities as you will see later in this chapter.
  169.    If there is a chance that the strings may contain trailing blanks that
  170. should be ignored, you can use RTRIM$ on each pair of elements:
  171.  
  172.    IF RTRIM$(Array$(X)) > RTRIM$(Array$(X + 1)) THEN
  173.  
  174. Of course, you can easily combine these enhancements to consider only the
  175. characters in the middle after they have been converted to upper or lower
  176. case.
  177.    Sorting in reverse (descending) order is equally easy; you'd simply
  178. replace the greater-than symbol (>) with a less-than symbol (<).
  179.    Finally, you can modify the routine to work with any type of data by
  180. changing the array type identifier.  That is, for every occurrence of
  181. Array$ you will change that to Array% or Array# or whatever is appropriate. 
  182. If you are sorting a numeric array, then different modifications may be in
  183. order.  For example, to sort ignoring whether the numbers are positive or
  184. negative you would use BASIC's ABS (absolute value) function:
  185.  
  186.    IF ABS(Array!(X)) > ABS(Array!(X + 1)) THEN
  187.  
  188. It is important to point out that all of the simple modifications described
  189. here can also be applied to the more sophisticated sort routines we will
  190. look at later in this chapter.
  191.  
  192.  
  193. INDEXED SORTS
  194.  
  195. Besides the traditional sorting methods--whether a Bubble Sort or Quick
  196. Sort or any other type of sort--there is another category of sort routine
  197. you should be familiar with.  Where a conventional sort exchanges elements
  198. in an array until they are in order, an Index Sort instead exchanges
  199. elements in a parallel numeric array of *pointers*.  The original data is left
  200. intact, so it may still be accessed in its natural order.  However, the
  201. array can also be accessed in sorted order by using the element numbers
  202. contained in the index array.
  203.    As with a conventional sort, the comparisons in an indexed sort routine
  204. examine each element in the primary array, but based on the element numbers
  205. in that index array.  If it is determined that the data is out of order,
  206. the routine exchanges the elements in the index array instead of the
  207. primary array.  A modification to the Bubble Sort routine to sort using an
  208. index is shown below.
  209.  
  210. DEFINT A-Z
  211. DECLARE SUB BubbleISort (Array$(), Index())
  212.  
  213. CONST NumItems% = 20
  214. CONST False% = 0
  215. CONST True% = -1
  216.  
  217. DIM Array$(1 TO NumItems%)  'this holds the string data
  218. DIM Ndx(1 TO NumItems%)     'this holds the index
  219.  
  220. FOR X = 1 TO NumItems%
  221.   READ Array$(X)            'read the string data
  222.   Ndx(X) = X                'initialize the index array
  223. NEXT
  224.  
  225. CALL BubbleISort(Array$(), Ndx())
  226.  
  227. CLS
  228. FOR X = 1 TO NumItems%
  229.   PRINT Array$(Ndx(X))      'print based on the index
  230. NEXT
  231.  
  232. DATA Zorba, Cathy, Barbara, Kathy, Josephine
  233. DATA Joseph, Joe, Peter, Arnold, Glen
  234. DATA Ralph, Elli, Lucky, Rocky, Louis
  235. DATA Paula, Paul, Mary lou, Marilyn, Keith
  236.  
  237. SUB BubbleISort (Array$(), Index()) STATIC
  238.  
  239. DO
  240.   OutOfOrder = False%                 'assume it's sorted
  241.   FOR X = 1 TO UBOUND(Array$) - 1
  242.     IF Array$(Index(X)) > Array$(Index(X + 1)) THEN
  243.       SWAP Index(X), Index(X + 1)     'if we had to swap
  244.       OutOfOrder% = True%             'we're not done yet
  245.     END IF
  246.   NEXT
  247. LOOP WHILE OutOfOrder%
  248.  
  249. END SUB
  250.  
  251. In this indexed sort, all references to the data are through the index
  252. array.  And when a swap is necessary, it is the index array elements that
  253. are exchanged.  Note that an indexed sort requires that the index array be
  254. initialized to increasing values--even if the sort routine is modified to
  255. be descending instead of ascending.  Therefore, when BubbleISort is called
  256. Ndx(1) must hold the value 1, Ndx(2) is set to 2, and so forth.
  257.    In this example the index array is initialized by the caller.  However,
  258. it would be just as easy to put that code into the subprogram itself. 
  259. Since you can't pass an array that hasn't yet been dimensioned, it makes
  260. the most sense to do both steps outside of the subprogram.  Either way, the
  261. index array must be assigned to these initial values.
  262.    As I mentioned earlier, one feature of an indexed sort is that it lets
  263. you access the data in both its original and sorted order.  But there are
  264. other advantages, and a disadvantage as well.  The disadvantage is that
  265. each comparison takes slightly longer, because of the additional overhead
  266. required to first look up the element number in the index array, to
  267. determine which elements in the primary array will be compared.  In some
  268. cases, though, that can be more than offset by requiring less time to
  269. exchange elements.
  270.    If you are sorting an array of 230-byte TYPE variables, the time needed
  271. for SWAP to exchange the elements can become considerable.  Every byte in
  272. both elements must be read and written, so the time needed increases
  273. linearly as the array elements become longer.  Contrast that with the fixed
  274. two bytes in the integer index array that are swapped.
  275.    Another advantage of an indexed sort is that it lends itself to sorting
  276. more data than can fit in memory.  As you will see later in the section
  277. that shows how to sort files, it is far easier to manipulate an integer
  278. index than an entire file.  Further, sorting the file data using multiple
  279. passes requires twice as much disk space as the file already occupies.
  280.  
  281.  
  282. DATA MANIPULATION TECHNIQUES
  283.  
  284. Before I show the Quick Sort algorithm that will be used as a basis for
  285. the remaining sort examples in this chapter, you should also be aware of
  286. a few simple tricks that can help you maintain and sort your data.  One was
  287. described in Chapter 6, using a pair of functions that pack and unpack
  288. dates such that the year is stored before the month, which in turn is
  289. before the day.  Thus, date strings are reduced to only three characters
  290. each, and they can be sorted directly.
  291.    Another useful speed-up trick is to store string data as integers or
  292. long integers.  If you had a system of four-digit account numbers you could
  293. use an integer instead of a string.  Besides saving half the memory and
  294. disk space, the integer comparisons in a sort routine will be many times
  295. faster than a comparison on string equivalents.  Zip codes are also suited
  296. to this, and could be stored in a long integer.  Even though the space
  297. savings is only one byte, the time needed to compare the values for sorting
  298. will be greatly reduced.
  299.    This brings up another important point.  As you learned in Chapter 2,
  300. all conventional (not fixed-length) strings require more memory than might
  301. be immediately apparent.  Besides the amount of memory needed to hold the
  302. data itself, four additional bytes are used for a string descriptor, and
  303. two more beyond those for a back pointer.  Therefore, a zip code stored as
  304. a string will actually require eleven bytes rather than the five you might
  305. expect.  With this in mind, you may be tempted to think that using a fixed-
  306. length string to hold the zip codes will solve the problem.  Since fixed-
  307. length strings do not use either descriptors or back pointers, they do not
  308. need the memory they occupy.  And that leads to yet another issue.
  309.    Whenever a fixed-length string or the string portion of a TYPE variable
  310. is compared, it must first be converted to a regular descriptored string. 
  311. BASIC has only one string comparison routine, and it expects the addresses
  312. for two conventional string descriptors.  Every time a fixed-length string
  313. is used as an argument for comparison, BASIC must create a temporary copy,
  314. call its comparison routine, and then delete the copy.  This copying adds
  315. code and wastes an enormous amount of time; in many cases the copying will
  316. take longer than the comparison itself.  Therefore, using integers and long
  317. integers for numeric data where possible will provide more improvement than
  318. just the savings in memory use.
  319.    In some cases, however, you must use fixed-length string or TYPE arrays. 
  320. In particular, when sorting information from a random access disk file it
  321. is most sensible to load the records into a TYPE array.  And as you learned
  322. in Chapter 2, the string components of a TYPE variable or array element are
  323. handled by BASIC as a fixed-length string.  So how can you effectively sort
  324. fixed-length string arrays without incurring the penalty BASIC's overhead
  325. imposes?  With assembly language subroutines, of course!
  326.    Rather than ask BASIC to pass the data using its normal methods,
  327. assembly language routines can be invoked passing the data segments and
  328. addresses directly.  When you use SEG, or a combination of VARSEG and
  329. VARPTR with fixed-length and TYPE variables, BASIC knows that you want the
  330. segmented address of the variable or array element.  Thus, you are tricking
  331. BASIC into not making a copy as it usually would when passing such data. 
  332. An assembly language subroutine or function can be designed to accept data
  333. addresses in any number of ways.  As you will see later when we discuss
  334. sorting on multiple keys, extra trickery is needed to do the same thing in
  335. a BASIC procedure.
  336.    The three short assembly language functions that follow compare two
  337. portions of memory, and then return a result that can be tested by your
  338. program.
  339.  
  340. ;COMPARE.ASM - compares two ranges of memory
  341.  
  342. .Model Medium, Basic
  343. .Code
  344.  
  345. Compare Proc Uses DS ES DI SI, SegAdr1:DWord, _
  346.   SegAdr2:DWord, NumBytes:Word
  347.  
  348.     Cld                ;compare in the forward direction
  349.     Mov  SI,NumBytes   ;get the address for NumBytes%
  350.     Mov  CX,[SI]       ;put it into CX for comparing below
  351.  
  352.     Les  DI,SegAdr1    ;load ES:DI with the first
  353.                        ;  segmented address
  354.     Lds  SI,SegAdr2    ;load DS:SI with the second
  355.                        ;  segmented address
  356.  
  357.     Repe Cmpsb         ;do the compare
  358.     Mov  AX,0          ;assume the bytes didn't match
  359.     Jne  Exit          ;we were right, skip over
  360.     Dec  AX            ;wrong, decrement AX down to -1
  361.  
  362. Exit:
  363.     Ret                ;return to BASIC
  364.  
  365. Compare Endp
  366. End
  367.  
  368. ;COMPARE2.ASM - compares memory case-insensitive
  369.  
  370. .Model Medium, Basic
  371. .Code
  372.  
  373. Compare2 Proc Uses DS ES DI SI, SegAdr1:DWord, _
  374.   SegAdr2:DWord, NumBytes:Word
  375.  
  376.     Cld                ;compare in the forward direction
  377.     Mov  BX,-1         ;assume the ranges are the same
  378.  
  379.     Mov  SI,NumBytes   ;get the address for NumBytes%
  380.     Mov  CX,[SI]       ;put it into CX for comparing below
  381.     Jcxz Exit          ;if zero bytes were given, they're
  382.                        ;  the same
  383.     Les  DI,SegAdr1    ;load ES:DI with the first address
  384.     Lds  SI,SegAdr2    ;load DS:SI with the second address
  385.  
  386. Do:
  387.     Lodsb              ;load the current character from
  388.                        ;  DS:SI into AL
  389.     Call Upper         ;capitalize as necessary
  390.     Mov  AH,AL         ;copy the character to AH
  391.     
  392.     Mov  AL,ES:[DI]    ;load the other character into AL
  393.     Inc  DI            ;point at the next one for later
  394.     Call Upper         ;capitalize as necessary
  395.  
  396.     Cmp  AL,AH         ;now, are they the same?
  397.     Jne  False         ;no, exit now and show that
  398.     Loop Do            ;yes, continue
  399.     Jmp  Short Exit    ;if we get this far, the bytes are
  400.                        ;  all the same
  401. False:
  402.     Inc  BX            ;increment BX to return zero
  403.     
  404. Exit:
  405.     Mov  AX,BX         ;assign the function output
  406.     Ret                ;return to BASIC
  407.  
  408. Upper:
  409.     Cmp  AL,"a"        ;is the character below an "a"?
  410.     Jb   Done          ;yes, so we can skip it
  411.     Cmp  AL,"z"        ;is the character above a "z"?
  412.     Ja   Done          ;yes, so we can skip that too
  413.     Sub  AL,32         ;convert to upper case
  414.  
  415. Done:
  416.     Retn               ;do a near return to the caller
  417.  
  418. Compare2 Endp
  419. End
  420.  
  421. ;COMPARE3.ASM - case-insensitive, greater/less than
  422.  
  423. .Model Medium, Basic
  424. .Code
  425.  
  426. Compare3 Proc Uses DS ES DI SI, SegAdr1:DWord, _
  427.   SegAdr2:DWord, NumBytes:Word
  428.  
  429.     Cld               ;compare in the forward direction
  430.     Xor  BX,BX        ;assume the ranges are the same
  431.  
  432.     Mov  SI,NumBytes  ;get the address for NumBytes%
  433.     Mov  CX,[SI]      ;put it into CX for comparing below
  434.     Jcxz Exit         ;if zero bytes were given, they're
  435.                       ;  the same
  436.     Les  DI,SegAdr1   ;load ES:DI with the first address
  437.     Lds  SI,SegAdr2   ;load DS:SI with the second address
  438.  
  439. Do:
  440.     Lodsb             ;load the current character from
  441.                       ;  DS:SI into AL
  442.     Call Upper        ;capitalize as necessary, remove for
  443.                       ;  case-sensitive
  444.     Mov  AH,AL        ;copy the character to AH
  445.  
  446.     Mov  AL,ES:[DI]   ;load the other character into AL
  447.     Inc  DI           ;point at the next character for later
  448.     Call Upper        ;capitalize as necessary, remove for
  449.                       ;  case-sensitive
  450.  
  451.     Cmp  AL,AH        ;now, are they the same?
  452.     Loope Do          ;yes, continue
  453.     Je   Exit         ;we exhausted the data and they're
  454.                       ;  the same
  455.     Mov  BL,1         ;assume block 1 was "greater"
  456.     Ja   Exit         ;we assumed correctly
  457.     Dec  BX           ;wrong, bump BX down to -1
  458.     Dec  BX
  459.  
  460. Exit:
  461.     Mov  AX,BX        ;assign the function output
  462.     Ret               ;return to BASIC
  463.  
  464. Upper:
  465.     Cmp  AL,"a"       ;is the character below an "a"?
  466.     Jb   Done         ;yes, so we can skip it
  467.     Cmp  AL,"z"       ;is the character above a "z"?
  468.     Ja   Done         ;yes, so we can skip that too
  469.     Sub  AL,32        ;convert to upper case
  470.  
  471. Done:
  472.     Retn              ;do a near return to the caller
  473.  
  474. Compare3 Endp
  475. End
  476.  
  477. The first Compare routine above simply checks if all of the bytes are
  478. identical, and returns -1 (True) if they are, or 0 (False) if they are not. 
  479. By returning -1 or 0 you can use either
  480.  
  481.    IF Compare%(Type1, Type2, NumBytes%) THEN
  482. or
  483.    IF NOT Compare%(Type1, Type2, NumBytes%) THEN
  484.  
  485. depending on which logic is clearer for your program.  Compare2 is similar
  486. to Compare, except it ignores capitalization.  That is, "SMITH" and Smith"
  487. are considered equal.  The Compare3 function also compares memory and
  488. ignores capitalization, but it returns either -1, 0, or 1 to indicate if
  489. the first data range is less than, equal to, or greater than the second.
  490.    The correct declaration and usage for each of these routines is shown
  491. below.  Note that Compare and Compare2 are declared and used in the same
  492. fashion.
  493.  
  494.  
  495. Compare and Compare2:
  496.  
  497.    DECLARE FUNCTION Compare%(SEG Type1 AS ANY, SEG Type2 AS ANY, _
  498.      NumBytes%)
  499.    Same = Compare%(Type1, Type2, NumBytes%)
  500.  
  501. or
  502.  
  503.    DECLARE FUNCTION Compare%(BYVAL Seg1%, BYVAL Adr1%, BYVAL Seg2%, _
  504.      BYVAL Adr2%, NumBytes%)
  505.    Same = Compare%(Seg1%, Adr1%, Seg2%, Adr2%, NumBytes%)
  506.  
  507.  
  508. Here, Same receives -1 if the two TYPE variables or ranges of memory are
  509. the same, or 0 if they are not.  NumBytes% tells how many bytes to compare.
  510.  
  511.  
  512. Compare3:
  513.  
  514.    DECLARE FUNCTION Compare3%(SEG Type1 AS ANY, SEG Type2 AS ANY, _
  515.      NumBytes%)
  516.    Result = Compare3%(Type1, Type2, NumBytes%)
  517.  
  518. or
  519.    DECLARE FUNCTION Compare3%(BYVAL Seg1%, BYVAL Adr1%, BYVAL Seg2%, _
  520.      BYVAL Adr2%, NumBytes%)
  521.    Result = Compare3%(Seg1%, Adr1%, Seg2%, Adr2%, NumBytes%)
  522.  
  523.  
  524. Result receives 0 if the two type variables or ranges of memory are the
  525. same, -1 if the first is less when compared as strings, or 1 if the first
  526. is greater.  NumBytes% tells how many bytes are to be to compared.  In the
  527. context of a sort routine you could invoke Compare3 like this:
  528.  
  529.  
  530. IF Compare3%(TypeEl(X), TypeEl(X + 1), NumBytes%) = 1 THEN
  531.   SWAP TypeEl(X), TypeEl(X + 1)
  532. END IF
  533.  
  534.  
  535. As you can see, these routines may be declared in either of two ways. 
  536. When used with TYPE arrays the first is more appropriate and results in
  537. slightly less setup code being generated by the compiler.  When comparing
  538. fixed-length strings or arbitrary blocks of memory (for example, when one
  539. of the ranges is on the display screen) you should use the second method. 
  540. Since SEG does not work correctly with fixed-length strings, if you want
  541. to use that more efficient version you must create a dummy TYPE comprised
  542. solely of a single string portion:
  543.  
  544.  
  545. TYPE FixedLength
  546.   Something AS STRING * 35
  547. END TYPE
  548.  
  549.  
  550. Then simply use DIM to create a single variable or an array based on this
  551. or a similar TYPE, depending on what your program needs.  The requirement
  552. to create a dummy TYPE was discussed in Chapter 2, and I won't belabor the
  553. reasons again here.  These comparison routines will be used extensively in
  554. the sort routines presented later in this chapter; however, their value in
  555. other, non-sorting situations should also be apparent.
  556.    Although these routines are written in assembly language, they are
  557. fairly simple to follow.  It is important to understand that you do not
  558. need to know anything about assembly language to use them.  All of the
  559. files you need to add these and all of the other routines in this book are
  560. contained on the accompanying diskette [here, in the same ZIP file as this
  561. text].  Chapter 12 discusses assembly language in great detail, and you can
  562. refer there for further explanation of the instructions used.
  563.    If you plan to run the programs that follow in the QuickBASIC editor,
  564. you must load the BASIC.QLB Quick Library as follows:
  565.  
  566.    qb program /l basic
  567.  
  568. Later when you compile these or other programs you must link with the
  569. parallel BASIC.LIB file:
  570.  
  571.    bc program [/o];
  572.    link program , , nul , basic;
  573.  
  574. If you are using BASIC PDS start QBX using the BASIC7.QLB file, and then
  575. link with BASIC7.LIB to produce a stand-alone .EXE program.  [VB/DOS users
  576. will also use the BASIC7 version.]
  577.  
  578.  
  579. THE QUICK SORT ALGORITHM
  580. ========================
  581.  
  582. It should be obvious to you by now that a routine written in assembly
  583. language will always be faster than an equivalent written in BASIC. 
  584. However, simply translating a procedure to assembly language is not always
  585. the best solution.  Far more important than which language you use is
  586. selecting an appropriate algorithm.  The best sorting method I know is the
  587. Quick Sort, and a well-written version of Quick Sort using BASIC will be
  588. many times faster than an assembly language implementation of the Bubble
  589. Sort.
  590.    The main problem with the Bubble Sort is that the number of comparisons
  591. required grows exponentially as the number of elements increases.  Since
  592. each pass through the array exchanges only a few elements, many passes are
  593. required before the entire array is sorted.  The Quick Sort was developed
  594. by C.A.R. (Tony) Hoare, and is widely recognized as the fastest algorithm
  595. available.  In some special cases, such as when the data is already sorted
  596. or nearly sorted, the Quick Sort may be slightly slower than other methods. 
  597. But in most situations, a Quick Sort is many times faster than any other
  598. sorting algorithm.
  599.    As with the Bubble Sort, there are many different variations on how a
  600. Quick Sort may be coded.  (You may have noticed that the Bubble Sort shown
  601. in Chapter 7 used a nested FOR/NEXT loop, while the one shown here uses a
  602. FOR/NEXT loop within a DO/WHILE loop.)  A Quick Sort divides the array into
  603. sections--sometimes called partitions--and then sorts each section
  604. individually.  Many implementations therefore use recursion to invoke the
  605. subprogram from within itself, as each new section is about to be sorted. 
  606. However, recursive procedures in any language are notoriously slow, and
  607. also consume stack memory at an alarming rate.
  608.    The Quick Sort version presented here avoids recursion, and instead uses
  609. a local array as a form of stack.  This array stores the upper and lower
  610. bounds showing which section of the array is currently being considered. 
  611. Another refinement I have added is to avoid making a copy of elements in
  612. the array.  As a Quick Sort progresses, it examines one element selected
  613. arbitrarily from the middle of the array, and compares it to the elements
  614. that lie above and below it.  To avoid assigning a temporary copy this
  615. version simply keeps track of the selected element number.
  616.    When sorting numeric data, maintaining a copy of the element is
  617. reasonable.  But when sorting strings--especially strings whose length is
  618. not known ahead of time--the time and memory required to keep a copy can
  619. become problematic.  For clarity, the generic Quick Sort shown below uses
  620. the copy method.  Although this version is meant for sorting a single
  621. precision array, it can easily be adapted to sort any type of data by
  622. simply changing all instances of the "!" type declaration character.
  623.  
  624. '******** QSORT.BAS, Quick Sort algorithm demonstration
  625.  
  626. 'Copyright (c) 1991 Ethan Winer
  627.  
  628. DEFINT A-Z
  629. DECLARE SUB QSort (Array!(), StartEl, NumEls)
  630.  
  631. RANDOMIZE TIMER         'generate a new series each run
  632.  
  633. DIM Array!(1 TO 21)             'create an array
  634. FOR X = 1 TO 21                 'fill with random numbers
  635.   Array!(X) = RND(1) * 500      'between 0 and 500
  636. NEXT
  637.  
  638. FirstEl = 6                     'sort starting here
  639. NumEls = 10                     'sort this many elements
  640.  
  641. CLS
  642. PRINT "Before Sorting:"; TAB(31); "After sorting:"
  643. PRINT "==============="; TAB(31); "=============="
  644.  
  645. FOR X = 1 TO 21                 'show them before sorting
  646.   IF X >= FirstEl AND X <= FirstEl + NumEls - 1 THEN
  647.     PRINT "==>";
  648.   END IF
  649.   PRINT TAB(5); USING "###.##"; Array!(X)
  650. NEXT
  651.  
  652. CALL QSort(Array!(), FirstEl, NumEls)
  653.  
  654. LOCATE 3
  655. FOR X = 1 TO 21                 'print them after sorting
  656.   LOCATE , 30
  657.   IF X >= FirstEl AND X <= FirstEl + NumEls - 1 THEN
  658.     PRINT "==>";                'point to sorted items
  659.   END IF
  660.   LOCATE , 35
  661.   PRINT USING "###.##"; Array!(X)
  662. NEXT
  663.  
  664. SUB QSort (Array!(), StartEl, NumEls) STATIC
  665.  
  666. REDIM QStack(NumEls \ 5 + 10)  'create a stack array
  667.  
  668. First = StartEl                'initialize work variables
  669. Last = StartEl + NumEls - 1
  670.  
  671. DO
  672.   DO
  673.     Temp! = Array!((Last + First) \ 2)  'seek midpoint
  674.     I = First
  675.     J = Last
  676.  
  677.     DO     'reverse both < and > below to sort descending
  678.       WHILE Array!(I) < Temp!
  679.         I = I + 1
  680.       WEND
  681.       WHILE Array!(J) > Temp!
  682.         J = J - 1
  683.       WEND
  684.       IF I > J THEN EXIT DO
  685.       IF I < J THEN SWAP Array!(I), Array!(J)
  686.       I = I + 1
  687.       J = J - 1
  688.     LOOP WHILE I <= J
  689.  
  690.     IF I < Last THEN
  691.       QStack(StackPtr) = I              'Push I
  692.       QStack(StackPtr + 1) = Last       'Push Last
  693.       StackPtr = StackPtr + 2
  694.     END IF
  695.  
  696.     Last = J
  697.   LOOP WHILE First < Last
  698.  
  699.   IF StackPtr = 0 THEN EXIT DO          'Done
  700.   StackPtr = StackPtr - 2
  701.   First = QStack(StackPtr)              'Pop First
  702.   Last = QStack(StackPtr + 1)           'Pop Last
  703. LOOP
  704.  
  705. ERASE QStack               'delete the stack array
  706.  
  707. END SUB
  708.  
  709. Notice that I have designed this routine to allow sorting only a portion
  710. of the array.  To sort the entire array you'd simply omit the StartEl and
  711. NumEls parameters, and assign First and Last from the LBOUND and UBOUND
  712. element numbers.  That is, you will change these:
  713.  
  714.    First = StartEl
  715. and
  716.    Last = StartEl + NumEls - 1
  717.  
  718. to these:
  719.  
  720.    First = LBOUND(Array!)
  721. and
  722.    Last = UBOUND(Array!)
  723.  
  724. As I mentioned earlier, the QStack array serves as a table of element
  725. numbers that reflect which range of elements is currently being considered. 
  726. You will need to dimension this array to one element for every five
  727. elements in the primary array being sorted, plus a few extra for good
  728. measure.  In this program I added ten elements, because one stack element
  729. for every five main array elements is not enough for very small arrays. 
  730. For data arrays that have a large amount of duplicated items, you will
  731. probably need to increase the size of the stack array.
  732.    Note that this ratio is not an absolute--the exact size of the stack
  733. that is needed depends on how badly out of order the data is to begin with. 
  734. Although it is possible that one stack element for every five in the main
  735. array is insufficient in a given situation, I have never seen this formula
  736. fail.  Because the stack is a dynamic integer array that is stored in far
  737. memory, it will not impinge on near string memory.  If this routine were
  738. designed using the normal recursive method, BASIC's stack would be used
  739. which is in near memory.
  740.    Each of the innermost DO loops searches the array for the first element
  741. in each section about the midpoint that belongs in the other section.  If
  742. the elements are indeed out of order (when I is less than J) the elements
  743. are exchanged.  This incrementing and comparing continues until I and J
  744. cross.  At that point, assuming the variable I has not exceeded the upper
  745. limits of the current partition, the partition bounds are saved and Last
  746. is assigned to the top of the next inner partition level.  When the entire
  747. partition has been processed, the previous bounds are retrieved, but as a
  748. new set of First and Last values.  This process continues until no more
  749. partition boundaries are on the stack.  At that point the entire array is
  750. sorted.
  751.    On the accompanying disk you will find a program called SEEQSORT.BAS
  752. that contains an enhanced version of the QSort demo and subprogram.  This
  753. program lets you watch the progress of the comparisons and exchanges as
  754. they are made, and actually see this complex algorithm operate.  Simply
  755. load SEEQSORT.BAS into the BASIC editor and run it.  A constant named
  756. Delay! is defined at the beginning of the program.  Increasing its value
  757. makes the program run more slowly; decreasing it causes the program to run
  758. faster.
  759.  
  760.  
  761. AN ASSEMBLY LANGUAGE QUICK SORT
  762.  
  763. As fast as the BASIC QuickSort routine is, we can make it even faster. 
  764. The listing below shows an assembly language version that is between ten
  765. and twenty percent faster, depending on which compiler you are using and
  766. if the BASIC PDS /fs (far strings) option is in effect.
  767.  
  768. ;SORT.ASM - sorts an entire BASIC string array
  769.  
  770. .Model Medium, Basic
  771. .Data
  772.     S          DW 0
  773.     F          DW 0
  774.     L          DW 0
  775.     I          DW 0
  776.     J          DW 0
  777.     MidPoint   DW 0
  778.  
  779. .Code
  780.     Extrn B$SWSD:Proc   ;this swaps two strings
  781.     Extrn B$SCMP:Proc   ;this compares two strings
  782.  
  783. Sort Proc Uses SI DI ES, Array:Word, Dir:Word
  784.  
  785.     Cld                 ;all fills and compares are forward
  786.     Push DS             ;set ES = DS for string compares
  787.     Pop  ES
  788.     
  789.     Xor  CX,CX          ;clear CX
  790.     Mov  AX,7376h       ;load AL and AH with the opcodes
  791.                         ;  Jae and Jbe in preparation for
  792.                         ;  code self-modification
  793.     Mov  BX,Dir         ;get the sorting direction
  794.     Cmp  [BX],CX        ;is it zero (ascending sort)?
  795.     Je   Ascending      ;yes, skip ahead
  796.     Xchg AL,AH          ;no exchange the opcodes
  797.  
  798. Ascending:
  799.     Mov  CS:[X1],AH     ;install correct comparison opcodes
  800.     Mov  CS:[X2],AL     ;  based on the sort direction
  801.  
  802.     Mov  BX,Array       ;load the array descriptor address
  803.     Mov  AX,[BX+0Eh]    ;save the number of elements
  804.     Dec  AX             ;adjust the number to zero-based
  805.     Jns  L0             ;at least 1 element, continue
  806.     Jmp  L4             ;0 or less elements, get out now!
  807.  
  808. L0:
  809.     Mov  BX,Array       ;reload array descriptor address
  810.     Mov  BX,[BX]        ;Array$(LBOUND) descriptor address
  811.     Mov  S,SP           ;StackPtr = 0 (normalized to SP)
  812.     Mov  F,CX           ;F = 0
  813.     Mov  L,AX           ;L = Size%
  814.  
  815. ;----- calculate the value of MidPoint
  816. L1:
  817.     Mov  DI,L           ;MidPoint = (L + F) \ 2
  818.     Add  DI,F
  819.     Shr  DI,1
  820.     Mov  MidPoint,DI
  821.  
  822.     Mov  AX,F           ;I = F
  823.     Mov  I,AX
  824.  
  825.     Mov  AX,L           ;J = L
  826.     Mov  J,AX
  827.  
  828. ;----- calculate the offset into the descriptor table for Array$(MidPoint)
  829. L1_2:
  830.  
  831.     Shl  DI,1           ;multiply MidPoint in DI times 4
  832.     Shl  DI,1           ;now DI holds how far beyond Array$(Start)
  833.                         ;  Array$(MidPoint)'s descriptor is
  834.     Add  DI,BX          ;add the array base address to produce the final
  835.                         ;  address for Array$(MidPoint)
  836.  
  837. ;----- calculate descriptor offset for Array$(I)
  838. L2:
  839.     Mov  SI,I           ;put I into SI
  840.     Shl  SI,1           ;as above
  841.     Shl  SI,1           ;now SI holds how far beyond Array$(Start)
  842.                         ;  Array$(I)'s descriptor is
  843.     Add  SI,BX          ;add the base to produce the final descriptor
  844.                         ;  address
  845.  
  846.     ;IF Array$(I) < Array$(MidPoint) THEN I = I + 1: GOTO L2
  847.     Push BX             ;save BX because B$SCMP trashes it
  848.     Push SI
  849.     Push DI
  850.     Call B$SCMP         ;do the compare
  851.     Pop  BX             ;restore BX
  852.  
  853. X1 Label Byte           ;modify the code below to "Jbe" if descending sort
  854.     Jae  L2_1           ;Array$(I) isn't less, continue on
  855.  
  856.     Inc  Word Ptr I     ;I = I + 1
  857.     Jmp  Short L2       ;GOTO L2
  858.  
  859. ;----- calculate descriptor offset for Array$(J)
  860. L2_1:
  861.     Mov  SI,J           ;put J into SI
  862.     Shl  SI,1           ;as above
  863.     Shl  SI,1           ;now SI holds how far beyond Array$(Start)
  864.                         ;  Array$(J)'s descriptor is
  865.     Add  SI,BX          ;add the base to produce the final descriptor
  866.                         ;  address
  867.  
  868.     ;IF Array$(J) > Array$(MidPoint) THEN J = J - 1: GOTO L2.1
  869.     Push BX             ;preserve BX
  870.     Push SI
  871.     Push DI
  872.     Call B$SCMP         ;do the compare
  873.     Pop  BX             ;restore BX
  874.  
  875. X2 Label Byte           ;modify the code below to "Jae" if descending sort
  876.     Jbe  L2_2           ;Array$(J) isn't greater, continue on
  877.  
  878.     Dec  Word Ptr J     ;J = J - 1
  879.     Jmp  Short L2_1     ;GOTO L2.1
  880.  
  881. L2_2:
  882.     Mov  AX,I           ;IF I > J GOTO L3
  883.     Cmp  AX,J
  884.     Jg   L3             ;J is greater, go directly to L3
  885.     Je   L2_3           ;they're the same, skip the swap
  886.  
  887.     ;Swap Array$(I), Array$(J)
  888.     Mov  SI,I           ;put I into SI
  889.     Mov  DI,J           ;put J into DI
  890.  
  891.     Cmp  SI,MidPoint    ;IF I = MidPoint THEN MidPoint = J
  892.     Jne  No_Mid1        ;not equal, skip ahead
  893.     Mov  MidPoint,DI    ;equal, assign MidPoint = J
  894.     Jmp  Short No_Mid2  ;don't waste time comparing again
  895.  
  896. No_Mid1:
  897.     Cmp  DI,MidPoint    ;IF J = MidPoint THEN MidPoint = I
  898.     Jne  No_Mid2        ;not equal, skip ahead
  899.     Mov  MidPoint,SI    ;equal, assign MidPoint = I
  900.  
  901. No_Mid2:
  902.     Mov  SI,I           ;put I into SI
  903.     Shl  SI,1           ;multiply times four for the
  904.     Shl  SI,1           ;  for the descriptors
  905.     Add  SI,BX          ;add address for first descriptor
  906.  
  907.     Mov  DI,J           ;do the same for J in DI
  908.     Shl  DI,1
  909.     Shl  DI,1
  910.     Add  DI,BX
  911.  
  912.     Push BX             ;save BX because B$SWSD destroys it
  913.     Call B$SWSD         ;and swap 'em good
  914.     Pop  BX
  915.  
  916. L2_3:
  917.     Inc  Word Ptr I     ;I = I + 1
  918.     Dec  Word Ptr J     ;J = J - 1
  919.  
  920.     Mov  AX,I           ;IF I <= J GOTO L2
  921.     Cmp  AX,J
  922.     Jg   L3             ;it's greater, skip to L3
  923.     Mov  DI,MidPoint    ;get MidPoint again
  924.     Jmp  L1_2           ;go back to just before L2
  925.  
  926. L3:
  927.     Mov  AX,I           ;IF I < L THEN PUSH I: PUSH L
  928.     Cmp  AX,L
  929.     Jnl  L3_1           ;it's not less, so skip Pushes
  930.  
  931.     Push I              ;Push I
  932.     Push L              ;Push L
  933.  
  934. L3_1:
  935.     Mov  AX,J           ;L = J
  936.     Mov  L,AX
  937.  
  938.     Mov  AX,F           ;IF F < L GOTO L1
  939.     Cmp  AX,L
  940.     Jnl  L3_2           ;it's not less, jump ahead to L3_2
  941.     Jmp  L1             ;it's less, go to L1
  942.  
  943. L3_2:
  944.     Cmp  S,SP           ;IF S = 0 GOTO L4
  945.     Je   L4
  946.  
  947.     Pop  L              ;Pop L
  948.     Pop  F              ;Pop F
  949.     Jmp  L1             ;GOTO L1
  950.  
  951. L4:
  952.     Ret                 ;return to BASIC
  953.  
  954. Sort Endp
  955. End
  956.  
  957. Besides being faster than the BASIC version, the assembly language Sort
  958. routine is half the size.  This version also supports sorting either
  959. forward or backward, but not just a portion of an array.  The general
  960. syntax is:
  961.  
  962.    CALL Sort(Array$(), Direction)
  963.  
  964. Where Array$() is any variable-length string array, and Direction is 0 for
  965. ascending, or any other value for descending.  Note that this routine calls
  966. upon BASIC's internal services to perform the actual comparing and
  967. swapping; therefore, the exact same code can be used with either QuickBASIC
  968. or BASIC PDS.  Again, I refer you forward to Chapter 12 for an explanation
  969. of the assembly language commands used in SORT.ASM.
  970.  
  971.  
  972. SORTING ON MULTIPLE KEYS
  973.  
  974. In many situations, sorting based on one key is sufficient.  For example,
  975. if you are sorting a mailing list to take advantage of bulk rates you must
  976. sort all of the addresses in order by zip code.  When considering complex
  977. data such as a TYPE variable, it is easy to sort the array based on one
  978. component of each element.  The earlier Bubble Sort example showed how MID$
  979. could be used to consider just a portion of each string, even though the
  980. entire elements were exchanged.  Had that routine been designed to operate
  981. on a TYPE array, the comparisons would have examined just one component,
  982. but the SWAP statements would exchange entire elements:
  983.  
  984.    IF Array(X).ZipCode > Array(X + 1).ZipCode THEN
  985.      SWAP Array(X), Array(X + 1)
  986.    END IF
  987.  
  988. This way, each customer's last name, first name, street address, and so
  989. forth remain connected to the zip codes that are being compared and
  990. exchanged.
  991.    There are several ways to sort on more than one key, and all are of
  992. necessity more complex than simply sorting based on a single key.  One
  993. example of a multi-key sort first puts all of the last names in order. 
  994. Then within each group of identical last names the first names are sorted,
  995. and within each group of identical last and first names further sorting is
  996. performed on yet another key--perhaps Balance Due.  As you can see, this
  997. requires you to sort based on differing types of data, and also to compare
  998. ranges of elements for the subgroups that need further sorting.
  999.    The biggest complication with this method is designing a calling syntax
  1000. that lets you specify all of the information.  A table array must be
  1001. established to hold the number of keys, the type of data in each key
  1002. (string, double precision, and so forth), and how many bytes into the TYPE
  1003. element each key portion begins.  Worse, you can't simply use the name of
  1004. a TYPE component in the comparisons inside the sort routine--which would
  1005. you use: Array(X).LastName, Array(X).FirstName, or Array(X).ZipCode? 
  1006. Therefore, a truly general multi-key sort must be called passing the
  1007. address where the array begins in memory, and a table of offsets beyond
  1008. that address where each component being considered is located.
  1009.    To avoid this added complexity I will instead show a different method
  1010. that has only a few minor restrictions, but is much easier to design and
  1011. understand.  This method requires you to position each TYPE component into
  1012. the key order you will sort on.  You will also need to store all numbers
  1013. that will be used for a sort key as ASCII digits.  To sort first on last
  1014. name, then first name, and then on balance due, the TYPE might be
  1015. structured as follows:
  1016.  
  1017.  
  1018. TYPE Customer
  1019.   LastName   AS STRING * 15
  1020.   FirstName  AS STRING * 15
  1021.   BalanceDue AS STRING * 9
  1022.   Street     AS STRING * 32
  1023.   City       AS STRING * 15
  1024.   State      AS STRING * 2
  1025.   ZipCode    AS STRING * 5
  1026.   AnyNumber  AS DOUBLE
  1027. END TYPE
  1028.  
  1029.  
  1030. In most cases the order in which each TYPE member is placed has no
  1031. consequence.  When you refer to TypeVar.LastName, BASIC doesn't care if
  1032. LastName is defined before or after FirstName in the TYPE structure. 
  1033. Either way it translates your reference to LastName into an address. 
  1034. Having to store numeric data as strings is a limitation, but this is needed
  1035. only for those TYPE fields that will be used as a sort key.
  1036.    The key to sorting on multiple items simultaneously is by treating the
  1037. contiguous fields as a single long field.  Since assignments to the string
  1038. portion of a TYPE variable are handled internally by BASIC's LSET routine,
  1039. the data in each element will be aligned such that subsequent fields can
  1040. be treated as an extension of the primary field.  Figure 8-2 below shows
  1041. five TYPE array elements in succession, as they would be viewed by a string
  1042. comparison routine.  This data is defined as a subset of the name and
  1043. address TYPE shown above, using just the first three fields.  Notice that
  1044. the balance due fields must be right-aligned (using RSET) for the numeric
  1045. values to be considered correctly.
  1046.  
  1047.  
  1048. Type.LastName  Type.FirstName Type.BalanceDue
  1049. ===============---------------=========
  1050. Munro          Jay              8000.00
  1051. Smith          John              122.03
  1052. Johnson        Alfred          14537.89
  1053. Rasmussen      Peter             100.90
  1054. Hudson         Cindy              21.22
  1055. ^              ^              ^
  1056. Field 1        Field 2        Field 3
  1057. starts here    starts here    starts here
  1058.  
  1059. Figure 8-2: Multiple contiguous fields in a TYPE can be treated as a single
  1060. long field.
  1061.  
  1062.  
  1063. Thus, the sort routine would be told to start at the first field, and
  1064. consider the strings to be 15 + 15 + 9 = 39 characters long.  This way all
  1065. three fields are compared at one time, and treated as a single entity. 
  1066. Additional fields can of course follow these, and they may be included in
  1067. the comparison or not at your option.
  1068.    The combination demonstration and subroutine below sorts such a TYPE
  1069. array on any number of keys using this method, and it has a few additional
  1070. features as well.  Besides letting you confine the sorting to just a
  1071. portion of the array, you may also specify how far into each element the
  1072. first key is located.  As long as the key fields are contiguous, they do
  1073. not have to begin at the start of each TYPE.  Therefore, you could sort
  1074. just on the first name field, or on any other field or group of fields.
  1075.  
  1076. 'TYPESORT.BAS - performs a multi-key sort on TYPE arrays
  1077.  
  1078. 'Copyright (c) 1991 Ethan Winer
  1079.  
  1080. DEFINT A-Z
  1081. DECLARE FUNCTION Compare3% (BYVAL Seg1, BYVAL Adr1, BYVAL Seg2, _
  1082.   BYVAL Adr2, NumBytes)
  1083. DECLARE SUB SwapMem (BYVAL Seg1, BYVAL Adr1, BYVAL Seg2, BYVAL Adr2, _
  1084.   BYVAL Length)
  1085. DECLARE SUB TypeSort (Segment, Address, ElSize, Offset, KeySize, NumEls)
  1086.  
  1087. CONST NumEls% = 23              'this keeps it all on the screen
  1088.  
  1089. TYPE MyType
  1090.   LastName  AS STRING * 10
  1091.   FirstName AS STRING * 10
  1092.   Dollars   AS STRING * 6
  1093.   Cents     AS STRING * 2
  1094. END TYPE
  1095. REDIM Array(1 TO NumEls%) AS MyType
  1096.  
  1097. '---- Disable (REM out) all but one of the following blocks to test
  1098.  
  1099. Offset = 27                 'start sorting with Cents
  1100. ElSize = LEN(Array(1))      'the length of each element
  1101. KeySize = 2                 'sort on the Cents only
  1102.  
  1103. Offset = 21                 'start sorting with Dollars
  1104. ElSize = LEN(Array(1))      'the length of each element
  1105. KeySize = 8                 'sort Dollars and Cents only
  1106.  
  1107. Offset = 11                 'start sorting with FirstName
  1108. ElSize = LEN(Array(1))      'the length of each element
  1109. KeySize = 18                'sort FirstName through Cents
  1110.  
  1111. Offset = 1                  'start sorting with LastName
  1112. ElSize = LEN(Array(1))      'the length of each element
  1113. KeySize = ElSize            'sort based on all 4 fields
  1114.  
  1115. FOR X = 1 TO NumEls%        'build the array from DATA
  1116.   READ Array(X).LastName
  1117.   READ Array(X).FirstName
  1118.   READ Amount$              'format the amount into money
  1119.   Dot = INSTR(Amount$, ".")
  1120.   IF Dot THEN
  1121.     RSET Array(X).Dollars = LEFT$(Amount$, Dot - 1)
  1122.     Array(X).Cents = LEFT$(MID$(Amount$, Dot + 1) + "00", 2)
  1123.   ELSE
  1124.     RSET Array(X).Dollars = Amount$
  1125.     Array(X).Cents = "00"
  1126.   END IF
  1127. NEXT
  1128.  
  1129. Segment = VARSEG(Array(1))      'show where the array is
  1130. Address = VARPTR(Array(1))      '  located in memory
  1131. CALL TypeSort(Segment, Address, ElSize, Offset, KeySize, NumEls%)
  1132.  
  1133. CLS                             'display the results
  1134. FOR X = 1 TO NumEls%
  1135.   PRINT Array(X).LastName, Array(X).FirstName,
  1136.   PRINT Array(X).Dollars; "."; Array(X).Cents
  1137. NEXT
  1138.  
  1139. DATA Smith, John, 123.45
  1140. DATA Cramer, Phil, 11.51
  1141. DATA Hogan, Edward, 296.08
  1142. DATA Cramer, Phil, 112.01
  1143. DATA Malin, Donald, 13.45
  1144. DATA Cramer, Phil, 111.3
  1145. DATA Smith, Ralph, 123.22
  1146. DATA Smith, John, 112.01
  1147. DATA Hogan, Edward, 8999.04
  1148. DATA Hogan, Edward, 8999.05
  1149. DATA Smith, Bob, 123.45
  1150. DATA Cramer, Phil, 11.50
  1151. DATA Hogan, Edward, 296.88
  1152. DATA Malin, Donald, 13.01
  1153. DATA Cramer, Phil, 111.1
  1154. DATA Smith, Ralph, 123.07
  1155. DATA Smith, John, 112.01
  1156. DATA Hogan, Edward, 8999.33
  1157. DATA Hogan, Edward, 8999.17
  1158. DATA Hogan, Edward, 8999.24
  1159. DATA Smith, John, 123.05
  1160. DATA Cramer, David, 1908.80
  1161. DATA Cramer, Phil, 112
  1162. END
  1163.  
  1164. SUB TypeSort (Segment, Address, ElSize, Displace, KeySize, NumEls) STATIC
  1165.  
  1166. REDIM QStack(NumEls \ 5 + 10) 'create a stack array
  1167.  
  1168. First = 1                  'initialize working variables
  1169. Last = NumEls
  1170. Offset = Displace - 1      'decrement once now rather than
  1171.                            '  repeatedly later
  1172. DO
  1173.   DO
  1174.     Temp = (Last + First) \ 2   'seek midpoint
  1175.     I = First
  1176.     J = Last
  1177.  
  1178.     DO
  1179.       WHILE Compare3%(Segment, Address + Offset + (I - 1) * ElSize, Segment, _
  1180.         Address + Offset + (Temp-1) * ElSize, KeySize) = -1 '< 1 for descending
  1181.         I = I + 1
  1182.       WEND
  1183.       WHILE Compare3%(Segment, Address + Offset + (J - 1) * ElSize, Segment, _
  1184.         Address + Offset + (Temp-1)  * ElSize, KeySize) = 1 '< -1 for descending
  1185.         J = J - 1
  1186.       WEND
  1187.       IF I > J THEN EXIT DO
  1188.       IF I < J THEN
  1189.         CALL SwapMem(Segment, Address + (I - 1) * ElSize, Segment, _
  1190.           Address + (J - 1) * ElSize, ElSize)
  1191.         IF Temp = I THEN
  1192.           Temp = J
  1193.         ELSEIF Temp = J THEN
  1194.           Temp = I
  1195.         END IF
  1196.       END IF
  1197.       I = I + 1
  1198.       J = J - 1
  1199.     LOOP WHILE I <= J
  1200.  
  1201.     IF I < Last THEN
  1202.       QStack(StackPtr) = I              'Push I
  1203.       QStack(StackPtr + 1) = Last       'Push Last
  1204.       StackPtr = StackPtr + 2
  1205.     END IF
  1206.  
  1207.     Last = J
  1208.   LOOP WHILE First < Last
  1209.  
  1210.   IF StackPtr = 0 THEN EXIT DO          'Done
  1211.   StackPtr = StackPtr - 2
  1212.   First = QStack(StackPtr)              'Pop First
  1213.   Last = QStack(StackPtr + 1)           'Pop Last
  1214. LOOP
  1215.  
  1216. ERASE QStack                    'delete the stack array
  1217.  
  1218. END SUB
  1219.  
  1220. As you can see, this version of the Quick Sort subprogram is derived from
  1221. the one shown earlier.  The important difference is that all of the
  1222. incoming information is passed as segments, addresses, and bytes, rather
  1223. than using an explicit array name.  But before describing the inner details
  1224. of the subprogram itself, I'll address the demonstration portion and show
  1225. how the routine is set up and called.
  1226.    As with some of the other procedures on the disk that comes with this
  1227. book, you will extract the TypeSort subprogram and add it to your own
  1228. programs by loading it as a module, and then using the Move option of
  1229. BASIC's View Subs menu.  You can quickly access this menu by pressing F2,
  1230. and then use Alt-M to select Move.  Once this is done you will unload
  1231. TYPESORT.BAS using the Alt-F-U menu selection, and answer *No* when asked
  1232. if you want to save the modified file.  You could also copy the TypeSort
  1233. subprogram into a separate file, and then load that file as a module in
  1234. each program that needs it.
  1235.    Although the example TYPE definition here shows only four components,
  1236. you may of course use any TYPE structure.  TypeSort expects six parameters
  1237. to tell it where in memory the array is located, how far into each element
  1238. the comparison routines are to begin, the total length of each element, the
  1239. length of the key fields, and the number of elements to sort.
  1240.    After defining MyType, the setup portion of TYPESORT.BAS establishes the
  1241. offset, element size, and key size parameters.  As you can see, four
  1242. different sample setups are provided, and you should add remarking
  1243. apostrophes to all but one of them.  If the program is left as is, the last
  1244. setup values will take precedence.
  1245.    The next section reads sample names, addresses and dollar amounts from
  1246. DATA statements, and formats the dollar amounts as described earlier.  The
  1247. dollar portion of the amounts are right justified into the Dollars field
  1248. of each element, and the Cents portion is padded with trailing zeros as
  1249. necessary to provide a dollars and cents format.  This way, the value 12.3
  1250. will be assigned as 12.30, and 123 will be formatted to 123.00 which gives
  1251. the expected appearance.
  1252.    The final setup step is to determine where the array begins in memory. 
  1253. Since you specify the starting segment and address, it is simple to begin
  1254. sorting at any array element.  For example, to sort elements 100 through
  1255. 200--even if the array is larger than that--you'd use VARSEG(Array(100))
  1256. and VARPTR(Array(100) instead of element 1 as shown in this example.
  1257.    In addition to the starting segment and address of the array, TypeSort
  1258. also requires you to tell it how many elements to consider.  If you are
  1259. sorting the entire array and the array starts with element 1, this will
  1260. simply be the UBOUND of the array.  If you are sorting just a portion of
  1261. the array then you give it only the number of elements to be sorted.  So
  1262. to sort elements 100 through 200, the number of elements will be 101.  A
  1263. general formula you can use for calculating this based on element numbers
  1264. is NumElements = LastElement - FirstElement + 1.
  1265.    Now let's consider the TypeSort subprogram itself.  Since it is more
  1266. like the earlier QSort program than different, I will cover only the
  1267. differences here.  In fact, the primary difference is in the way
  1268. comparisons and exchanges are handled.  The Compare3 function introduced
  1269. earlier is used to compare the array elements with the midpoint.  Although
  1270. QSort made a temporary copy of the midpoint element, that would be
  1271. difficult to do here.  Since the routine is designed to work with any type
  1272. of data--and the size of each element can vary depending on the TYPE
  1273. structure--it is impractical to make a copy.
  1274.    While SPACE$ could be used to claim a block of memory into which the
  1275. midpoint element is copied, there's a much better way:  the Temp variable
  1276. is used to remember the element number itself.  The only complication is
  1277. that once elements I and J are swapped, Temp must be reassigned if it was
  1278. equal to either of them.  (This happens just below the call to SwapMem.) 
  1279. But the simple integer IF test and assignment required adds far less code
  1280. and is much faster than making a copy of the element.
  1281.    TypeSort is designed to sort the array in ascending order, and comments
  1282. in the code show how to change it to sort descending instead.  If you
  1283. prefer to have one subprogram that can do both, you should add an extra
  1284. parameter, perhaps called Direction.  Near the beginning of the routine
  1285. before the initial outer DO you would add this:
  1286.  
  1287.    IF Direction = 0 THEN     'sort ascending
  1288.      ICompare = -1
  1289.      JCompare = 1
  1290.    ELSE                      'sort descending
  1291.      ICompare = 1
  1292.      JCompare = -1
  1293.    END IF
  1294.  
  1295. Then, where the results from Compare3 are compared to -1 and 1 replace
  1296. those comparisons (at the end of each WHILE line) to instead use ICompare
  1297. and JCompare:
  1298.  
  1299.    WHILE Compare3%(...) = ICompare
  1300.      I = I + 1
  1301.    WEND
  1302.    WHILE Compare3%(...) = JCompare
  1303.      J = J - 1
  1304.    WEND
  1305.  
  1306. This way, you are using variables to establish the sorting direction, and
  1307. those variables can be set either way each time TypeSort is called.
  1308.    The last major difference is that elements are exchanged using the
  1309. SwapMem routine rather than BASIC's SWAP statement.  While it is possible
  1310. to call SWAP by aliasing its name as shown in Chapter 5, it was frankly
  1311. simpler to write a new routine for this purpose.  Further, BASIC's SWAP is
  1312. slower than SwapMem because it must be able to handle variables of
  1313. different lengths, and also exchange fixed-length and conventional strings. 
  1314. SwapMem is extremely simple, and it works very quickly.
  1315.    As I stated earlier, the only way to write a truly generic sort routine
  1316. is by passing segments and addresses and bytes, instead of array names. 
  1317. Although it would be great if BASIC could let you declare a subprogram or
  1318. function using the AS ANY option to allow any type of data, that simply
  1319. wouldn't work.  As BASIC compiles your program, it needs to know the size
  1320. and type of each parameter.  When you reference TypeVar.LastName, BASIC
  1321. knows where within TypeVar the LastName portion begins, and uses that in
  1322. its address calculations.  It is not possible to avoid this limitation
  1323. other than by using addresses as is done here.
  1324.    Indeed, this is the stuff that C and assembly language programs are made
  1325. of.  In these languages--especially assembly language--integer pointer
  1326. variables are used extensively to show where data is located and how long
  1327. it is.  However, the formulas used within the Compare3 and SwapMem function
  1328. calls are not at all difficult to understand.
  1329.    The formula Address + Offset - (I - 1) * ElSize indicates where the key
  1330. field of element I begins.  Address holds the address of the beginning of
  1331. the first element, and Offset is added to identify the start of the first
  1332. sort key.  (I - 1) is used instead of I because addresses are always zero-
  1333. based.  That is, the first element in the array from TypeSort's perspective
  1334. is element 0, even though the calling program considers it to be element
  1335. 1.  Finally, the element number is multiplied times the length of each
  1336. element, to determine the value that must be added to the starting address
  1337. and offset to obtain the final address for the data in element I.  Please
  1338. understand that calculations such as these are what the compiler must do
  1339. each time you access an array element.
  1340.    Note that if you call TypeSort incorrectly or give it illegal element
  1341. numbers, you will not receive a "Subscript out of range" error from BASIC. 
  1342. Rather, you will surely crash your PC and have to reboot.  This is the
  1343. danger--and fun--of manipulating pointers directly.
  1344.    As I stated earlier, the SwapMem routine that does the actual exchanging
  1345. of elements is very simple, and it merely takes a byte from one element and
  1346. exchanges it with the corresponding byte in the other.  This task is
  1347. greatly simplified by the use of the XCHG assembly language command, which
  1348. is similar to BASIC's SWAP statement.  Although XCHG cannot swap a word in
  1349. memory with another word in memory, it can exchange memory with a register. 
  1350. SwapMem is shown in the listing below.
  1351.  
  1352. ;SWAPMEM.ASM, swaps two sections of memory
  1353.  
  1354. .Model Medium, Basic
  1355. .Code
  1356.  
  1357. SwapMem Proc Uses SI DI DS ES, Var1:DWord, Var2:DWord, NumBytes:Word
  1358.  
  1359.     Lds  SI,Var1      ;get the segmented address of the
  1360.                       ;  first variable
  1361.     Les  DI,Var2      ;and for the second variable
  1362.     Mov  CX,NumBytes  ;get the number of bytes to exchange
  1363.     Jcxz Exit         ;we can't swap zero bytes!
  1364.  
  1365. DoSwap:
  1366.     Mov  AL,ES:[DI]   ;get a byte from the second variable
  1367.     Xchg AL,[SI]      ;swap it with the first variable
  1368.     Stosb             ;complete the swap and increment DI
  1369.     Inc  SI           ;point to the next source byte
  1370.     Loop DoSwap       ;continue until done
  1371.  
  1372. Exit:
  1373.     Ret               ;return to BASIC
  1374.  
  1375. SwapMem Endp
  1376. End
  1377.  
  1378. INDEXED SORTING ON MULTIPLE KEYS
  1379.  
  1380. Earlier I showed how to modify the simple Bubble Sort routine to sort a
  1381. parallel index array instead of the primary array.  One important reason
  1382. you might want to do that is to allow access to the primary array in both
  1383. its original and sorted order.  Another reason, and one we will get to
  1384. shortly, is to facilitate sorting disk files.  Although a routine to sort
  1385. the records in a file could swap the actual data, it takes a long time to
  1386. read and write that much data on disk.  Further, each time you wanted to
  1387. access the data sorted on a different key, the entire file would need to
  1388. be sorted again.
  1389.    A much better solution is to create one or more sorted lists of record
  1390. numbers, and store those on disk each in a separate file.  This lets you
  1391. access the data sorted by name, or by zip code, or by any other field,
  1392. without ever changing the actual file.  The TypeISort subprogram below is
  1393. adapted from TypeSort, and it sorts an index array that holds the element
  1394. numbers of a TYPE array.
  1395.  
  1396. 'TYPISORT.BAS, indexed multi-key sort for TYPE arrays
  1397.  
  1398. DEFINT A-Z
  1399.  
  1400. DECLARE FUNCTION Compare3% (BYVAL Seg1, BYVAL Adr1, BYVAL Seg2, _
  1401.   BYVAL Adr2, NumBytes)
  1402. DECLARE SUB SwapMem (BYVAL Seg1, BYVAL Adr1, BYVAL Seg2, _
  1403.   BYVAL Adr2, BYVAL Length)
  1404. DECLARE SUB TypeISort (Segment, Address, ElSize, Offset, KeySize, _
  1405.   NumEls, Index())
  1406.  
  1407. CONST NumEls% = 23              'this keeps it all on the screen
  1408.  
  1409. TYPE MyType
  1410.   LastName  AS STRING * 10
  1411.   FirstName AS STRING * 10
  1412.   Dollars   AS STRING * 6
  1413.   Cents     AS STRING * 2
  1414. END TYPE
  1415. REDIM Array(1 TO NumEls%) AS MyType
  1416. REDIM Index(1 TO NumEls%)   'create the index array
  1417.  
  1418. Offset = 1                  'start sorting with LastName
  1419. ElSize = LEN(Array(1))      'the length of each element
  1420. KeySize = ElSize            'sort based on all 4 fields
  1421.  
  1422. FOR X = 1 TO NumEls%        'build the array from DATA
  1423.   READ Array(X).LastName
  1424.   READ Array(X).FirstName
  1425.   READ Amount$            
  1426.    ...                      'this continues as already
  1427.    ...                      '  shown in TypeSort
  1428. NEXT
  1429.  
  1430. FOR X = 1 TO NumEls%            'initialize the index
  1431.   Index(X) = X - 1              'but starting with 0
  1432. NEXT
  1433.  
  1434. Segment = VARSEG(Array(1))      'show where the array is
  1435. Address = VARPTR(Array(1))      '  located in memory
  1436. CALL TypeISort(Segment, Address, ElSize, Offset, KeySize, NumEls%, Index())
  1437.  
  1438. CLS                             'display the results
  1439. FOR X = 1 TO NumEls%            '+ 1 adjusts to one-based
  1440.   PRINT Array(Index(X) + 1).LastName,
  1441.   PRINT Array(Index(X) + 1).FirstName,
  1442.   PRINT Array(Index(X) + 1).Dollars; ".";
  1443.   PRINT Array(Index(X) + 1).Cents
  1444. NEXT
  1445.  
  1446. DATA Smith, John, 123.45        'this continues as already
  1447.   ...                           '  shown in TypeSort
  1448.   ...
  1449.  
  1450. END
  1451.  
  1452. SUB TypeISort (Segment, Address, ElSize, Displace, KeySize, NumEls, _
  1453.   Index()) STATIC
  1454.  
  1455. REDIM QStack(NumEls \ 5 + 10) 'create a stack
  1456.  
  1457. First = 1                     'initialize working variables
  1458. Last = NumEls
  1459. Offset = Displace - 1         'make zero-based now for speed later
  1460.  
  1461. DO
  1462.   DO
  1463.     Temp = (Last + First) \ 2 'seek midpoint
  1464.     I = First
  1465.     J = Last
  1466.  
  1467.     DO  'change -1 to 1 and 1 to -1 to sort descending
  1468.       WHILE Compare3%(Segment, Address + Offset + (Index(I) * ElSize), _
  1469.         Segment, Address + Offset + (Index(Temp) * ElSize), KeySize) = -1
  1470.         I = I + 1
  1471.       WEND
  1472.       WHILE Compare3%(Segment, Address + Offset + (Index(J) * ElSize), _
  1473.         Segment, Address + Offset + (Index(Temp) * ElSize), KeySize) = 1
  1474.         J = J - 1
  1475.       WEND
  1476.       IF I > J THEN EXIT DO
  1477.       IF I < J THEN
  1478.         SWAP Index(I), Index(J)
  1479.         IF Temp = I THEN
  1480.           Temp = J
  1481.         ELSEIF Temp = J THEN
  1482.           Temp = I
  1483.         END IF
  1484.       END IF
  1485.       I = I + 1
  1486.       J = J - 1
  1487.     LOOP WHILE I <= J
  1488.  
  1489.     IF I < Last THEN
  1490.       QStack(StackPtr) = I              'Push I
  1491.       QStack(StackPtr + 1) = Last       'Push Last
  1492.       StackPtr = StackPtr + 2
  1493.     END IF
  1494.  
  1495.     Last = J
  1496.   LOOP WHILE First < Last
  1497.  
  1498.   IF StackPtr = 0 THEN EXIT DO          'Done
  1499.   StackPtr = StackPtr - 2
  1500.   First = QStack(StackPtr)              'Pop First
  1501.   Last = QStack(StackPtr + 1)           'Pop Last
  1502. LOOP
  1503.  
  1504. ERASE QStack                    'delete the stack array
  1505.  
  1506. END SUB
  1507.  
  1508. As with TypeSort, TypeISort is entirely pointer based so it can be used
  1509. with any type of data and it can sort multiple contiguous keys.  The only
  1510. real difference is the addition of the Index() array parameter, and the
  1511. extra level of indirection needed to access the index array each time a
  1512. comparison is made.  Also, when a swap is required, only the integer index
  1513. elements are exchanged, which simplifies the code and reduces its size. 
  1514. Like TypeSort, you can change the sort direction by reversing the -1 and
  1515. 1 values used with Compare3, or add a Direction parameter to the list and
  1516. modify the code to use that.
  1517.    As with BubbleISort, the index array is initialized to increasing values
  1518. by the calling program; however, here the first element is set to hold a
  1519. value of 0 instead of 1.  This reduces the calculations needed within the
  1520. routine each time an address must be obtained.  Therefore, when TypeISort
  1521. returns, the caller must add 1 to the element number held in each index
  1522. element.  This is shown within the FOR/NEXT loop that displays the sorted
  1523. results.
  1524.  
  1525.  
  1526. SORTING FILES
  1527.  
  1528. With the development of TypeISort complete, we can now use that routine
  1529. to sort disk files.  The sorting strategy will be to determine how many
  1530. records are in the file, to determine how many separate passes are needed
  1531. to process the entire file.  TypeISort and TypeSort are restricted to
  1532. working with arrays no larger than 64K (32K in the editing environment),
  1533. so there is a limit as to how much data may be loaded into memory at one
  1534. time.  These sort routines can accommodate more data when compiled because
  1535. address calculations that result in values larger than 32767 cause an
  1536. overflow error in the QB editor.  This overflow is in fact harmless, and
  1537. is ignored in a compiled program unless you use the /d switch.
  1538.    Although the routines could be modified to perform segment and address
  1539. arithmetic to accommodate larger arrays, that still wouldn't solve the
  1540. problem of having more records than can fit in memory at once.  Therefore,
  1541. separate passes must be used to sort the file contents in sections, with
  1542. each pass writing a temporary index file to disk.  A final merge pass then
  1543. reads each index to determine which pieces fits where, and then writes the
  1544. final index file.  The program FILESORT.BAS below incorporates all of the
  1545. sorting techniques shown so far, and includes a few custom BASIC routines
  1546. to improve its performance.
  1547.  
  1548. 'FILESORT.BAS, indexed multi-key random access file sort
  1549.  
  1550. DEFINT A-Z
  1551.  
  1552. DECLARE FUNCTION Compare3% (BYVAL Seg1, BYVAL Adr1, BYVAL Seg2, _
  1553.   BYVAL Adr2, NumBytes)
  1554. DECLARE FUNCTION Exist% (FileSpec$)
  1555. DECLARE SUB DOSInt (Registers AS ANY)
  1556. DECLARE SUB FileSort (FileName$, NDXName$, RecLength, Offset, KeySize)
  1557. DECLARE SUB LoadFile (FileNum, Segment, Address, Bytes&)
  1558. DECLARE SUB SaveFile (FileNum, Segment, Address, Bytes&)
  1559. DECLARE SUB SwapMem (BYVAL Seg1, BYVAL Adr1, BYVAL Seg2, BYVAL Adr2, _
  1560.   BYVAL Length)
  1561. DECLARE SUB TypeISort (Segment, Address, ElSize, Offset, KeySize, _
  1562.   NumEls, Index())
  1563.  
  1564. RANDOMIZE TIMER                 'create new data each run
  1565. DEF FnRand% = INT(RND * 10 + 1) 'returns RND from 1 to 10
  1566.  
  1567. TYPE RegType                    'used by DOSInt
  1568.   AX AS INTEGER
  1569.   BX AS INTEGER
  1570.   CX AS INTEGER
  1571.   DX AS INTEGER
  1572.   BP AS INTEGER
  1573.   SI AS INTEGER
  1574.   DI AS INTEGER
  1575.   FL AS INTEGER
  1576.   DS AS INTEGER
  1577.   ES AS INTEGER
  1578. END TYPE
  1579.  
  1580. DIM SHARED Registers AS RegType 'share among all subs
  1581. REDIM LastNames$(1 TO 10)       'we'll select names at
  1582. REDIM FirstNames$(1 TO 10)      '  random from these
  1583.  
  1584. NumRecords = 2988               'how many test records to use
  1585. FileName$ = "TEST.DAT"          'really original, eh?
  1586. NDXName$ = "TEST.NDX"           'this is the index file name
  1587.  
  1588. TYPE RecType
  1589.   LastName  AS STRING * 11
  1590.   FirstName AS STRING * 10
  1591.   Dollars   AS STRING * 6
  1592.   Cents     AS STRING * 2
  1593.   AnyNumber AS LONG         'this shows that only key
  1594.   OtherNum  AS LONG         '  information must be ASCII
  1595. END TYPE
  1596.  
  1597. FOR X = 1 TO 10             'read the possible last names
  1598.   READ LastNames$(X)
  1599. NEXT
  1600.  
  1601. FOR X = 1 TO 10             'and the possible first names
  1602.   READ FirstNames$(X)
  1603. NEXT
  1604.  
  1605. DIM RecordVar AS RecType    'to create the sample file
  1606. RecLength = LEN(RecordVar)  'the length of each record
  1607. CLS
  1608. PRINT "Creating a test file..."
  1609.  
  1610. IF Exist%(FileName$) THEN   'if there's an existing file
  1611.   KILL FileName$            'kill the old data from prior
  1612. END IF                      '  runs to start fresh
  1613.  
  1614. IF Exist%(NDXName$) THEN    'same for any old index file
  1615.   KILL NDXName$
  1616. END IF
  1617.  
  1618.  
  1619. '---- Create some test data and write it to the file
  1620. OPEN FileName$ FOR RANDOM AS #1 LEN = RecLength
  1621.   FOR X = 1 TO NumRecords
  1622.     RecordVar.LastName = LastNames$(FnRand%)
  1623.     RecordVar.FirstName = FirstNames$(FnRand%)
  1624.     Amount$ = STR$(RND * 10000)
  1625.     Dot = INSTR(Amount$, ".")
  1626.     IF Dot THEN
  1627.       RSET RecordVar.Dollars = LEFT$(Amount$, Dot - 1)
  1628.       RecordVar.Cents = LEFT$(MID$(Amount$, Dot + 1) + "00", 2)
  1629.     ELSE
  1630.       RSET RecordVar.Dollars = Amount$
  1631.       RecordVar.Cents = "00"
  1632.     END IF
  1633.     RecordVar.AnyNumber = X
  1634.     PUT #1, , RecordVar
  1635.   NEXT
  1636. CLOSE
  1637.  
  1638. '----- Created a sorted index based on the main data file
  1639. Offset = 1                  'start sorting with LastName
  1640. KeySize = 29                'sort based on first 4 fields
  1641. PRINT "Sorting..."
  1642. CALL FileSort(FileName$, NDXName$, RecLength, Offset, KeySize)
  1643.  
  1644.  
  1645. '----- Display the results
  1646. CLS
  1647. VIEW PRINT 1 TO 24
  1648. LOCATE 25, 1
  1649. COLOR 15
  1650. PRINT "Press any key to pause/resume";
  1651. COLOR 7
  1652. LOCATE 1, 1
  1653.  
  1654. OPEN FileName$ FOR RANDOM AS #1 LEN = RecLength
  1655. OPEN NDXName$ FOR BINARY AS #2
  1656.   FOR X = 1 TO NumRecords
  1657.     GET #2, , ThisRecord            'get next rec. number
  1658.     GET #1, ThisRecord, RecordVar   'then the actual data
  1659.  
  1660.     PRINT RecordVar.LastName;       'print each field
  1661.     PRINT RecordVar.FirstName;
  1662.     PRINT RecordVar.Dollars; ".";
  1663.     PRINT RecordVar.Cents
  1664.  
  1665.     IF LEN(INKEY$) THEN             'pause on a keypress
  1666.       WHILE LEN(INKEY$) = 0: WEND
  1667.     END IF
  1668.   NEXT
  1669. CLOSE
  1670.  
  1671. VIEW PRINT 1 TO 24                  'restore the screen
  1672. END
  1673.  
  1674. DATA Smith, Cramer, Malin, Munro, Passarelli
  1675. DATA Bly, Osborn, Pagliaro, Garcia, Winer
  1676.  
  1677. DATA John, Phil, Paul, Anne, Jacki
  1678. DATA Patricia, Ethan, Donald, Tami, Elli
  1679. END
  1680.  
  1681.  
  1682. FUNCTION Exist% (Spec$) STATIC  'reports if a file exists
  1683.  
  1684. DIM DTA AS STRING * 44          'the work area for DOS
  1685. DIM LocalSpec AS STRING * 60    'guarantee the spec is in
  1686. LocalSpec$ = Spec$ + CHR$(0)    '  DGROUP for BASIC PDS
  1687.  
  1688. Exist% = -1                     'assume true for now
  1689.  
  1690. Registers.AX = &H1A00           'assign DTA service
  1691. Registers.DX = VARPTR(DTA)      'show DOS where to place it
  1692. Registers.DS = VARSEG(DTA)
  1693. CALL DOSInt(Registers)
  1694.  
  1695. Registers.AX = &H4E00           'find first matching file
  1696. Registers.CX = 39               'any file attribute okay
  1697. Registers.DX = VARPTR(LocalSpec)
  1698. Registers.DS = VARSEG(LocalSpec)
  1699. CALL DOSInt(Registers)          'see if there's a match
  1700.  
  1701. IF Registers.FL AND 1 THEN      'if the Carry flag is set
  1702.   Exist% = 0                    '  there were no matches
  1703. END IF
  1704.  
  1705. END FUNCTION
  1706.  
  1707.  
  1708. SUB FileSort (FileName$, NDXName$, RecLength, Displace, KeySize) STATIC
  1709.  
  1710. CONST BufSize% = 32767  'holds the data being sorted
  1711. Offset = Displace - 1   'make zero-based for speed later
  1712.  
  1713. '----- Open the main data file
  1714. FileNum = FREEFILE
  1715. OPEN FileName$ FOR BINARY AS #FileNum
  1716.  
  1717. '----- Calculate the important values we'll need
  1718. NumRecords = LOF(FileNum) \ RecLength
  1719. RecsPerPass = BufSize% \ RecLength
  1720. IF RecsPerPass > NumRecords THEN RecsPerPass = NumRecords
  1721.  
  1722. NumPasses = (NumRecords \ RecsPerPass) - ((NumRecords MOD RecsPerPass) _
  1723.   <> 0)
  1724. IF NumPasses = 1 THEN
  1725.   RecsLastPass = RecsPerPass
  1726. ELSE
  1727.   RecsLastPass = NumRecords MOD RecsPerPass
  1728. END IF
  1729.  
  1730. '----- Create the buffer and index sorting arrays
  1731. REDIM Buffer(1 TO 1) AS STRING * BufSize
  1732. REDIM Index(1 TO RecsPerPass)
  1733. IndexAdjust = 1
  1734.  
  1735.  
  1736. '----- Process all of the records in manageable groups
  1737. FOR X = 1 TO NumPasses
  1738.  
  1739.   IF X < NumPasses THEN         'if not the last pass
  1740.     RecsThisPass = RecsPerPass  'do the full complement
  1741.   ELSE                          'the last pass may have
  1742.     RecsThisPass = RecsLastPass '  fewer records to do
  1743.   END IF
  1744.  
  1745.   FOR Y = 1 TO RecsThisPass     'initialize the index
  1746.     Index(Y) = Y - 1            'starting with value of 0
  1747.   NEXT
  1748.  
  1749.   '----- Load a portion of the main data file
  1750.   Segment = VARSEG(Buffer(1))   'show where the buffer is
  1751.   CALL LoadFile(FileNum, Segment, Zero, RecsThisPass * CLNG(RecLength))
  1752.   CALL TypeISort(Segment, Zero, RecLength, Displace, KeySize, _
  1753.     RecsThisPass, Index())
  1754.  
  1755.   '----- Adjust the zero-based index to record numbers
  1756.   FOR Y = 1 TO RecsThisPass
  1757.     Index(Y) = Index(Y) + IndexAdjust
  1758.   NEXT
  1759.  
  1760.   '----- Save the index file for this pass
  1761.   TempNum = FREEFILE
  1762.   OPEN "$$PASS." + LTRIM$(STR$(X)) FOR OUTPUT AS #TempNum
  1763.   CALL SaveFile(TempNum, VARSEG(Index(1)), Zero, RecsThisPass * 2&)
  1764.   CLOSE #TempNum
  1765.  
  1766.   '----- The next group of record numbers start this much higher
  1767.   IndexAdjust = IndexAdjust + RecsThisPass
  1768.  
  1769. NEXT
  1770.  
  1771. ERASE Buffer, Index             'free up the memory
  1772.  
  1773.  
  1774. '----- Do a final merge pass if necessary
  1775. IF NumPasses > 1 THEN
  1776.  
  1777.   NDXNumber = FREEFILE
  1778.   OPEN NDXName$ FOR BINARY AS #NDXNumber
  1779.   REDIM FileNums(NumPasses)        'this holds the file numbers
  1780.   REDIM RecordNums(NumPasses)      'this holds record numbers
  1781.  
  1782.   REDIM MainRec$(1 TO NumPasses)   'holds main record data
  1783.   REDIM Remaining(1 TO NumPasses)  'tracks index files
  1784.  
  1785.   '----- Open the files and seed the first round of data
  1786.   FOR X = 1 TO NumPasses
  1787.     FileNums(X) = FREEFILE
  1788.     OPEN "$$PASS." + LTRIM$(STR$(X)) FOR BINARY AS #FileNums(X)
  1789.     Remaining(X) = LOF(FileNums(X))   'this is what remains
  1790.     MainRec$(X) = SPACE$(RecLength)   'holds main data file
  1791.  
  1792.     GET #FileNums(X), , RecordNums(X)     'get the next record number
  1793.     RecOffset& = (RecordNums(X) - 1) * CLNG(RecLength) + 1
  1794.     GET #FileNum, RecOffset&, MainRec$(X) 'then get the data
  1795.   NEXT
  1796.  
  1797.   FOR X = 1 TO NumRecords
  1798.  
  1799.     Lowest = 1               'assume this is the lowest data in the group
  1800.     WHILE Remaining(Lowest) = 0 'Lowest can't refer to a dead index
  1801.       Lowest = Lowest + 1       'so seek to the next higher active index
  1802.     WEND
  1803.  
  1804.     FOR Y = 2 TO NumPasses      'now seek out the truly lowest element
  1805.       IF Remaining(Y) THEN      'consider only active indexes
  1806.         IF Compare3%(SSEG(MainRec$(Y)), _    '<-- use VARSEG with QB
  1807.           SADD(MainRec$(Y)) + Offset,   _
  1808.           SSEG(MainRec$(Lowest)),       _    '<-- use VARSEG with QB
  1809.           SADD(MainRec$(Lowest)) + Offset, KeySize) = -1 THEN
  1810.            Lowest = Y
  1811.         END IF
  1812.       END IF
  1813.     NEXT
  1814.  
  1815.     PUT #NDXNumber, , RecordNums(Lowest)     'write the main index
  1816.     Remaining(Lowest) = Remaining(Lowest) - 2
  1817.     IF Remaining(Lowest) THEN                'if the index is still active
  1818.       GET #FileNums(Lowest), , RecordNums(Lowest)
  1819.       RecOffset& = (RecordNums(Lowest) - 1) * CLNG(RecLength) + 1
  1820.       GET #FileNum, RecOffset&, MainRec$(Lowest)
  1821.     END IF
  1822.  
  1823.   NEXT
  1824.  
  1825. ELSE
  1826.   '----- Only one pass was needed so simply rename the index file
  1827.   NAME "$$PASS.1" AS NDXName$
  1828. END IF
  1829.  
  1830. CLOSE                       'close all open files
  1831.  
  1832. IF Exist%("$$PASS.*") THEN  'ensure there's a file to kill
  1833.   KILL "$$PASS.*"           'kill the work files
  1834. END IF
  1835.  
  1836. ERASE FileNums, RecordNums  'erase the work arrays
  1837. ERASE MainRec$, Remaining
  1838.  
  1839. END SUB
  1840.  
  1841.  
  1842. SUB LoadFile (FileNum, Segment, Address, Bytes&) STATIC
  1843.   IF Bytes& > 32767 THEN Bytes& = Bytes& - 65536
  1844.   Registers.AX = &H3F00         'read from file service
  1845.   Registers.BX = FILEATTR(FileNum, 2) 'get the DOS handle
  1846.   Registers.CX = Bytes&         'how many bytes to load
  1847.   Registers.DX = Address        'and at what address
  1848.   Registers.DS = Segment        'and at what segment
  1849.   CALL DOSInt(Registers)
  1850. END SUB
  1851.  
  1852.  
  1853. SUB SaveFile (FileNum, Segment, Address, Bytes&) STATIC
  1854.   IF Bytes& > 32767 THEN Bytes& = Bytes& - 65536
  1855.   Registers.AX = &H4000         'write to file service
  1856.   Registers.BX = FILEATTR(FileNum, 2) 'get the DOS handle
  1857.   Registers.CX = Bytes&         'how many bytes to load
  1858.   Registers.DX = Address        'and at what address
  1859.   Registers.DS = Segment        'and at what segment
  1860.   CALL DOSInt(Registers)
  1861. END SUB
  1862.  
  1863.  
  1864. SUB TypeISort (....) STATIC     'as shown in TYPISORT.BAS
  1865.  
  1866. END SUB
  1867.  
  1868. FILESORT.BAS begins by defining a function that returns a random number
  1869. between 1 and 10.  Although the earlier sort demonstrations simply read the
  1870. test data from DATA statements, that is impractical when creating thousands
  1871. of records.  Instead, two arrays are filled--one with ten last names and
  1872. another with ten first names--and these names are drawn from at random.
  1873.    The Registers TYPE variable that is defined is used by three of the
  1874. supporting routines in this program.  RegType is normally associated with
  1875. CALL Interrupt and InterruptX, but I have written a small-code replacement
  1876. to mimic InterruptX that works with DOS Interrupt &H21 only.  DOSInt
  1877. accepts just a single Registers argument, instead of the three parameters
  1878. that BASIC's Interrupt and InterruptX require.  Besides adding less code
  1879. each time it is used, the routine itself is smaller and simpler than
  1880. InterruptX.
  1881.    The remainder of the demonstration program should be easy to follow, so
  1882. I won't belabor its operation; the real action is in the FileSort
  1883. subprogram.
  1884.    Like TypeSort and TypeISort, FileSort is entirely pointer based, to
  1885. accommodate TYPE elements of any size and structure.  You provide the name
  1886. of the main data file to be sorted, the name of an index file to create,
  1887. and the length and offset of the keys within the disk records.  The
  1888. Displace parameter tells how far into the TYPE structure the key
  1889. information is located.  When calling TypeISort this value is should be
  1890. one-based, but in the final merge pass where Compare3 is used, a zero-based
  1891. number is required.  Therefore, a copy is made (Offset = Displace - 1) near
  1892. the beginning of the routine.  This way, both are available quickly without
  1893. having to calculate - 1 repeatedly slowing its operation.
  1894.    The initial steps FileSort performs are to determine how many records
  1895. are in the data file, and from that how many records can fit into memory
  1896. at one time.  Once these are known, the number of passes necessary can be
  1897. easily calculated.  An extra step is needed to ensure that RecsPerPass is
  1898. not greater than the number of records in the file.  Just because 200
  1899. records can fit into memory at once doesn't mean there are that many
  1900. records.  In most cases where multiple passes are needed the last pass will
  1901. process fewer records than the others.  If there are, say, 700 records and
  1902. each pass can sort 300, the last pass will sort only 100 records.
  1903.    Once the pass information is determined, a block of memory is created
  1904. to hold each portion of the file for sorting.  This is the purpose of the
  1905. Buffer array.  REDIM is used to create a 32K chunk of memory that doesn't
  1906. impinge on available string space.
  1907.    For each pass that is needed, the number of records in the current pass
  1908. is determined and the index array is initialized to increasing values. 
  1909. Then, a portion of the main data file is read using the LoadFile
  1910. subprogram.  BASIC does not allow you to read records from a random access
  1911. file directly into a buffer specified by its address.  And even if it did,
  1912. we can load data much faster than pure BASIC by reading a number of records
  1913. all at once.
  1914.    Once the current block of records has been loaded, TypeISort is called
  1915. to sort the index array.  The index array is also saved very quickly using
  1916. SaveFile, which is the compliment to LoadFile.  A unique name is given to
  1917. each temporary index file such that the first one is named $$PASS.1, the
  1918. second $$PASS.2, and so forth.  By using dollar signs in the name it is
  1919. unlikely that the routine will overwrite an existing file from another
  1920. application.  Of course, you may change the names to anything else if you
  1921. prefer.
  1922.    Notice the extra step that manipulates the IndexAdjust variable.  This
  1923. adjustment is needed because each sort pass returns the index array holding
  1924. record numbers starting at 0.  The first time through, 1 must be added to
  1925. each element to reflect BASIC's use of record numbers that start at 1.  If
  1926. the first pass sorts, say, 250 records, then the index values 1 through 250
  1927. are saved to disk.  But the second pass is processing records 251 through
  1928. 500, so an adjustment value of 251 must be added to each element prior to
  1929. writing it to disk.
  1930.    If the data file is small and only one pass was needed, the $$PASS.1
  1931. file is simply renamed to whatever the caller specified.  Otherwise, a
  1932. merge pass is needed to determine which record number is the next in
  1933. sequence based on the results of each pass.  Believe it or not, this is the
  1934. trickiest portion of the entire program.  For the sake of discussion, we'll
  1935. assume that four passes were required to sort the file.
  1936.    Each of the four index files contains a sequence of record numbers, and
  1937. all of the records within that sequence are in sorted order.  However,
  1938. there is no relationship between the data records identified in one index
  1939. file and those in another.  Thus, each index file and corresponding data
  1940. record must be read in turn.  A FOR/NEXT loop then compares each of the
  1941. four records, to see which is truly next in the final sequence.  The
  1942. complication arises as the merge nears completion, because some of the
  1943. indexes will have become exhausted.  This possibility is handled by the
  1944. Remaining array.
  1945.    Elements in the Remaining array are initialized to the length of each
  1946. index file as the indexes are opened.  Then, as each index entry is read
  1947. from disk, the corresponding element is decremented by two to show that
  1948. another record number was read.  Therefore, the current Remaining element
  1949. must be checked to see if that index has been exhausted.  Otherwise, data
  1950. that was already processed might be considered in the merge comparisons.
  1951.    The final steps are to close all the open files, delete the temporary
  1952. index files, and erase the work arrays to free the memory they occupied.
  1953.    One important point to observe is the use of SSEG to show Compare3 where
  1954. the MainRec$ elements are located.  SSEG is for BASIC 7 only; if you are
  1955. using QuickBASIC you must change SSEG to VARSEG.  SSEG can be used with
  1956. either near or far strings in BASIC 7, but VARSEG works with near strings
  1957. only.  SSEG is used as the default, so an error will be reported if you are
  1958. using QuickBASIC.  The cursor will then be placed near the comment in the
  1959. program that shows the appropriate correction.
  1960.  
  1961.  
  1962. SEARCHING FUNDAMENTALS
  1963. ======================
  1964.  
  1965. As with sorting, searching data effectively also requires that you select
  1966. an appropriate algorithm.  There are many ways to search data, and we will
  1967. look at several methods here.  The easiest to understand is a linear
  1968. search, which simply examines each item in sequence until a match is found:
  1969.  
  1970.  
  1971. FoundAt = 0                   'assume no match
  1972.  
  1973. FOR X = 1 TO NumElements      'search all elements
  1974.   IF Array$(X) = Sought$ THEN
  1975.     FoundAt = X               'remember where it is
  1976.     EXIT FOR                  'no need to continue
  1977.   END IF
  1978. NEXT
  1979.  
  1980. IF FoundAt THEN               'if it was found
  1981.   PRINT "Found at element"; FoundAt
  1982. ELSE
  1983.   PRINT "Not found"           'otherwise
  1984. END IF
  1985.  
  1986.  
  1987. For small arrays a linear search is effective and usually fast enough. 
  1988. Also, integer and long integer arrays can be searched reasonably quickly
  1989. even if there are many elements.  But with string data, as the number of
  1990. elements that must be searched increases, the search time can quickly
  1991. become unacceptable.  This is particularly true when additional features
  1992. are required such as searching without regard to capitalization or
  1993. comparing only a portion of each element using MID$.  Indeed, many of the
  1994. same techniques that enhance a sort routine can also be employed when
  1995. searching.
  1996.    To search ignoring capitalization you would first capitalize Sought$
  1997. outside of the loop, and then use UCASE$ with each element in the
  1998. comparisons.  Using UCASE$(Sought$) repeatedly within the loop is both
  1999. wasteful and unnecessary:
  2000.  
  2001.    Sought$ = UCASE$(Sought$)
  2002.     .
  2003.     .
  2004.    IF UCASE$(Array$(X)) = Sought$ THEN
  2005.  
  2006. Likewise, comparing only a portion of each string will require MID$ with
  2007. each comparison, after using MID$ initially to extract what is needed from
  2008. Sought$:
  2009.  
  2010.    Sought$ = MID$(Sought$, 12, 6)
  2011.     .
  2012.     .
  2013.    IF MID$(Array$(X), 12, 6) = Sought$ THEN
  2014.  
  2015. And again, as with sorting, these changes may be combined in a variety of
  2016. ways.  You could even use INSTR to see if the string being searched for
  2017. is within the array, when an exact match is not needed:
  2018.  
  2019.    IF INSTR(UCASE$(Array$(X)), Sought$) THEN
  2020.  
  2021. However, each additional BASIC function you use will make the searching
  2022. slower and slower.  Although BASIC's INSTR is very fast, adding UCASE$ to
  2023. each comparison as shown above slows the overall process considerably.
  2024.    There are three primary ways that searching can be speeded up.  One is
  2025. to apply simple improvements based on understanding how BASIC works, and
  2026. knowing which commands are fastest.  The other is to select a better
  2027. algorithm.  The third is to translate selected portions of the search
  2028. routine into assembly language.  I will use all three of these techniques
  2029. here, starting with enhancements to the linear search, and culminating with
  2030. a very fast binary search for use with sorted data.
  2031.    One of the slowest operations that BASIC performs is comparing strings. 
  2032. For each string, its descriptor address must be loaded and passed to the
  2033. comparison routine.  That routine must then obtain the actual data address,
  2034. and examine each byte in both strings until one of the characters is
  2035. different, or it determines that both strings are the same.  As I mentioned
  2036. earlier, if one or both of the strings are fixed-length, then copies also
  2037. must be made before the comparison can be performed.
  2038.    There is another service that the string comparison routine must
  2039. perform, which is probably not obvious to most programmers and which also
  2040. impacts its speed.  BASIC frequently creates and then deletes temporary
  2041. strings without your knowing it.  One example is the copy it makes of
  2042. fixed-length strings before comparing them.  But there are other, more
  2043. subtle situations in which this can happen.
  2044.    For example, when you use IF X$ + Y$ > Z$ BASIC must create a temporary
  2045. string comprised of X$ + Y$, and then pass that to the comparison routine. 
  2046. Therefore, that routine is also responsible for determining if the incoming
  2047. string is a temporary copy, and deleting it if so.  In fact, all of BASIC's
  2048. internal routines that accept string arguments are required to do this.
  2049.    Therefore, one good way to speed searching of conventional (not fixed-
  2050. length) string arrays is to first compare the lengths.  Since strings whose
  2051. lengths are different can't possibly be the same, this will quickly weed
  2052. those out.  BASIC's LEN function is much faster than its string compare
  2053. routine, and it offers a simple but effective opportunity to speed things
  2054. up.  LEN is made even faster because it requires only a single argument,
  2055. as opposed to the two required for the comparison routine.
  2056.  
  2057.  
  2058. SLen = LEN(Sought$)       'do this once outside the loop
  2059. FOR X = 1 TO NumElements
  2060.   IF LEN(Array$(X)) = SLen THEN   'maybe...
  2061.     IF Array$(X) = Sought$ THEN   'found it!
  2062.       FoundAt = X
  2063.       EXIT FOR
  2064.     END IF
  2065.   END IF
  2066. NEXT
  2067.  
  2068.  
  2069. Similarly, if the first characters are not the same then the strings can't
  2070. match either.  Like LEN, BASIC's ASC is much faster than the full string
  2071. comparison routine, and it too can improve search time by eliminating
  2072. elements that can't possibly match.  Depending on the type and distribution
  2073. of the data in the array, using both LEN and ASCII can result in a very
  2074. fast linear search:
  2075.  
  2076.  
  2077. SLen = LEN(Sought$)
  2078. SAsc = ASC(Sought$)
  2079. FOR X = 1 TO NumElements
  2080.   IF LEN(Array$(X)) = SLen THEN
  2081.     IF ASC(Array$(X)) = SAsc THEN
  2082.       IF Array$(X) = Sought$ THEN
  2083.         ...
  2084.       END IF
  2085.     END IF
  2086.   END IF
  2087. NEXT
  2088.  
  2089.  
  2090. Notice that the LEN test must always be before the ASC test, to avoid an
  2091. "Illegal function call" error if the array element is a null string.  If
  2092. all or most of the strings are the same length, then LEN will not be
  2093. helpful, and ASC should be used alone.
  2094.    As I mentioned before, when comparing fixed-length string arrays BASIC
  2095. makes a copy of each element into a conventional string, prior to calling
  2096. its comparison routine.  This copying is also performed when using ASC is
  2097. used, but not LEN.  After all, the length of a fixed-length never changes,
  2098. and BASIC is smart enough to know the length directly.  But then, comparing
  2099. the lengths of these string is pointless anyway.
  2100.    Because of the added overhead to make these copies, the performance of
  2101. a conventional linear search for fixed-length data is generally quite poor. 
  2102. This is a shame, because fixed-length strings are often the only choice
  2103. when as much data as possible must be kept in memory at once.  And fixed-
  2104. length strings lend themselves perfectly to names and addresses.  It should
  2105. be apparent by now that the best solution for quickly comparing fixed-
  2106. length string arrays--and the string portion of TYPE arrays too--is with
  2107. the various Compare functions already shown.
  2108.    If you are searching for an exact match, then either Compare or Compare2
  2109. will be ideal, depending on whether you want to ignore capitalization.  If
  2110. you have only a single string element in each array, you should define a
  2111. dummy TYPE.  This avoids the overhead of having to use both VARSEG and
  2112. VARPTR as separate arguments.  The short example program and SearchType
  2113. functions that follow search a fixed-length string array for a match.
  2114.  
  2115. DEFINT A-Z
  2116. DECLARE FUNCTION Compare% (SEG Type1 AS ANY, SEG Type2 AS ANY, NumBytes)
  2117. DECLARE FUNCTION Compare2% (SEG Type1 AS ANY, SEG Type2 AS ANY, NumBytes)
  2118. DECLARE FUNCTION SearchType% (Array() AS ANY, Sought AS ANY)
  2119. DECLARE FUNCTION SearchType2% (Array() AS ANY, Sought AS ANY)
  2120. DECLARE FUNCTION SearchType3% (Array() AS ANY, Searched AS ANY)
  2121.  
  2122. CLS
  2123. TYPE FLen                       'this lets us use SEG
  2124.   LastName AS STRING * 15
  2125. END TYPE
  2126.  
  2127. REDIM Array(1 TO 4000) AS FLen  '4000 is a lot of names
  2128. DIM Search AS FLen              'best comparing like data
  2129.  
  2130. FOR X = 1 TO 4000 STEP 2        'impart some realism
  2131.   Array(X).LastName = "Henderson"
  2132. NEXT
  2133.  
  2134. Array(4000).LastName = "Henson" 'almost at the end
  2135. Search.LastName = "Henson"      'find the same name
  2136.  
  2137. '----- first time how long it takes using Compare
  2138. Start! = TIMER                  'start timing
  2139.  
  2140. FOR X = 1 TO 5                  'search five times
  2141.    FoundAt = SearchType%(Array(), Search)
  2142. NEXT
  2143.  
  2144. IF FoundAt >= 0 THEN
  2145.   PRINT "Found at element"; FoundAt
  2146. ELSE
  2147.   PRINT "Not found"
  2148. END IF
  2149.  
  2150. Done! = TIMER
  2151. PRINT USING "##.## seconds with Compare"; Done! - Start!
  2152. PRINT
  2153.  
  2154.  
  2155. '----- then time how long it takes using Compare2
  2156. Start! = TIMER                  'start timing
  2157.  
  2158. FOR X = 1 TO 5                  'as above
  2159.    FoundAt = SearchType2%(Array(), Search)
  2160. NEXT
  2161.  
  2162. IF FoundAt >= 0 THEN
  2163.   PRINT "Found at element"; FoundAt
  2164. ELSE
  2165.   PRINT "Not found"
  2166. END IF
  2167.  
  2168. Done! = TIMER
  2169. PRINT USING "##.## seconds with Compare2"; Done! - Start!
  2170. PRINT
  2171.  
  2172.  
  2173. '---- finally, time how long it takes using pure BASIC
  2174. Start! = TIMER
  2175.  
  2176. FOR X = 1 TO 5
  2177.    FoundAt = SearchType3%(Array(), Search)
  2178. NEXT
  2179.  
  2180. IF FoundAt >= 0 THEN
  2181.   PRINT "Found at element"; FoundAt
  2182. ELSE
  2183.   PRINT "Not found"
  2184. END IF
  2185.  
  2186. Done! = TIMER
  2187. PRINT USING "##.## seconds using BASIC"; Done! - Start!
  2188. END
  2189.  
  2190. FUNCTION SearchType% (Array() AS FLen, Sought AS FLen) STATIC
  2191.  
  2192. SearchType% = -1                'assume not found
  2193.  
  2194. FOR X = LBOUND(Array) TO UBOUND(Array)
  2195.   IF Compare%(Array(X), Sought, LEN(Sought)) THEN
  2196.     SearchType% = X             'save where it was found
  2197.     EXIT FOR                    'and skip what remains
  2198.   END IF
  2199. NEXT
  2200.  
  2201. END FUNCTION
  2202.  
  2203.  
  2204. FUNCTION SearchType2% (Array() AS FLen, Sought AS FLen) STATIC
  2205.  
  2206. SearchType2% = -1               'assume not found
  2207.  
  2208. FOR X = LBOUND(Array) TO UBOUND(Array)
  2209.   IF Compare2%(Array(X), Sought, LEN(Sought)) THEN
  2210.     SearchType2% = X            'save where it was found
  2211.     EXIT FOR                    'and skip what remains
  2212.   END IF
  2213. NEXT
  2214.  
  2215. END FUNCTION
  2216.  
  2217.  
  2218. FUNCTION SearchType3% (Array() AS FLen, Searched AS FLen) STATIC
  2219.  
  2220. SearchType3% = -1               'assume not found
  2221.  
  2222. FOR X = LBOUND(Array) TO UBOUND(Array)
  2223.   IF Array(X).LastName = Searched.LastName THEN
  2224.     SearchType3% = X            'save where it was found
  2225.     EXIT FOR                    'and skip what remains
  2226.   END IF
  2227. NEXT
  2228.   
  2229. END FUNCTION
  2230.  
  2231. When you run this program it will be apparent that the SearchType function
  2232. is the fastest, because it uses Compare which doesn't perform any case
  2233. conversions.  SearchType2 is only slightly slower with that added overhead,
  2234. and the purely BASIC function, SearchType3, lags far behind at half the
  2235. speed.  Note that the array is searched five times in succession, to
  2236. minimize the slight errors TIMER imposes.  Longer timings are generally
  2237. more accurate than short ones, because of the 1/18th second resolution of
  2238. the PC's system timer.
  2239.  
  2240.  
  2241. BINARY SEARCHES
  2242.  
  2243. This is about as far as we can go using linear searching, and to achieve
  2244. higher performance requires a better algorithm.  The Binary Search is one
  2245. of the fastest available; however, it requires the data to already be in
  2246. sorted order.  A Binary Search can also be used with a sorted index, and
  2247. both methods will be described.
  2248.    Binary searches are very fast, and also very simple to understand. 
  2249. Unlike the Quick Sort algorithm which achieves great efficiency at the
  2250. expense of being complicated, a Binary Search can be written using only a
  2251. few lines of code.  The strategy is to start the search at the middle of
  2252. the array.  If the value of that element value is less than that of the
  2253. data being sought, a new halfway point is checked and the process repeated. 
  2254. This way, the routine can quickly zero in on the value being searched for. 
  2255. Figure 8-3 below shows how this works.
  2256.  
  2257.  
  2258. 13:  Zambia
  2259. 12:  Sweden
  2260. 11:  Peru
  2261. 10:  Mexico  <-- step 2
  2262.  9:  Holland
  2263.  8:  Germany
  2264.  7:  Finland <-- step 1
  2265.  6:  England
  2266.  5:  Denmark
  2267.  4:  China
  2268.  3:  Canada
  2269.  2:  Austria
  2270.  1:  Australia
  2271.  
  2272. Figure 8-3: How a Binary Search locates data in a sorted array.
  2273.  
  2274.  
  2275. If you are searching for Mexico, the first element examined is number 7,
  2276. which is halfway through the array.  Comparing Mexico to Finland shows
  2277. that Mexico is greater, so the distance is again cut in half.  In this
  2278. case, a match was found after only two tries--remarkably faster than a
  2279. linear search that would have required ten comparisons.  Even when huge
  2280. arrays must be searched, data can often be found in a dozen or so tries. 
  2281. One interesting property of a binary search is that it takes no longer to
  2282. find the last element in the array than the first one.
  2283.    The program below shows one way to implement a Binary Search.
  2284.  
  2285. DEFINT A-Z
  2286. DECLARE FUNCTION BinarySearch% (Array$(), Find$)
  2287.  
  2288. CLS
  2289. PRINT "Creating test data..."
  2290.  
  2291. REDIM Array$(1 TO 1000)         'create a "sorted" array
  2292. FOR X = 1 TO 1000
  2293.   Array$(X) = "String " + RIGHT$("000" + LTRIM$(STR$(X)), 4)
  2294. NEXT
  2295.  
  2296. PRINT "Searching array..."
  2297.  
  2298. FoundAt = BinarySearch%(Array$(), "String 0987")
  2299. IF FoundAt >= 0 THEN
  2300.   PRINT "Found at element"; FoundAt
  2301. ELSE
  2302.   PRINT "Not found"
  2303. END IF
  2304.  
  2305. END
  2306.  
  2307.  
  2308. FUNCTION BinarySearch% (Array$(), Find$) STATIC
  2309.  
  2310. BinarySearch% = -1              'no matching element yet
  2311. Min = LBOUND(Array$)            'start at first element
  2312. Max = UBOUND(Array$)            'consider through last
  2313.  
  2314. DO
  2315.   Try = (Max + Min) \ 2         'start testing in middle
  2316.  
  2317.   IF Array$(Try) = Find$ THEN   'found it!
  2318.     BinarySearch% = Try         'return matching element
  2319.     EXIT DO                     'all done
  2320.   END IF
  2321.  
  2322.   IF Array$(Try) > Find$ THEN   'too high, cut in half
  2323.     Max = Try - 1
  2324.   ELSE
  2325.     Min = Try + 1               'too low, cut other way
  2326.   END IF
  2327. LOOP WHILE Max >= Min
  2328.  
  2329. END FUNCTION
  2330.  
  2331. The BinarySearch function returns either the element number where a match
  2332. was found, or -1 if the search string was not found.  Not using a value of
  2333. zero to indicate failure lets you use arrays that start with element number
  2334. 0.  As you can see, the simplicity of this algorithm belies its incredible
  2335. efficiency.  The only real problem is that the data must already be in
  2336. sorted order.  Also notice that two string comparisons must be made--one
  2337. to see if the strings are equal, and another to see if the current element
  2338. is too high.  Although you could use Compare3 which examines the strings
  2339. once and tells if the data is the same or which is greater, a Binary Search
  2340. is so fast that this probably isn't worth the added trouble.  As you will
  2341. see when you run the test program, it takes far longer to create the data
  2342. than to search it!
  2343.    Besides the usual enhancements that can be applied to the comparisons
  2344. using UCASE$ or MID$, this function could also be structured to use a
  2345. parallel index array.  Assuming the data is not sorted but the index array
  2346. is, the modified Binary Search would look like this:
  2347.  
  2348. FUNCTION BinaryISearch% (Array$(), Index(), Find$) STATIC
  2349.  
  2350. BinaryISearch% = -1             'assume not found
  2351. Min = LBOUND(Array$)            'start at first element
  2352. Max = UBOUND(Array$)            'consider through last
  2353.  
  2354. DO
  2355.   Try = (Max + Min) \ 2         'start testing in middle
  2356.  
  2357.   IF Array$(Index(Try)) = Find$ THEN    'found it!
  2358.     BinaryISearch% = Try        'return matching element
  2359.     EXIT DO                     'all done
  2360.   END IF
  2361.  
  2362.   IF Array$(Index(Try)) > Find$ THEN    'too high, cut
  2363.     Max = Try - 1
  2364.   ELSE
  2365.     Min = Try + 1               'too low, cut other way
  2366.   END IF
  2367. LOOP WHILE Max >= Min
  2368.  
  2369. END FUNCTION
  2370.  
  2371. NUMERIC ARRAYS
  2372.  
  2373. All of the searching techniques considered so far have addressed string
  2374. data.  In most cases, string array searches are the ones that will benefit
  2375. the most from improved techniques.  As you have already seen, BASIC makes
  2376. copies of fixed-length strings before comparing them, which slows down
  2377. searching.  And the very nature of strings implies that many bytes may have
  2378. to be compared before determining if they are equal or which string is
  2379. greater.  In most cases, searching a numeric array is fast enough without
  2380. requiring any added effort, especially when the data is integer or long
  2381. integer.
  2382.    However, a few aspects of numeric searching are worth mentioning here. 
  2383. One is avoiding the inevitable rounding errors that are sure to creep into
  2384. the numbers you are examining.  Another is that in many cases, you may not
  2385. be looking for an exact match.  For example, you may need to find the first
  2386. element that is higher than a given value, or perhaps determine the
  2387. smallest value in an array.
  2388.    Unlike strings that are either the same or they aren't, the binary
  2389. representation of numeric values is not always so precise.  Consider the
  2390. following test which *should* result in a match, but doesn't.
  2391.  
  2392.  
  2393. Value! = 1!
  2394. Result! = 2!
  2395. CLS
  2396.  
  2397. FOR X = 1 TO 1000
  2398.   Value! = Value! + .001
  2399. NEXT
  2400.  
  2401. IF Value! = Result! THEN
  2402.   PRINT "They are equal"
  2403. ELSE
  2404.   PRINT "Value! ="; Value!
  2405.   PRINT "Result! ="; Result!
  2406. END IF
  2407.  
  2408.  
  2409.  After adding .001 to Value! 1000 times Value! should be equal to 2, but
  2410. instead it is slightly higher.  This is because the binary storage method
  2411. used by computers simply cannot represent every possible value with
  2412. absolute accuracy.  Even changing all of the single precision exclamation
  2413. points (!) to double precision pound signs (#) will not solve the problem. 
  2414. Therefore, to find a given value in a numeric array can require some extra
  2415. trickery.
  2416.    What is really needed is to determine if the numbers are *very close* to
  2417. each other, as opposed to exactly the same.  One way to accomplish this is
  2418. to subtract the two, and see if the result is very close to zero.  This is
  2419. shown below.
  2420.  
  2421.  
  2422. Value! = 1!
  2423. Result! = 2!
  2424. CLS
  2425.  
  2426. FOR X = 1 TO 1000
  2427.   Value! = Value! + .001
  2428. NEXT
  2429.  
  2430. IF ABS(Value! - Result!) < .0001 THEN
  2431.   PRINT "They are equal"
  2432. ELSE
  2433.   PRINT "Value! ="; Value!
  2434.   PRINT "Result! ="; Result!
  2435. END IF
  2436.  
  2437.  
  2438. Here, the absolute value of the difference between the numbers is examined,
  2439. and if that difference is very small the numbers are assumed to be the
  2440. same.  Unfortunately, the added overhead of subtracting before comparing
  2441. slows the comparison even further.  There is no simple cure for this, and
  2442. an array search must apply this subtraction to each element that is
  2443. examined.
  2444.    Another common use for numeric array searches is when determining the
  2445. largest or smallest value.  Many programmers make the common mistake shown
  2446. below when trying to find the largest value in an array.
  2447.  
  2448.  
  2449. MaxValue# = 0
  2450.  
  2451. FOR X = 1 TO NumElements
  2452.   IF Array#(X) > MaxValue# THEN
  2453.     MaxValue# = Array#(X)
  2454.     Element = X
  2455.   END IF
  2456. NEXT
  2457.  
  2458. PRINT "The largest value found is"; MaxValue#
  2459. PRINT "And it was found at element"; Element
  2460.  
  2461.  
  2462. The problem with this routine is that it doesn't account for arrays where
  2463. all of the elements are negative numbers!  In that case no element will be
  2464. greater than the initial MaxValue#, and the routine will incorrectly report
  2465. zero as the result.  The correct method is to obtain the lowest element
  2466. value, and use that as a starting point:
  2467.  
  2468.  
  2469. MaxValue# = Array#(1)
  2470.  
  2471. FOR X = 2 TO NumElements
  2472.   IF Array#(X) > MaxValue# THEN
  2473.     MaxValue# = Array#(X)
  2474.   END IF
  2475. NEXT
  2476.  
  2477. PRINT "The largest value found is"; MaxValue#
  2478.  
  2479.  
  2480. Determining the highest value in an array would be handled similarly,
  2481. except the greater-than symbol (>) would be replaced with a less-than
  2482. operator (<).
  2483.  
  2484.  
  2485. SOUNDEX
  2486.  
  2487. The final searching technique I will show is Soundex.  It is often useful
  2488. to search for data based on its sound, for example when you do not know how
  2489. to spell a person's name.  Soundex was invented in the 1920's and has been
  2490. used since then by, among others, the U.S. Census Bureau.  A Soundex code
  2491. is an alpha-numeric representation of the sound of a word, and it is
  2492. surprisingly accurate despite its simplicity.  The classic implementation
  2493. of Soundex returns a four-character result code.  The first character is
  2494. the same as the first letter of the word, and the other three are numeric
  2495. digits coded as shown in Figure 8-4.
  2496.  
  2497.  
  2498.     1    B, F, P, V
  2499.     2    C, G, J, K, Q, S, X
  2500.     3    D, T
  2501.     4    L
  2502.     5    M, N
  2503.     6    R
  2504.  
  2505. Figure 8-4: The Soundex code numbers returned for significant letters of
  2506. the alphabet.
  2507.  
  2508.  
  2509. Letters not shown are simply skipped as being statistically insignificant
  2510. to the sound of the word.  In particular, speaking accents often minimize
  2511. the importance of vowels, and blur their distinction.  If the string is
  2512. short and there are fewer than four digits, the result is simply padded
  2513. with trailing zeros.  One additional rule is that a code digit is never
  2514. repeated, unless there is an uncoded letter in between.  In the listing
  2515. that follows, two different implementations of Soundex are shown.
  2516.  
  2517. 'SOUNDEX.BAS, Soundex routines and example
  2518.  
  2519. DEFINT A-Z
  2520.  
  2521. DECLARE FUNCTION ASoundex$ (Word$)
  2522. DECLARE FUNCTION ISoundex% (Word$)
  2523.  
  2524. CLS
  2525. DO
  2526.   PRINT "press Enter alone to exit"
  2527.   INPUT "What is the first word"; FWord$
  2528.   IF LEN(FWord$) = 0 THEN EXIT DO
  2529.   INPUT "What is the second word"; SWord$
  2530.   PRINT
  2531.  
  2532.   'Test by alpha-numeric soundex
  2533.   PRINT "Alpha-Numeric Soundex: "; FWord$; " and ";
  2534.   PRINT SWord$; " do ";
  2535.   IF ASoundex$(FWord$) <> ASoundex$(SWord$) THEN
  2536.     PRINT "NOT ";
  2537.   END IF
  2538.   PRINT "sound the same."
  2539.   PRINT
  2540.  
  2541.   'Test by numeric soundex
  2542.   PRINT "      Numeric Soundex: "; FWord$; " and ";
  2543.   PRINT SWord$; " do ";
  2544.   IF ISoundex%(FWord$) <> ISoundex%(SWord$) THEN
  2545.     PRINT "NOT ";
  2546.   END IF
  2547.   PRINT "sound the same."
  2548.   PRINT
  2549. LOOP
  2550. END
  2551.  
  2552.  
  2553. FUNCTION ASoundex$ (InWord$) STATIC
  2554.  
  2555.   Word$ = UCASE$(InWord$)
  2556.   Work$ = LEFT$(Word$, 1) + "000"
  2557.   WkPos = 2
  2558.   PrevCode = 0
  2559.  
  2560.   FOR L = 2 TO LEN(Word$)
  2561.     Temp = INSTR("BFPVCGJKQSXZDTLMNR", MID$(Word$, L, 1))
  2562.     IF Temp THEN
  2563.       Temp = ASC(MID$("111122222222334556", Temp, 1))
  2564.       IF Temp <> PrevCode THEN
  2565.         MID$(Work$, WkPos) = CHR$(Temp)
  2566.         PrevCode = Temp
  2567.         WkPos = WkPos + 1
  2568.         IF WkPos > 4 THEN EXIT FOR
  2569.       END IF
  2570.     ELSE
  2571.       PrevCode = 0
  2572.     END IF
  2573.   NEXT
  2574.  
  2575.   ASoundex$ = Work$
  2576.  
  2577. END FUNCTION
  2578.  
  2579.  
  2580. FUNCTION ISoundex% (InWord$) STATIC
  2581.  
  2582.   Word$ = UCASE$(InWord$)
  2583.   Work$ = "0000"
  2584.   WkPos = 1
  2585.   PrevCode = 0
  2586.  
  2587.   FOR L = 1 TO LEN(Word$)
  2588.     Temp = INSTR("BFPVCGJKQSXZDTLMNR", MID$(Word$, L, 1))
  2589.     IF Temp THEN
  2590.       Temp = ASC(MID$("111122222222334556", Temp, 1))
  2591.       IF Temp <> PrevCode THEN
  2592.         MID$(Work$, WkPos) = CHR$(Temp)
  2593.         PrevCode = Temp
  2594.         WkPos = WkPos + 1
  2595.         IF WkPos > 4 THEN EXIT FOR
  2596.       END IF
  2597.     ELSE
  2598.       PrevCode = 0
  2599.     END IF
  2600.   NEXT
  2601.  
  2602.   ISoundex% = VAL(Work$)
  2603.  
  2604. END FUNCTION
  2605.  
  2606.  
  2607. The first function, ASoundex, follows the standard Soundex definition and
  2608. returns the result as a string.  The ISoundex version cheats slightly by
  2609. coding the first letter as a number, but it returns an integer value
  2610. instead of a string.  Because integer searches are many times faster than
  2611. string searches, this version will be better when thousands--or even
  2612. hundreds of thousands--of names must be examined.
  2613.    An additional benefit of the integer-only method is that it allows for
  2614. variations on the first letter.  For example, if you enter Cane and Kane
  2615. in response to the prompts from SOUNDEX.BAS ASoundex will not recognize the
  2616. names as sounding alike where ISoundex will.
  2617.  
  2618.  
  2619. LINKED DATA
  2620. ===========
  2621.  
  2622. No discussion of searching and sorting would be complete without a mention
  2623. of linked lists and other data links.  Unlike arrays where all of the
  2624. elements lie in adjacent memory locations, linked data is useful when data
  2625. locations may be disjointed.  One example is the linked list used by the
  2626. DOS File Allocation Table (FAT) on every disk.  As I described in Chapter
  2627. 6, the data in each file may be scattered throughout the disk, and only
  2628. through a linked list can DOS follow the thread from one sector in a file
  2629. to another.
  2630.    Another example where linked data is useful--and the one we will focus
  2631. on here--is to keep track of memo fields in a database.  A memo field is
  2632. a field that can store freeform text such as notes about a sales contact
  2633. or a patient's medical history.  Since these fields typically require
  2634. varying lengths, it is inefficient to reserve space for the longest one
  2635. possible in the main database file.  Therefore, most programs store memo
  2636. fields in a separate disk file, and use a *pointer field* in the main data
  2637. file to show where the corresponding memo starts in the dedicated memo
  2638. file.  Similarly, a back pointer adjacent to each memo identifies the
  2639. record that points to it.  This is shown in Figure 8-5 below.
  2640.  
  2641.  
  2642.          ┌────────┬─────────┬─╥────────┬─────────┬──╥─────────
  2643. MAIN.DAT │LastName│FirstName│1║LastName│FirstName│73║LastNa...
  2644.          └────────┴─────────┴─╨────────┴─────────┴──╨─────────
  2645.                              ^                    ^
  2646.                              |                    |
  2647. pointers into memo file -----+--------------------+
  2648. (forward pointers)
  2649.  
  2650.  
  2651.  
  2652. offsets into --+----------+-------+--------------+
  2653. this memo file |          |       |              |
  2654.                1          73     126            233
  2655.               ┌──────────┬───────┬──────────────┬─────────┐
  2656.      MEMO.DAT │1LMemo1   │2LMemo2│3LMemo3       │4LMemo4  │
  2657.               └──────────┴───────┴──────────────┴─────────┘
  2658.                ^          ^       ^              ^
  2659.                |          |       |              |
  2660. record numbers +----------+-------+--------------+
  2661. (back pointers)
  2662.  
  2663. (L = length of this memo)
  2664.  
  2665. Figure 8-5: Pointers relate record numbers to memo file offsets and vice
  2666. versa.
  2667.  
  2668.  
  2669. Here, the pointer in the main data file record is a long integer that holds
  2670. the byte offset into the memo file where the corresponding memo text
  2671. begins.  And just before the memo text is an integer record number that
  2672. shows which record this memo belongs to.  (If you anticipate more than
  2673. 65,535 records a long integer must be used instead.)  Thus, these pointers
  2674. provide links between the two files, and relate the information they
  2675. contain.
  2676.    When a new record is added to the main file, the memo that goes with it
  2677. is appended to the end of the memo file.  BASIC's LOF function can be used
  2678. to determine the current end of the memo file, which is then used as the
  2679. beginning offset for the new memo text.  And as the new memo is appended
  2680. to MEMO.DAT, the first data actually written is the number of the new
  2681. record in the main data file.
  2682.    The record number back pointer in the memo file is needed to allow memo
  2683. data to be edited.  Since there's no reasonable way to extend memo text
  2684. when other memo data follows it, most programs simply abandon the old text,
  2685. and allocate new space at the end of the file.  The abandoned text is then
  2686. marked as such, perhaps by storing a negative value as the record number. 
  2687. Storing a negative version of the abandoned data's length is ideal, because
  2688. that both identifies the data as obsolete, and also tells how much farther
  2689. into the file the next memo is located.
  2690.    The idea here is that you would periodically run a memo file maintenance
  2691. program that compacts the file, thus eliminating the wasted space the
  2692. abandoned memos occupy.  This is similar to the DBPACK.BAS utility shown
  2693. in Chapter 7, and also similar to the way that BASIC compacts string memory
  2694. when it becomes full.  But when an existing memo is relocated in the memo
  2695. file, the field in the main data file that points to the memo must also be
  2696. updated.  And that's why the record number back pointer is needed: so the
  2697. compaction program can know which record in the main file must be updated.
  2698.    The "L" identifier in the memo file in Figure 8-5, shown between the
  2699. record number and memo text, is a length byte or word that tells how long
  2700. the text is.  If you plan to limit the memo field lengths to 255 or fewer
  2701. characters, then a single byte is sufficient.  Otherwise an integer must
  2702. be used.  An example of code that reads a data record and then its
  2703. associated memo text is shown below.
  2704.  
  2705.  
  2706. GET #MainFile, RecNumber, TypeVar
  2707. MemoOffset& = TypeVar.MemoOff
  2708. GET #MemoFile, MemoOffset& + 2, MemoLength%
  2709. Memo$ = SPACE$(MemoLength%)
  2710. GET #MemoFile, , Memo$
  2711.  
  2712.  
  2713. The first step reads a record from the main data file into a TYPE variable,
  2714. and the second determines where in the memo file the memo text begins.  Two
  2715. is added to that offset in the second GET statement, to skip over the
  2716. record number back pointer which isn't needed here.  Once the length of the
  2717. memo text is known, a string is assigned to that length, and the actual
  2718. text is read into it.
  2719.    If you are using long integer record numbers you would of course use
  2720. MemoOffset& + 4 in the second GET.  And if you're using a single byte to
  2721. hold the memo length you would define a fixed-length string to receive
  2722. that byte:
  2723.  
  2724.  
  2725. DIM Temp AS STRING *1
  2726. GET #MemoFile, MemoOffset& + 2, Temp
  2727. MemoLength = ASC(Temp)
  2728.  
  2729.  
  2730. Since BASIC doesn't offer a byte-sized integer data type, ASC and STR$ can
  2731. be used to convert between numeric and string formats.
  2732.  
  2733.  
  2734. ARRAY ELEMENT INSERTION AND DELETION
  2735. ====================================
  2736.  
  2737. The last issue related to array and memory manipulation I want to cover
  2738. is inserting and deleting elements.  If you intend to maintain file indexes
  2739. or other information in memory and in sorted order, you will need some way
  2740. to insert a new entry.  By the same token, deleting an entry in a database
  2741. requires that the parallel index entry also be deleted.
  2742.    The most obvious way to insert or delete elements in an array is with
  2743. a FOR/NEXT loop.  The first example below inserts an element, and the
  2744. second deletes one.
  2745.  
  2746.  
  2747. '----- Insert an element:
  2748. Element = 200
  2749. InsertValue = 999
  2750.  
  2751. FOR X = UBOUND(Array) TO Element + 1 STEP -1
  2752.   Array(X) = Array(X - 1)
  2753. NEXT
  2754. Array(Element) = InsertValue
  2755.  
  2756.  
  2757. '----- Delete an element:
  2758. Element = 200
  2759. FOR X = Element TO UBOUND(Array) - 1
  2760.   Array(X) = Array(X + 1)
  2761. NEXT
  2762. Array(UBOUND(Array)) = 0  'optionally clear last element
  2763.  
  2764.  
  2765. For integer, long integer, and fixed-length arrays this is about as
  2766. efficient as you can get, short of rewriting the code in assembly language. 
  2767. However, with floating point and string arrays the performance is less than
  2768. ideal.  Unless a numeric coprocessor is installed, floating point values
  2769. are assigned using interrupts and support code in the emulator library. 
  2770. This adds an unnecessary level of complication that also impacts the speed. 
  2771. When strings are assigned the situation is even worse, because of the
  2772. memory allocation overhead associated with dynamic string management.
  2773.    A better solution for floating point and string arrays is a series of
  2774. SWAP statements.  The short program below benchmarks the speed difference
  2775. of the two methods, as it inserts an element into a single precision array.
  2776.  
  2777. REDIM Array(1 TO 500)
  2778. CLS
  2779. Element% = 200
  2780. InsertValue = 999
  2781.  
  2782. Start = TIMER
  2783. FOR A% = 1 TO 500
  2784.   FOR X% = UBOUND(Array) TO Element% + 1 STEP -1
  2785.     Array(X%) = Array(X% - 1)
  2786.   NEXT
  2787.   Array(Element%) = InsertValue
  2788. NEXT
  2789. Done = TIMER
  2790. PRINT USING "##.## seconds when assigning"; Done - Start
  2791.  
  2792. Start = TIMER
  2793. FOR A% = 1 TO 500
  2794.   FOR X% = UBOUND(Array) TO Element% + 1 STEP -1
  2795.     SWAP Array(X%), Array(X% - 1)
  2796.   NEXT
  2797.   Array(Element%) = InsertValue
  2798. NEXT
  2799. Done = TIMER
  2800. PRINT USING "##.## seconds when swapping"; Done - Start
  2801.  
  2802. If you run this program in the BASIC environment, the differences may not
  2803. appear that significant.  But when the program is compiled to an executable
  2804. file, the swapping method is more than four times faster.  In fact, you
  2805. should never compare programming methods using the BASIC editor for exactly
  2806. this reason.  In many cases, the slowness of the interpreting process
  2807. overshadows significant differences between one approach and another.
  2808.    String arrays also benefit greatly from using SWAP instead of
  2809. assignments, though the amount of benefit varies depending on the length
  2810. of the strings.  If you modify the previous program to use a string array,
  2811. also add this loop to initialize the elements:
  2812.  
  2813.    FOR X% = 1 TO 500
  2814.      Array$(X%) = "String number" + STR$(X)
  2815.    NEXT
  2816.  
  2817. With BASIC PDS far strings the difference is only slightly less at about
  2818. three to one, due to the added complexity of far data.  Also, SWAP will
  2819. always be worse than assignments when inserting or deleting elements in a
  2820. fixed-length string or TYPE array.  An assignment merely copies the data
  2821. from one location to another.  SWAP, however, must copy the data in both
  2822. directions.
  2823.    Understand that when using SWAP with conventional string arrays, the
  2824. data itself is not exchanged.  Rather, the four-byte string descriptors are
  2825. copied.  But because BASIC program modules store string data in different
  2826. segments, extra work is necessary to determine which descriptor goes with
  2827. which segment.  When near strings are being used, only six bytes are
  2828. exchanged, regardless of the length of the strings.  Four bytes hold the
  2829. descriptors, and two more store the back pointers.
  2830.  
  2831.  
  2832. SUMMARY
  2833. =======
  2834.  
  2835. This chapter explained many of the finer points of sorting and searching
  2836. all types of data in BASIC.  It began with sorting concepts using the
  2837. simple Bubble Sort as a model, and then went on to explain indexed and
  2838. multi-key sorts.  One way to implement a multi-key sort is by aligning the
  2839. key fields into adjacent TYPE components.  While there are some
  2840. restrictions to this method, it is fairly simple to implement and also
  2841. very fast.
  2842.    The Quick Sort algorithm was shown, and the SEEQSORT.BAS program on the
  2843. accompanying disk helps you to understand this complex routine by
  2844. displaying graphically the progress of the comparisons and exchanges as
  2845. they are performed.  Along the way you saw how a few simple modifications
  2846. to any string sort routine can be used to sort regardless of
  2847. capitalization, or based on only a portion of a string element.
  2848.    You also learned that writing a truly general sort routine that can
  2849. handle any type of data requires dealing exclusively with segment and
  2850. address pointers.  Here, assembly language routines are invaluable for
  2851. assisting you when performing the necessary comparisons and data exchanges. 
  2852. Although the actual operation of the assembly language routines will be
  2853. deferred until Chapter 12, such routines may easily be added to a BASIC
  2854. program using .LIB and .QLB libraries.
  2855.    I mentioned briefly the usefulness of packing and aligning data when
  2856. possible, as an aid to fast sorting.  In particular, dates can be packed
  2857. to only three bytes in Year/Month/Day order, and other data such as zip
  2858. codes can be stored in long integers.  Because numbers can be compared much
  2859. faster than strings, this helps the sorting routines operate more quickly.
  2860.    Array searching was also discussed in depth, and both linear and binary
  2861. search algorithms were shown.  As with the sorting routines, searching can
  2862. also employ UCASE$ and MID$ to search regardless of capitalization, or on
  2863. only a portion of each array element.  Two versions of the Soundex
  2864. algorithm were given, to let you easily locate names and other data based
  2865. on how they sound.
  2866.    Besides showing the more traditional searching methods, I presented
  2867. routines to determine the minimum and maximum values in a numeric array. 
  2868. I also discussed some of the ramifications involved when searching floating
  2869. point data, to avoid the inevitable rounding errors that might cause a
  2870. legitimate match to be ignored.
  2871.    Finally, some simple ways to insert and delete elements in both string
  2872. and numeric arrays were shown.  Although making direct assignments in a
  2873. loop is the most obvious way to do this, BASIC's often-overlooked SWAP
  2874. command can provide a significant improvement in speed.
  2875.    The next chapter will conclude this section about hands-on programming
  2876. by showing a variety of program optimization techniques.
  2877.