Linked List / Array Hybrid

Share your advanced PureBasic knowledge/code with the community.
Xombie
Addict
Addict
Posts: 898
Joined: Thu Jul 01, 2004 2:51 am
Location: Tacoma, WA
Contact:

Linked List / Array Hybrid

Post by Xombie »

Code updated For 5.20+

I figured I would finallly separate this out and put it together as an example in case people needed this ability.

In several of my projects I need to be able to nest arrays. Not possible to do. And structure arrays are static sizes anyway. Linked lists are global and can't be nested either.

So what's a guy to do?

Originally I made my own linked list code. It worked. Kinda. But it was bloated and there was a lot of issues in freeing memory and other things. Not as convenient as using PB's internal linked list and certainly not as convenient as arrays. So, I compromised. I started doing this in my xGrid project and liked it so much that I added it to my main project. The different to me is amazing now that I don't have to use my bloated linked list stuff :)

This is kind of a hybrid between a linked list and an array. It should have a lot less overhead than an array and be faster since you can immediately reference an item by it's index. You can also quickly remove an item and you can add an item in a presorted order if you like. It should also be extremely easy to add a quicksort routine to resort if you like. Of course, it's not quite as fast as an array but you can nest them as much as you like.

So, the downsides are - not as fast as arrays, each structure type has to be set up with it's own add/delete (and sometimes copy) functions.

For the example, I used a simple address book. Check it out, try it out and maybe it'll be useful in your project. It's saved me a lot of headaches in mine :) I added as many comments as I could without driving myself nuts (I hate commenting) so maybe it'll make sense when you read through it.

Code: Select all

    Structure s_ContactInfo
       FirstName.s
       LastName.s
       EmailAddress.s
    EndStructure
    ;
    Structure s_ContactGroup
       ;
       Named.s
       ;
       Contacts.l
       ; Pointer to the s_contactinfo 'array'
       Size.l
       ;
       Count.l
       ;
    EndStructure
    ;
    Structure s_AddressBook
       ContactGroup.l
       ; Pointer to the s_contacts 'array'
       Size.l
       Count.l
    EndStructure
    ;
    Global fAddress.s_AddressBook
    ; The main address book structure.
    ;- Procedures
    Procedure DestroyAddresses()
       ; Free the memory used by all contact groups and their contacts.

       ;
       *HoldGroup.s_ContactGroup
       ;
       If fAddress\ContactGroup
          ;
          *Position = fAddress\ContactGroup
          ;
          While *Position - fAddress\ContactGroup < fAddress\Size
             ;
             *HoldGroup = PeekL(*Position)
             ;
             If *HoldGroup\Contacts
                ;
                *PositionSub = *HoldGroup\Contacts
                ;
                While *PositionSub - *HoldGroup\Contacts < *HoldGroup\Size
                   ;
                   FreeMemory(PeekL(*PositionSub))
                   ;
                   *PositionSub + 4
                   ;
                Wend
                ;
                FreeMemory(*PositionSub)
                ;
             EndIf
             ;
             FreeMemory(*HoldGroup)
             ;
             *Position + 4
             ;
          Wend
          ;
          FreeMemory(fAddress\ContactGroup) : fAddress\ContactGroup = 0
          ;
       EndIf
       ;
    EndProcedure
    Procedure.l AddContactGroup(Named.s)
       ;

       ;
       HoldString.s : HoldText.s
       ;
       *HoldGroup.s_ContactGroup
       ;
       If fAddress\ContactGroup
          ; Check if the contact group array was previously created.
          HoldString = LCase(Named)
          ; Store the lower-case equivalent of the new contact group name.
          *Position = fAddress\ContactGroup
          ; Set *Position to the address of the first contact group structure.
          While *Position - fAddress\ContactGroup < fAddress\Size
             ; Loop through all of the contact groups based on the stored size.
             *HoldGroup = PeekL(*Position)
             ; Fill the group structure with the current contact group.
             HoldText = LCase(*HoldGroup\Named)
             ; Store the lowercase equivalent of the current contact group's name.
             If HoldText = HoldString
                ; The passed contact group already exists so there's no need to add it.
                ProcedureReturn
                ; Exit.
             ElseIf HoldText > HoldString
                ; Adding contact groups in presorted order by name so check if the current contact group's name is "larger" than the new one.  Insert before this group if so.
                NewArray = AllocateMemory(fAddress\Size + 4)
                ; Allocate enough space to store the new contact group structure pointer.
                CopyMemory(fAddress\ContactGroup, NewArray, *Position - fAddress\ContactGroup)
                ; Copy the contact groups that exist before the insertion point.
                *HoldGroup = AllocateMemory(SizeOf(s_ContactGroup))
                ; Allocate space for the new contact group structure.
                *HoldGroup\Named = Named
                ; Store the name of the new contact group.
                PokeL(NewArray + (*Position - fAddress\ContactGroup), *HoldGroup)
                ; Store the pointer to the new contact group.
                CopyMemory(*Position, NewArray + (*Position - fAddress\ContactGroup) + 4, fAddress\Size - (*Position - fAddress\ContactGroup))
                ; Copy the contact groups that exist after the insertion point.
                FreeMemory(fAddress\ContactGroup) : fAddress\ContactGroup = NewArray
                ; Destroy the original contact group array and reassign it to the new array.
                fAddress\Count + 1
                ; Increase the count since a new contact group was added.
                fAddress\Size + 4
                ; Increase the size of the contact group array for boundary checking.
                ProcedureReturn *HoldGroup
                ; Return the pointer to the newly allocated contact group structure.
             EndIf
             ;
             *Position + 4
             ; Increment to the next contact group structure.
          Wend
          ;
          NewArray = AllocateMemory(fAddress\Size + 4)
          ; Allocate an additional 4 bytes for the new contact group.
          CopyMemory(fAddress\ContactGroup, NewArray, fAddress\Size)
          ; Copy the existing contact group information into the newly allocated memory.
          *HoldGroup = AllocateMemory(SizeOf(s_ContactGroup))
          ; Allocate space for the contact group structure.
          *HoldGroup\Named = Named
          ; Store the new of the contact group.
          PokeL(NewArray + fAddress\Size, *HoldGroup)
          ; Insert the address for the new contact group structure.
          FreeMemory(fAddress\ContactGroup) : fAddress\ContactGroup = NewArray
          ; Destroy the memory used to store the original structures and then reassign the new memory to the old array.
       Else
          ; No contact group set up yet.  Create the initial contact group.
          fAddress\ContactGroup = AllocateMemory(4)
          ; Allocate 4 bytes to store the first structure pointer.
          *HoldGroup = AllocateMemory(SizeOf(s_ContactGroup))
          ; Allocate enough space for the contact group structure.
          *HoldGroup\Named = Named
          ; Store the name of the new contact group.
          PokeL(fAddress\ContactGroup, *HoldGroup)
          ; Insert the address for the first contact group structure.
       EndIf
       ;
       fAddress\Count + 1
       ; Increment the contact group count.
       fAddress\Size + 4
       ; Contact groups are stored by structure pointers - 4 bytes to store the LONG value pointer.  Increment for the new structure.
       ProcedureReturn *HoldGroup
       ;
    EndProcedure
    Procedure.l DeleteContactGroup(Index.l)
       ; Delete a contact group based on it's index.

       ;
       *HoldGroup.s_ContactGroup
       ;
       If fAddress\Count = 0 Or Index > fAddress\Count : ProcedureReturn : EndIf
       ; Check to make sure contact groups exist and that the index is not outside of the count.
       *HoldGroup = PeekL(fAddress\ContactGroup + (Index * 4))
       ; Fill the contact group structure based on the index.  This works because the first item in the contact group array
       ; is at fAddress\ContactGroup.  All of the contact groups are stored as 4 bytes so the next one is at 'fAddress\ContactGroup + 4'
       If *HoldGroup\Contacts
          ; Check if a contact exists for the contact group.
          *Position = *HoldGroup\Contacts
          ; Store the pointer to the first contact group structure.
          While *Position - *HoldGroup\Contacts < *HoldGroup\Size : FreeMemory(PeekL(*Position)) : *Position + 4 : Wend
          ; Destroy the memory used by the contact structures.
          FreeMemory(*HoldGroup\Contacts)
          ; Destroy the memory used to store the structures.
       EndIf
       ;
       If fAddress\Count = 1
          ; Only one contact group currently exists for the address book.
          FreeMemory(*HoldGroup)
          ; Destroy the structure used to store the contact group.
          FreeMemory(fAddress\ContactGroup)
          ; Destroy the memory used to store the structure.
          fAddress\ContactGroup = 0
          ; Store a null pointer for the contact group array for later checking.
          fAddress\Size = 0 : fAddress\Count = 0
          ; There are no contact groups now so reset the size and count.
          ProcedureReturn
          ; Exit.
       EndIf
       ;
       NewArray = AllocateMemory(fAddress\Size - 4)
       ; Create a new array to store the remaining contact groups.
       *NewPosition = NewArray
       ; Point to the new array.  We'll fill this with the existing structures.
       *Position = fAddress\ContactGroup
       ; Set the position to the first contact group.
       While *Position - fAddress\ContactGroup < fAddress\Size
          ; Loop through the existing contact groups.
          If *Position <> *HoldGroup
             ; Make sure the current contact group is not the one being removed.
             PokeL(*NewPosition, PeekL(*Position))
             ; Insert the existing contact group into the new array.
             *NewPosition + 4
             ; Point to the next available space in the new array.
          EndIf
          ;
          *Position + 4
          ; Increment to the next contact group.
       Wend
       ;
       FreeMemory(*HoldGroup)
       ; Destroy the contact group.
       FreeMemory(fAddress\ContactGroup) : fAddress\ContactGroup = NewArray
       ; Destroy the memory used by the previous contact groups and reassign it to the new array.
       fAddress\Count - 1 : fAddress\Size - 4
       ; Decrement the contact group count and size.
    EndProcedure
    Procedure CopyContact(*CopyTo.s_ContactInfo, *CopyFrom.s_ContactInfo)
       ; Copy information from one contact info structure to another.
       *CopyTo\FirstName = *CopyFrom\FirstName
       *CopyTo\LastName = *CopyFrom\LastName
       *CopyTo\EmailAddress = *CopyFrom\EmailAddress
       ;
    EndProcedure
    Procedure.l AddContact(*HoldGroup.s_ContactGroup, *Contact.s_ContactInfo, AtIndex.l)
       ; Pass -1 AtIndex to add the line in sort order.  Otherwise, the line will be inserted at the index point.

       ;
       HoldCurrentLastName.s : HoldCurrentFirstName.s
       ;
       HoldNewLastName.s : HoldNewFirstName.s
       ;
       *HoldContact.s_ContactInfo
       ;
       If *HoldGroup\Contacts
          ; Check if contacts exist for the passed contact group.
          HoldNewLastName = LCase(*Contact\LastName) : HoldNewFirstName = LCase(*Contact\FirstName)
          ; Store the lower-case equivalents of the first and last name for the new contact.
          *Position = *HoldGroup\Contacts
          ; Point to the first contact in the group.
          While *Position - *HoldGroup\Contacts < *HoldGroup\Size
             ; Loop through the contacts in the group.
             *HoldContact = PeekL(*Position)
             ; Fill the contact structure with the current contact.
             HoldCurrentLastName = LCase(*HoldContact\LastName) : HoldCurrentFirstName = LCase(*HoldContact\FirstName)
             ; Store the lower-case first and last name for the current contact.
             If HoldNewFirstName = HoldCurrentFirstName And HoldNewLastName = HoldCurrentLastName
                ; The contact already exists.
                ProcedureReturn
                ; Exit.
             ElseIf AtIndex = iCount Or (AtIndex = -1 And (HoldNewFirstName = HoldCurrentFirstName And HoldNewLastName = HoldCurrentLastName))
                ; Found the insertion point.  Add the new line here.  This will be triggered if a specific index was passed or
                ; if no specific index was passed and the current name is "larger" than the new contact name.
                NewArray = AllocateMemory(*HoldGroup\Size + 4)
                ; Allocate enough space to store the new contact contact structure.
                CopyMemory(*HoldGroup\Contacts, NewArray, *Position - *HoldGroup\Contacts)
                ; Copy the contacts before the insertion point.
                *HoldContact = AllocateMemory(SizeOf(s_ContactInfo))
                ; Allocate room for the new contact info structure.
                CopyContact(*HoldContact, *Contact)
                ; Copy the new contact into the newly allocated contact space.
                PokeL(NewArray + (*Position - *HoldGroup\Contacts), *HoldContact)
                ; Insert the structure pointer.
                CopyMemory(*Position, NewArray + (*Position - *HoldGroup\Contacts) + 4, *HoldGroup\Size - (*Position - *HoldGroup\Contacts))
                ; Copy the contacts after the insertion point.
                FreeMemory(*HoldGroup\Contacts) : *HoldGroup\Contacts = NewArray
                ; Free the memory used to store the previous contacts and reassign using the new array.
                *HoldGroup\Count + 1
                ; Increment the contact count to show the new contact.
                *HoldGroup\Size + 4
                ; Increment the size used to store the contact structures.
                ProcedureReturn *HoldContact
                ; Return the pointer to the new contact structure.
             EndIf
             ;
             iCount + 1
             ; Increment the position counter.
             *Position + 4
             ; Increment to the next contact.
          Wend
          ; The following code block is similar to the above.  The only difference is the new contact is added to the end.
          NewArray = AllocateMemory(*HoldGroup\Size + 4)
          ;
          CopyMemory(*HoldGroup\Contacts, NewArray, *HoldGroup\Size)
          ;
          *HoldContact = AllocateMemory(SizeOf(s_ContactInfo))
          ;
          CopyContact(*HoldContact, *Contact)
          ;
          PokeL(NewArray + *HoldGroup\Size, *HoldContact)
          ;
          FreeMemory(*HoldGroup\Contacts) : *HoldGroup\Contacts = NewArray
          ;
          *HoldGroup\Count + 1
          ;
          *HoldGroup\Size + 4
          ;
          ProcedureReturn *HoldContact
          ;
       Else
          ; No contacts exist for the contact group.  Create the first contact for the group.  The procedure is somewhat similar to the above code.
          *HoldGroup\Contacts = AllocateMemory(4)
          ;
          *HoldContact = AllocateMemory(SizeOf(s_ContactInfo))
          ;
          CopyContact(*HoldContact, *Contact)
          ;
          PokeL(*HoldGroup\Contacts, *HoldContact)
          ;
          *HoldGroup\Count + 1
          ; There is now one line in the section.  One based count.
          *HoldGroup\Size + 4
          ; Four bytes are used to store the pointer to the line structure.
          ProcedureReturn *HoldContact
          ;
       EndIf
       ;
    EndProcedure
    Procedure DeleteContact(*HoldGroup.s_ContactGroup, Index.l)
       ; Delete a contact from the contact group - based on the contact's index.

       ;
       *HoldContact.s_ContactInfo
       ;
       If *HoldGroup\Count = 0 Or Index > *HoldGroup\Count : ProcedureReturn : EndIf
       ; Check to make sure contacts exist and that the passed index is not larger than the number of items.
       *HoldContact = PeekL(*HoldGroup\Contacts + (Index * 4))
       ; Fill the contact structure based on it's index.
       If *HoldGroup\Count = 1
          ; Deleting the only contact in the group.
          FreeMemory(*HoldContact)
          ; Free the memory used to store the contact information.
          FreeMemory(*HoldGroup\Contacts)
          ; Free the memory used to store the contact structure.
          *HoldGroup\Count = 0 : *HoldGroup\Size = 0
          ; Reset the count and size to 0 to reflect no more contacts in the group.
          ProcedureReturn
          ; Exit.
       EndIf
       ;
       NewArray = AllocateMemory(*HoldGroup\Size - 4)
       ; Allocate space for the new array.  The new size is four bytes smaller to reflect the removed contact.
       *NewPosition = NewArray
       ; Set a pointer to the new array.
       *Position = *HoldGroup\Contacts
       ; Set a pointer to the original contact array for the passed contact group.
       While *Position - *HoldGroup\Contacts < *HoldGroup\Size
          ; Loop through the existing contacts.
          If *Position <> *HoldContact
             ; Check if the current contact is the contact being removed.
             PokeL(*NewPosition, PeekL(*Position))
             ; Insert the old contact structure pointer into the new array.
             *NewPosition + 4
             ; Point to the next available space in the new array.
          EndIf
          ;
          *Position + 4
          ; Point to the next existing contact.
       Wend
       ;
       FreeMemory(*HoldContact)
       ; Erase the memory used to store the contact.
       FreeMemory(*HoldGroup\Contacts) : *HoldGroup\Contacts = NewArray
       ; Free the memory used by the previous contacts and reassign to the new array.
       *HoldGroup\Count - 1 : *HoldGroup\Size - 4
       ; Decrement the contact count for the group.
    EndProcedure
    ;
    *TestGroup.s_ContactGroup
    ;
    TestContact.s_ContactInfo
    ;
    *Test.s_ContactInfo
    ;
    *TestGroup = AddContactGroup("Home")
    ; Add a contact group and store the pointer in *TestGroup.
    TestContact\FirstName = "Sam" : TestContact\LastName = "Shults" : TestContact\EmailAddress = "xombie@seijin.net"
    ; Create a contact.
    *Test = AddContact(*TestGroup, @TestContact, -1)
    ; Add the contact to the 'Home' contact group. 
    TestContact\FirstName = "Joe" : TestContact\LastName = "Internet" : TestContact\EmailAddress = "joe@internet.com"
    ; Create a new contact.
    *Test = AddContact(*TestGroup, @TestContact, -1)
    ; Add it to the 'Home' group.
    *TestGroup = AddContactGroup("Work")
    ; Create a new group.
    TestContact\FirstName = "Jane" : TestContact\LastName = "Doe" : TestContact\EmailAddress = "jane@gmail.com"
    ; Create a new contact.
    *Test = AddContact(*TestGroup, @TestContact, -1)
    ; Add the contact to the 'Work' group.
    ;/
    *TestGroup = PeekL(fAddress\ContactGroup + (0 * 4))
    ; This is where the array like ability is tested.  The above line will use the group stored at index 0.  Since the structures are stored
    ; in order you simply have to skip 4 bytes to get to whatever index you want.  Index 1 would be 'PeekL(fAddress\ContactGroup + (1 * 4))'
    ; You can set this up as a loop easily - see the AddContact() function for an example.
    Debug "The '"+*TestGroup\Named+"' Group."
    ;
    *Test = PeekL(*TestGroup\Contacts + (1 * 4))
    ; Same deal for the sub array.  This will pull the contact at index 1 in the previous contact group.  Should be 'Joe Internet'.
    Debug *Test\FirstName + " " +*Test\LastName + " - " + *Test\EmailAddress
    ;
    *Test = PeekL(*TestGroup\Contacts)
    ; This should pull the contact at index 0.
    Debug *Test\FirstName + " " +*Test\LastName + " - " + *Test\EmailAddress
    ;
    *Test\FirstName = "Samantha" : *Test\LastName = "Schultz" : *Test\EmailAddress = "samantha@seijin.net"
    ; Since the pointer points to the actual data in memory, it can be updated immediately.  We'll check the group again to prove it.
    *Test = PeekL(*TestGroup\Contacts + (1 * 4))
    ; Same deal for the sub array.  This will pull the contact at index 1 in the previous contact group.  Should be 'Joe Internet'.
    Debug *Test\FirstName + " " +*Test\LastName + " - " + *Test\EmailAddress
    ;
    *Test = PeekL(*TestGroup\Contacts)
    ; This should pull the contact at index 0.
    Debug *Test\FirstName + " " +*Test\LastName + " - " + *Test\EmailAddress
    ;
    *TestGroup = PeekL(fAddress\ContactGroup + (1 * 4))
    ; Test the second contact group.
    Debug "The '"+*TestGroup\Named+"' Group."
    ;
    *Test = PeekL(*TestGroup\Contacts)
    ; Should be only one contact in this group.
    Debug *Test\FirstName + " " +*Test\LastName + " - " + *Test\EmailAddress
    ;
    ;/
    DestroyAddresses()
    ;
    End
rsts
Addict
Addict
Posts: 2736
Joined: Wed Aug 24, 2005 8:39 am
Location: Southwest OH - USA

Post by rsts »

Wow - this looks great.

If this works out, I might not even need a data base for some of the things I'm working on.

Thanks, once again, for some great code Xombie.

cheers
Post Reply