How would you shuffle a doubly linked list?

147 views Asked by At

Im trying to shuffle a doubly linked list without changing it's pointers. So far my idea to do this is to have a position integer value that gets assigned randomly to each struct in the doubly linked list. I want to traverse the doubly linked list backwards and forwards in accordance to the order their position values were assigned.

Lets say the order of the position values in the list goes like 1,9,8,10,4,5,3,7,6,2. How would I traverse the doubly linkedlist so that I access 1,2,3,4... and so on in order?

Here is the struct:

typedef struct record{
    char artist[50];
    char albumTitle[50];
    char songTitle[50];
    char genre[50];
    Duration songLength;
    int timesPlayed;
    int rating;
    int position;
}Record;

Here is my node struct:

typedef struct node{
    struct node *pPrev;
    struct node *pNext;
    Record record;
}Node;

Here is my attempt so far:

void shuffleRecords(Node *pList){
    int recordAmount = countRecords(pList);
    Node *pCurr = pList;
    Record recordArr[recordAmount];
    int assignedPositions[recordAmount];
    int index=0;
    Record tempRecord;

    //initialize assigned positions
    for(int i = 0; i < recordAmount; i++){
        assignedPositions[i] = 0;
    }


    while(pCurr != NULL){
        recordArr[index] = pCurr->record;
        index++;
        pCurr=pCurr->pNext;
    }
    for(int i = 0; i < recordAmount;i++){
        int newPosition;
        do{
            newPosition = rand() % recordAmount + 1;
        }while(assignedPositions[newPosition-1] == 1);
        recordArr[i].position = newPosition;
        assignedPositions[newPosition-1] = 1;
        printf("%s|%s|%s|%s|%d:%d|%d|%d POSITION: %d\n",recordArr[i].artist,recordArr[i].albumTitle,
               recordArr[i].songTitle,recordArr[i].genre,
               recordArr[i].songLength.minutes,recordArr[i].songLength.seconds,
               recordArr[i].timesPlayed,recordArr[i].rating,recordArr[i].position);
    }
    for(int i = 0; i < recordAmount;i++){
        Node *pTraverse=pList;
        int traverseIndex = i+1;
        if(traverseIndex % 2 == 0){//traverse forwards
            while(pTraverse != NULL && pTraverse->record.position != traverseIndex){
                pTraverse=pTraverse->pNext;
            }
        }else{//traverse backwards
            while(pTraverse != NULL && pTraverse->record.position != traverseIndex){
                pTraverse=pTraverse->pPrev;
            }
        }
        if (pTraverse != NULL){
            printf("%s\n",pTraverse->record.songTitle);
        }


    }

}

Here is an example of the position values:

Brooks, Garth|FRESH HORSES|The Old Stuff|Country|2:57|11|2 POSITION: 8
Swift, Taylor|RED|Stay Stay Stay|Pop|4:42|5|1 POSITION: 1
Adele|25|Remedy|Pop|4:11|24|4 POSITION: 4
Eminem|SHADYXV|Vegas|Rap|3:37|8|3 POSITION: 7
Bieber, Justin|PURPOSE|No Sense|Pop|4:12|6|1 POSITION: 5
Perri, Christina|HEAD OF HEART|Trust|Pop|2:35|3|5 POSITION: 3
Drake|YOU WELCOME|The Motto|Rap|4:13|7|4 POSITION: 9
Drake|NOTHING WAS THE SAME|Own it|Rap|3:23|3|3 POSITION: 2
Swift, Taylor|1989|Shake it Off|Pop|3:35|12|3 POSITION: 6
2

There are 2 answers

0
Chris On

If you define a few functions to create nodes, get the length of a list, and get a node at a specific index, this is made fairly straightforward.

A simplified example with just an int in each node, and a singly-linked list, but you should be able to extrapolate the logic. Caveat: this scales poorly as it is an O(n^2) algorithm for a linked list. For large sample sizes, you'd be better off converting to and from arrays which have constant-time access based on index.

#include <stdlib.h>
#include <stdio.h>
#include <time.h>

typedef struct ListNode {
    int val;
    struct ListNode *next;
} ListNode;

ListNode *make_node(int val) {
    ListNode *n = malloc(sizeof(*n));
    n->val = val;
    n->next = NULL;

    return n;
}

size_t length(ListNode *n) {
    size_t len = 0;
    for (ListNode *c = n; c; len++) {
        c = c->next;
    }

    return len;
}

ListNode *get_at_idx(ListNode *n, size_t i) {
    ListNode *c = n;
    for (size_t j = 0; c && j < i; j++) 
        c = c->next;

    return c;
}

void shuffle(ListNode *n) {
    size_t len = length(n);
    
    for (size_t i = 0; i < len - 1; i++) {
        ListNode *cur_node = get_at_idx(n, i);

        int rand_idx = rand() % (len - i - 1) + i + 1;

        ListNode *r = get_at_idx(n, rand_idx);

        int temp = r->val;
        r->val = cur_node->val;
        cur_node->val = temp;
    }
}

int main(void) {
    srand(time(NULL));

    ListNode *n = make_node(1);
    n->next = make_node(2);
    n->next->next = make_node(3);
    n->next->next->next = make_node(4);

    shuffle(n);

    for (ListNode *c = n; c; c = c->next) {
        printf("%d\n", c->val);
    }    
}

Result:

% ./a.out                 
3
1
4
2
% ./a.out
4
1
2
3
% ./a.out
2
3
4
1
% ./a.out
3
4
2
1
0
arfneto On

This makes sense just as an assignment for students, not for some practical issue.

  • linked lists are not suited for these kind of thing. Access by position is almost never even implemented. It is very slow.
  • the process itself is destructive, so if it gets interrupted the original link order are lost for good.

But as an exercise of pointer manipulation it is ok, but with no practical meaning beyond this.

Anyway, to shuffle a list is the same as sorting by some field of the record. This last is a more common assignment. As is common the assignment to create a new list with the same data but sorted the links by other criteria.

I will show below an iterative and fast way to do that. I will use you data just for familiarity. It is simple and worked first run.

TL;DR

output of the example

  list created

  ***** with the test data *****
  '0he Old Stuff' 2:57 from 'Brooks, Garth' in 'FRESH HORSES' [Pop] Played 11 times, rating 2 pos 8
  '1tay Stay Stay' 4:42 from 'Swift, Taylor' in 'RED' [Pop] Played 5 times, rating 1 pos 1
  '2emedy' 4:11 from 'Adele' in '25' [Pop] Played 24 times, rating 4 pos 4
  '3egas' 3:37 from 'Eminem' in 'SHADYXV' [Rap] Played 8 times, rating 3 pos 7
  '4o Sense' 4:12 from 'Bieber, Justin' in 'PURPOSE' [Pop] Played 6 times, rating 1 pos 5
  '5rust' 2:35 from 'Perri, Christina' in 'HEAD OF HEART' [Pop] Played 3 times, rating 3 pos 5
  '6he Motto' 4:13 from 'Drake' in 'YOU WELCOME' [Rap] Played 7 times, rating 4 pos 9
  '7wn it' 3:23 from 'Drake' in 'NOTHING WAS THE SAME' [Rap] Played 3 times, rating 3 pos 2
  '8hake it Off' 3:35 from 'Swift, Taylor' in '1989' [Pop] Played 12 times, rating 3 pos 6

[-----]

  final order for nodes:9 values: 6 3 0 5 2 4 7 8 1

  ***** after shuffle *****
  '6he Motto' 4:13 from 'Drake' in 'YOU WELCOME' [Rap] Played 7 times, rating 4 pos 9
  '3egas' 3:37 from 'Eminem' in 'SHADYXV' [Rap] Played 8 times, rating 3 pos 7
  '0he Old Stuff' 2:57 from 'Brooks, Garth' in 'FRESH HORSES' [Pop] Played 11 times, rating 2 pos 8
  '5rust' 2:35 from 'Perri, Christina' in 'HEAD OF HEART' [Pop] Played 3 times, rating 3 pos 5
  '2emedy' 4:11 from 'Adele' in '25' [Pop] Played 24 times, rating 4 pos 4
  '4o Sense' 4:12 from 'Bieber, Justin' in 'PURPOSE' [Pop] Played 6 times, rating 1 pos 5
  '7wn it' 3:23 from 'Drake' in 'NOTHING WAS THE SAME' [Rap] Played 3 times, rating 3 pos 2
  '8hake it Off' 3:35 from 'Swift, Taylor' in '1989' [Pop] Played 12 times, rating 3 pos 6
  '1tay Stay Stay' 4:42 from 'Swift, Taylor' in 'RED' [Pop] Played 5 times, rating 1 pos 1

[-----]

  list deleted

I changed the code to put the original position of the song in the first letter of the song, and printed before the shuffle the expected order, so the program tests itself. It is just 1 line of code in the factory function.

main() for these test

#include <stdio.h>
#include <stdlib.h>

#include "List.h"
#include "stuff.h"

int main(void)
{
   List*  my_list = so_create_list();
   unsigned N       = 9;  // 9 records on sample data
   for (unsigned i = 0; i < N; i += 1)
   {
       Info* one = so_factory_info();
       so_insert_back(one, my_list);
       free(one);
   }
   so_print_list(
       my_list, "\n    ***** with the test data *****\n");
   so_shuffle_nodes(my_list);
   so_print_list(
       my_list, "\n    ***** after shuffle *****\n");
   so_delete_list(my_list);
   return 0;
};

This is easy to read:

  • a list is created

  • so_factory_info() is called 9 times and gets some data records. Could the thousands or just 2.

  • so_insert_back() inserts them at the end of the list

  • so_print_list() does as expected and display the data before shuffling

  • so_shuffle_nodes() does its thing

  • so_print_list() is called again and displays the list after shuffling

  • the list is destroyed

    I will show am iterative and fast way of writing this. It is just a matter of writing 4 or 5 functions, apart from the linked list normal functions. The complete code is in this repository

a bonus: sorting the list by any criteria

The same idea can be used to sort the list. Here is a program that

  • sorts the list by position of the song
  • sorts by artist

output

    list created

    ***** with the test data *****
    '0he Old Stuff' 2:57 from 'Brooks, Garth' in 'FRESH HORSES' [Pop] Played 11 times, rating 2 pos 8
    '1tay Stay Stay' 4:42 from 'Swift, Taylor' in 'RED' [Pop] Played 5 times, rating 1 pos 1
    '2emedy' 4:11 from 'Adele' in '25' [Pop] Played 24 times, rating 4 pos 4
    '3egas' 3:37 from 'Eminem' in 'SHADYXV' [Rap] Played 8 times, rating 3 pos 7
    '4o Sense' 4:12 from 'Bieber, Justin' in 'PURPOSE' [Pop] Played 6 times, rating 1 pos 5
    '5rust' 2:35 from 'Perri, Christina' in 'HEAD OF HEART' [Pop] Played 3 times, rating 3 pos 5
    '6he Motto' 4:13 from 'Drake' in 'YOU WELCOME' [Rap] Played 7 times, rating 4 pos 9
    '7wn it' 3:23 from 'Drake' in 'NOTHING WAS THE SAME' [Rap] Played 3 times, rating 3 pos 2
    '8hake it Off' 3:35 from 'Swift, Taylor' in '1989' [Pop] Played 12 times, rating 3 pos 6

[-----]


    ***** after sort on position *****
    '1tay Stay Stay' 4:42 from 'Swift, Taylor' in 'RED' [Pop] Played 5 times, rating 1 pos 1
    '7wn it' 3:23 from 'Drake' in 'NOTHING WAS THE SAME' [Rap] Played 3 times, rating 3 pos 2
    '2emedy' 4:11 from 'Adele' in '25' [Pop] Played 24 times, rating 4 pos 4
    '4o Sense' 4:12 from 'Bieber, Justin' in 'PURPOSE' [Pop] Played 6 times, rating 1 pos 5
    '5rust' 2:35 from 'Perri, Christina' in 'HEAD OF HEART' [Pop] Played 3 times, rating 3 pos 5
    '8hake it Off' 3:35 from 'Swift, Taylor' in '1989' [Pop] Played 12 times, rating 3 pos 6
    '3egas' 3:37 from 'Eminem' in 'SHADYXV' [Rap] Played 8 times, rating 3 pos 7
    '0he Old Stuff' 2:57 from 'Brooks, Garth' in 'FRESH HORSES' [Pop] Played 11 times, rating 2 pos 8
    '6he Motto' 4:13 from 'Drake' in 'YOU WELCOME' [Rap] Played 7 times, rating 4 pos 9

[-----]


    ***** after sort on ARTIST *****
    '2emedy' 4:11 from 'Adele' in '25' [Pop] Played 24 times, rating 4 pos 4
    '4o Sense' 4:12 from 'Bieber, Justin' in 'PURPOSE' [Pop] Played 6 times, rating 1 pos 5
    '0he Old Stuff' 2:57 from 'Brooks, Garth' in 'FRESH HORSES' [Pop] Played 11 times, rating 2 pos 8
    '7wn it' 3:23 from 'Drake' in 'NOTHING WAS THE SAME' [Rap] Played 3 times, rating 3 pos 2
    '6he Motto' 4:13 from 'Drake' in 'YOU WELCOME' [Rap] Played 7 times, rating 4 pos 9
    '3egas' 3:37 from 'Eminem' in 'SHADYXV' [Rap] Played 8 times, rating 3 pos 7
    '5rust' 2:35 from 'Perri, Christina' in 'HEAD OF HEART' [Pop] Played 3 times, rating 3 pos 5
    '1tay Stay Stay' 4:42 from 'Swift, Taylor' in 'RED' [Pop] Played 5 times, rating 1 pos 1
    '8hake it Off' 3:35 from 'Swift, Taylor' in '1989' [Pop] Played 12 times, rating 3 pos 6

[-----]

    list deleted

the sort function

This is a simple sort function for the list. It should work for any list, any criteria. A cmp function is passed and makes the sort generic, as in qsort. It is simple because functions for locate and swap node info are available and because comparison is passed as a function.

int so_sort_nodes(List* list, int (*cmp)(void*, void*))
{
    if (list == NULL) return -1;
    if (list->size < 2) return -2;
    if (cmp == NULL) return -3;
    for (int i = 0; i < list->size - 1; i += 1)
        for (int j = 0; j < list->size - i - 1; j += 1)
        {
            Node* one = so_locate_at(list, j);
            if ( cmp((void*)one, (void*)one->next) < 0)
                so_swap_info(one, one->next);
        }
    return 0;
}

main.c for the sorting example

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "List.h"
#include "stuff.h"

int so_cmp_info_artist(void* a, void* b);

int main(void)
{
    List*    my_list = so_create_list();
    unsigned N       = 9;  // 9 records on sample data
    for (unsigned i = 0; i < N; i += 1)
    {
        Info* one = so_factory_info();
        so_insert_back(one, my_list);
        free(one);
    }
    so_print_list(
        my_list, "\n    ***** with the test data *****\n");
    so_sort_nodes(my_list, so_cmp_info);
    so_print_list(
        my_list,
        "\n    ***** after sort on position *****\n");
    so_sort_nodes(my_list, so_cmp_info_artist);
    so_print_list(
        my_list,
        "\n    ***** after sort on ARTIST *****\n");
    so_delete_list(my_list);
    return 0;
};

int so_cmp_info_artist(void* a, void* b)
{
    if (a == NULL) return 0;
    if (b == NULL) return 0;
    Node* one   = (Node*)a;
    Node* other = (Node*)b;
    return strcmp(
        other->info->artist, one->info->artist);
};

And it is easy to see how a new comparison function can be added on the fly: it is just a name.

All code is in this repository

back to the problem: the linked list

    typedef struct record
    {
        char     artist[50];
        char     albumTitle[50];
        char     songTitle[50];
        char     genre[50];
        Duration songLength;
        int      timesPlayed;
        int      rating;
        int      position;
    } Record;

    typedef struct node
    {
        struct node* pPrev;
        struct node* pNext;
        Record       record;
    } Node;

But where is the list? In this model we have no list. Also we have no Duration type.

This seems not to be a good model. As closer to the data the abstraction is, the simpler the code gets.

A linked list is a --- possibly empty --- collection of nodes. Each node points to --- or contains one --- unit of data, a record. This kind of data structure is called container in C++ and collection in java, for a reason. It is the same with sets, dictionaries, stacks, queues and arrays: sets of things. Your model should have a List. Also containers have metadata, information about the structure itself, for reference, identification, control, serialization, all implementation defined. As a minimum you should keep a size counter, and it is clear in your case since you need to shuffle the data records, so you need the size info.

building the implementation

the List

// operations on List
typedef struct st_node
{
    struct st_node* prev;  // links
    struct st_node* next;

    Info* info;  // data
} Node;

typedef struct
{
    size_t size;
    Node*  tail;
    Node*  head;
} List;  // a (possibly empty) list of nodes

List* so_create_list();
List* so_delete_list(List*);
int   so_insert_back(Info*, List*);
int   so_insert_front(Info*, List*);
int   so_print_list(List*, const char* msg);

This is a generic minimal List. Each node has a pointer to an Info thing.

This program here is for sure not the first program you write using lists so you should have some previous code for lists. We do not start from zero every time. Or at least we should not to.

The list here is a list of Info, a generic thing. If we want to preserve Record name we need just a typedef like

typedef Record Info;

So we do not need to change the code for list...

enhancing List

We need some non-standard operations:

  • access by position.
  • swap the data records of 2 nodes
  • shuffle the node's data without touching the pointers.

Let us consider

Node* so_locate_at(List* list, size_t pos);
int   so_swap_info(Node* one, Node* other);
int   so_shuffle_nodes(List*);

Why?

  • we need to access the list as an array, by position, so we a Node address back from so_locate_at is a good fit
  • in order to shuffle or sort the nodes we need a way to swap 2 of them. The nodes does not know if they belong to a list: they just point to data.
  • so_shuffle_nodes() is our target operation.

some operations on Record

Since we have a list of Record and List do not know about Record at all, we need to know what to do with Record without knowing its contents. By doing this we can use the List for any content by just writing these minimum operations. A list is a list and does not depend on what it it listing.

From now on, Record is Info since the code for List uses Info* as the data in each node.

Each node will have a pointer to Info but the data in the list must be owned by the list. It is essential, since the user can do nasty things:

  • use a pointer to the same Info many times. Then when deleting a list how to free memory only once?
  • pass a pointer to a static Record so it can not be free'd anyway
  • we need to know how to display an Info on screen, since we do not know its contents
  • we need to know how to delete an Info since it can have pointers inside it
  • for testing, we need a controlled way to generate data records.

So consider

    Info* so_copy_info(Info*);
    Info* so_delete_info(Info*);
    Info* so_factory_info(void);
    int   so_print_info(Info*, const char* msg);

In other languages these could be called copy constructor, destructor, constructor (sort of), and toString operations.

Why this?

If we can write this functions for each Info record we have we need to touch the List functions. In fact they could receive just function pointers to this operations, like in a C++ class.

putting things together

So if we had the previous List code we need now:

  • the 2 new operations for the list. Nothing special, just record access by position and node swapping.
  • the operations on the Record, for sure. Nothing fancy, since Record does not have pointers. For every new content of List we need the 3 obvious functions, and a factory one for testing is always handy

Record.h the record prototype

#include <stdio.h>
typedef const char Duration[10];  // "01:33:33"

typedef struct st_record
{
    char     artist[50];
    char     albumTitle[50];
    char     songTitle[50];
    char     genre[50];
    Duration songLength;
    int      timesPlayed;
    int      rating;
    int      position;
} Record;

typedef Record Info;

List.h the list stuff

The previous code for List used Info as the data record name.

// https://stackoverflow.com/questions/77983967/how-would-you-shuffle-a-doubly-linked-list
#include <stdio.h>

#include "Record.h"
// operations on data records
Info* so_copy_info(Info*);
int   so_cmp_info(void*, void*);
Info* so_delete_info(Info*);
Info* so_factory_info(void);
int   so_print_info(Info*, const char* msg);

// operations on List
typedef struct st_node
{
    struct st_node* prev;  // links
    struct st_node* next;

    Info* info;  // data
} Node;

typedef struct
{
    size_t size;
    Node*  tail;
    Node*  head;
} List;  // a (possibly empty) list of nodes

List* so_create_list(void);
List* so_delete_list(List*);
int   so_insert_back(Info*, List*);
int   so_insert_front(Info*, List*);
int   so_print_list(List*, const char* msg);
int   so_sort_nodes(List*, int (*f)(void*, void*));
int   so_shuffle_nodes(List*);

writing the new functions

locate a node in a certain position

Since we are shuffling data from the nodes we need to access them by position

// return a pointer to a copy of the record
// at such 'pos', or 'NULL'. 'pos'starts at 0
Node* so_locate_at(List* list, size_t pos)
{
    if (list == NULL) return NULL;
    if (list->size < 1 + pos) return NULL;
    Node* nd = list->head;
    for (size_t i = 0; i < pos; i += 1) nd = nd->next;
    return nd;
}

8 lines is not bad. It returns a pointer to an actual node, and should not be used out of this context, because a mistake can ruin the list ou crash the program. Returning Node* makes easier to swap the records.

swap info between nodes

This is also trivial:

// swap data from 'one' and 'other'
int so_swap_info(Node* one, Node* other)
{
    if (one == NULL) return -1;
    if (other == NULL) return -1;
    Info* temp  = one->info;
    one->info   = other->info;
    other->info = temp;
    return 0;
}

copy an Info record

A list does not create data, just copy

Info* so_copy_info(const Info* orig)
{
    if (orig == NULL) return NULL;
    Info* cp = malloc(sizeof(Info));
    if (cp == NULL) return NULL;
    memcpy(cp, orig, sizeof(Info));
    return cp; 
}

delete an Info record

since Info is plain static data, with no pointers, this is trivial

Info* so_delete_info(Info* del)
{
    if (del == NULL) return NULL;
    free(del);
    return NULL;
}

returning NULL gives chance of invalidating a pointer in the same line that frees the memory

printing an Info

Here we use something similar to the display in the question. Example:

  'Stay Stay Stay' 4:42 from 'Swift, Taylor' in 'RED' [Pop] Played 5 times, rating 1 pos 1
int so_print_info(Info* info, const char* msg)
{
    if (info == NULL) return -1;
    if (msg != NULL) printf("%s", msg);
    printf(
        "\
    '%s' %s from '%s' in '%s' [%s] Played %d times, rating %d pos %d\n",
        info->songTitle, info->songLength, info->artist,
        info->albumTitle, info->genre, info->timesPlayed,
        info->rating, info->position);
    return 0;
}

a factory function that builds the same data as in the question

This function just outputs over and over the same 9 records. Can be changed in minutes to generate more elaborate data.

Info* so_factory_info(void)
{  // just copy and paste from question data
#include "data.txt"
    Info* one = malloc(sizeof(Info));
    if (one == NULL) return NULL;
    static int ix = 0;
    memcpy(one, test_data+ix, sizeof(Info));
    one->songTitle[0] = (char)('0' + ix);
    ix             = (ix == 8) ? 0 : ++ix;
    return one;
}

the first letter of the song title was used to display the original order of the record in the original order. Convenient for testing.

data.txt is just the original data with some copy and paste and reformat. Here is the first record

    Info test_data[9] = {
        [0] =
            {.artist      = "Brooks, Garth",
             .albumTitle  = "FRESH HORSES",
             .songTitle   = "The Old Stuff",
             .genre       = "Pop",
             .songLength  = "2:57",
             .timesPlayed = 11,
             .rating      = 2,
             .position    = 8},
//... 9 records only

shuffling records

so_shuffle() is a simple idea: just returns a vector with the positions of the nodes after shuffling. Then we use the previous swap and locate functions to rearrange the list using these vector as index.

// returns a shuffled vector of 'N' values from 'min_v'
// to 'max_v' inclusive. If 'pSeed' is not 'NULL' will
// be used as a seed for rand()
int* so_shuffle(
    size_t N, size_t min_v, size_t max_v, unsigned* pSeed)
{
    if (pSeed != NULL) srand(*pSeed);
    if (min_v >= max_v) return NULL;
    size_t set = max_v - min_v + 1;
    if (set < 2) return NULL;  // at least 2...
    if (N == 0) return NULL;   // silly, but...
    if (N > set) N = set;      // for sanity
    int* v = (int*)malloc(set * sizeof(int));  // he vector
    if (v == NULL) return NULL;                // error
    // number the slots
    for (size_t i = 0, val = min_v; i < set; i += 1)
        *(v + i) = (int) val++;
    for (unsigned i = 1; i < N; i += 1)
    {  // one of the other values
        int* one   = v + i - 1;
        int* other = v + i + (rand() % (N - i));
        int  temp  = *one;    // the 1st value
        *one       = *other;  // exchange
        *other     = temp;
    };  // for()
    return v;
};