Back to course

Advanced Sorting Algorithms

Content Reader1 words • 0:00 • Browser TTS

image.png

Advanced Sorting Algorithms – Divide & Conquer Mastery

Imagine you're the Chief Operations Manager 🏢 of a massive distribution center with 1 million packages to sort for delivery, and your basic sorting methods are too slow:

📦 The Massive Distribution Challenge:

🏭 Industrial Sorting Scenario:

  • 1,000,000 Packages: Mixed destinations, priorities, sizes
  • Goal: Sort by delivery zones efficiently for optimal routing
  • Constraint: Must complete within 8-hour shift
  • Challenge: Basic methods take 3+ days - need industrial-grade solutions!

❌ Basic Method Limitations:

Bubble/Selection/Insertion Sort Problems:
- O(n²) time complexity = 1 trillion operations
- 3+ days to complete sorting
- Warehouse operations halted
- Customers receive delayed shipments
- Company loses millions in revenue!

Reality Check:
- 1,000,000² = 1,000,000,000,000 operations
- Even at 1 billion ops/second = 1000 seconds per package
- Completely impractical for real-world scale!

✅ Advanced Sorting Solutions: Advanced sorting algorithms use divide-and-conquer strategies like having multiple sorting teams work parallel on smaller sections, then intelligently combine their results:

The Theoretical Foundation

Divide-and-Conquer Paradigm

Core Principle: Break large problems into smaller, manageable subproblems, solve them independently, then combine solutions.

Three-Step Process:

  1. Divide: Split problem into smaller subproblems
  2. Conquer: Solve subproblems recursively or directly
  3. Combine: Merge solutions to solve original problem

Why This Works:

  • Parallelization: Multiple subproblems can be solved simultaneously
  • Reduced Complexity: Each subproblem is simpler than the original
  • Optimal Substructure: Optimal solution contains optimal solutions to subproblems
  • Scalability: Performance improves dramatically with problem size

1. Merge Sort - The Systematic Organizer

The Concept: Divide and Merge Strategy

Real-World Analogy: Document Organization System

Imagine you're organizing 10,000 research papers for a university library:

📄 Traditional Approach (Inefficient):

Single librarian sorting 10,000 papers:
- Compare paper A with all 9,999 others
- Find minimum, place in position 1
- Repeat for remaining 9,999 papers
- Time: Several weeks of continuous work

📄 Merge Sort Approach (Efficient):

Divide & Conquer Strategy:
1. Divide 10,000 papers into 2 stacks of 5,000
2. Recursively divide until single papers
3. Merge single papers into sorted pairs
4. Merge pairs into larger sorted groups
5. Continue until all papers are merged and sorted

Merge Sort Implementation

/**
 * Merge Sort Implementation
 * Time: O(n log n), Space: O(n)
 */

function mergeSort(arr) {
    // Base case: arrays with 0 or 1 element are already sorted
    if (arr.length <= 1) {
        return arr;
    }
    
    // Divide: Split array into two halves
    const mid = Math.floor(arr.length / 2);
    const left = arr.slice(0, mid);
    const right = arr.slice(mid);
    
    // Conquer: Recursively sort both halves
    const sortedLeft = mergeSort(left);
    const sortedRight = mergeSort(right);
    
    // Combine: Merge the sorted halves
    return merge(sortedLeft, sortedRight);
}

/**
 * Merge two sorted arrays into one sorted array
 */
function merge(left, right) {
    let result = [];
    let leftIndex = 0;
    let rightIndex = 0;
    
    // Compare elements from both arrays and merge in sorted order
    while (leftIndex < left.length && rightIndex < right.length) {
        if (left[leftIndex] <= right[rightIndex]) {
            result.push(left[leftIndex]);
            leftIndex++;
        } else {
            result.push(right[rightIndex]);
            rightIndex++;
        }
    }
    
    // Add remaining elements from left array
    while (leftIndex < left.length) {
        result.push(left[leftIndex]);
        leftIndex++;
    }
    
    // Add remaining elements from right array
    while (rightIndex < right.length) {
        result.push(right[rightIndex]);
        rightIndex++;
    }
    
    return result;
}

// Example usage and step-by-step visualization
console.log("Merge Sort Example:");
const unsorted = [64, 34, 25, 12, 22, 11, 90];
console.log("Original:", unsorted);

// Let's trace through the sorting process
function mergeSortWithTrace(arr, depth = 0) {
    const indent = "  ".repeat(depth);
    console.log(`${indent}Sorting: [${arr.join(", ")}]`);
    
    if (arr.length <= 1) {
        console.log(`${indent}Base case reached: [${arr.join(", ")}]`);
        return arr;
    }
    
    const mid = Math.floor(arr.length / 2);
    const left = arr.slice(0, mid);
    const right = arr.slice(mid);
    
    console.log(`${indent}Dividing into: [${left.join(", ")}] and [${right.join(", ")}]`);
    
    const sortedLeft = mergeSortWithTrace(left, depth + 1);
    const sortedRight = mergeSortWithTrace(right, depth + 1);
    
    const merged = merge(sortedLeft, sortedRight);
    console.log(`${indent}Merged result: [${merged.join(", ")}]`);
    
    return merged;
}

const sorted = mergeSortWithTrace(unsorted);
console.log("Final sorted:", sorted);

Step-by-Step Trace:

Sorting: [64, 34, 25, 12, 22, 11, 90]
  Dividing into: [64, 34, 25] and [12, 22, 11, 90]
    Sorting: [64, 34, 25]
      Dividing into: [64] and [34, 25]
        Sorting: [64]
        Base case reached: [64]
        Sorting: [34, 25]
          Dividing into: [34] and [25]
            Sorting: [34]
            Base case reached: [34]
            Sorting: [25]
            Base case reached: [25]
          Merged result: [25, 34]
      Merged result: [25, 34, 64]
    Sorting: [12, 22, 11, 90]
      Dividing into: [12, 22] and [11, 90]
        Sorting: [12, 22]
          Dividing into: [12] and [22]
            Sorting: [12]
            Base case reached: [12]
            Sorting: [22]
            Base case reached: [22]
          Merged result: [12, 22]
        Sorting: [11, 90]
          Dividing into: [11] and [90]
            Sorting: [11]
            Base case reached: [11]
            Sorting: [90]
            Base case reached: [90]
          Merged result: [11, 90]
      Merged result: [11, 12, 22, 90]
  Merged result: [11, 12, 22, 25, 34, 64, 90]
Final sorted: [11, 12, 22, 25, 34, 64, 90]

Merge Sort Analysis

Time Complexity: O(n log n)

  • Divide phase: O(log n) levels of recursion
  • Merge phase: O(n) work per level
  • Total: O(n) × O(log n) = O(n log n)

Space Complexity: O(n)

  • Requires additional arrays for merging
  • Not in-place sorting algorithm

Characteristics:

  • Stable: Maintains relative order of equal elements
  • Predictable: Always O(n log n) regardless of input
  • External: Works well for external sorting (disk-based)
  • Parallelizable: Divide phase can be parallelized

2. Quick Sort - The Strategic Partitioner

The Concept: Partition and Conquer

Real-World Analogy: Tournament Organization

Imagine organizing a tennis tournament with 1,000 players ranked by skill:

🎾 Quick Sort Tournament Strategy:

1. Choose a "pivot" player (median skill level)
2. Divide players into two groups:
   - Better than pivot (left side)
   - Worse than pivot (right side)
3. Recursively organize tournaments in each group
4. Combine: Better players + Pivot + Worse players

Why This Works:

  • Pivot Selection: Strategic choice reduces problem size efficiently
  • Partitioning: One pass divides problem into smaller subproblems
  • In-Place: No additional arrays needed (unlike merge sort)
  • Average Case: Excellent performance with good pivot selection

Quick Sort Implementation

/**
 * Quick Sort Implementation
 * Average Time: O(n log n), Worst Time: O(n²), Space: O(log n)
 */

function quickSort(arr, low = 0, high = arr.length - 1) {
    if (low < high) {
        // Partition the array and get the pivot index
        const pivotIndex = partition(arr, low, high);
        
        // Recursively sort elements before and after partition
        quickSort(arr, low, pivotIndex - 1);
        quickSort(arr, pivotIndex + 1, high);
    }
    return arr;
}

/**
 * Partition function using Lomuto partition scheme
 * Places pivot element at correct position and arranges array
 * so that smaller elements are on left, larger on right
 */
function partition(arr, low, high) {
    // Choose rightmost element as pivot
    const pivot = arr[high];
    
    // Index of smaller element (indicates right position of pivot)
    let i = low - 1;
    
    for (let j = low; j < high; j++) {
        // If current element is smaller than or equal to pivot
        if (arr[j] <= pivot) {
            i++; // Increment index of smaller element
            [arr[i], arr[j]] = [arr[j], arr[i]]; // Swap elements
        }
    }
    
    // Place pivot at correct position
    [arr[i + 1], arr[high]] = [arr[high], arr[i + 1]];
    return i + 1; // Return pivot position
}

// Example with detailed tracing
function quickSortWithTrace(arr, low = 0, high = arr.length - 1, depth = 0) {
    const indent = "  ".repeat(depth);
    console.log(`${indent}QuickSort range [${low}..${high}]: [${arr.slice(low, high + 1).join(", ")}]`);
    
    if (low < high) {
        const pivotIndex = partitionWithTrace(arr, low, high, depth);
        console.log(`${indent}After partition, pivot ${arr[pivotIndex]} at index ${pivotIndex}`);
        console.log(`${indent}Array state: [${arr.join(", ")}]`);
        
        quickSortWithTrace(arr, low, pivotIndex - 1, depth + 1);
        quickSortWithTrace(arr, pivotIndex + 1, high, depth + 1);
    }
    return arr;
}

function partitionWithTrace(arr, low, high, depth) {
    const indent = "  ".repeat(depth);
    const pivot = arr[high];
    console.log(`${indent}Partitioning with pivot: ${pivot}`);
    
    let i = low - 1;
    
    for (let j = low; j < high; j++) {
        if (arr[j] <= pivot) {
            i++;
            if (i !== j) {
                [arr[i], arr[j]] = [arr[j], arr[i]];
                console.log(`${indent}Swapped ${arr[j]} and ${arr[i]}: [${arr.join(", ")}]`);
            }
        }
    }
    
    [arr[i + 1], arr[high]] = [arr[high], arr[i + 1]];
    console.log(`${indent}Placed pivot ${pivot} at position ${i + 1}`);
    
    return i + 1;
}

// Example usage
console.log("Quick Sort Example:");
const quickSortArray = [64, 34, 25, 12, 22, 11, 90];
console.log("Original:", quickSortArray);
const quickSorted = quickSortWithTrace([...quickSortArray]);
console.log("Final sorted:", quickSorted);

Quick Sort Optimizations

1. Pivot Selection Strategies:

// Random pivot selection to avoid worst-case scenarios
function randomizedQuickSort(arr, low = 0, high = arr.length - 1) {
    if (low < high) {
        // Randomly select pivot and swap with last element
        const randomIndex = Math.floor(Math.random() * (high - low + 1)) + low;
        [arr[randomIndex], arr[high]] = [arr[high], arr[randomIndex]];
        
        const pivotIndex = partition(arr, low, high);
        randomizedQuickSort(arr, low, pivotIndex - 1);
        randomizedQuickSort(arr, pivotIndex + 1, high);
    }
    return arr;
}

// Median-of-three pivot selection
function medianOfThreePivot(arr, low, high) {
    const mid = Math.floor((low + high) / 2);
    
    // Sort three elements to find median
    if (arr[mid] < arr[low]) [arr[low], arr[mid]] = [arr[mid], arr[low]];
    if (arr[high] < arr[low]) [arr[low], arr[high]] = [arr[high], arr[low]];
    if (arr[high] < arr[mid]) [arr[mid], arr[high]] = [arr[high], arr[mid]];
    
    // Place median at end for partitioning
    [arr[mid], arr[high]] = [arr[high], arr[mid]];
    return arr[high];
}

2. Hybrid Approach for Small Arrays:

function hybridQuickSort(arr, low = 0, high = arr.length - 1) {
    const INSERTION_SORT_THRESHOLD = 10;
    
    if (low < high) {
        if (high - low + 1 <= INSERTION_SORT_THRESHOLD) {
            // Use insertion sort for small subarrays
            insertionSortRange(arr, low, high);
        } else {
            const pivotIndex = partition(arr, low, high);
            hybridQuickSort(arr, low, pivotIndex - 1);
            hybridQuickSort(arr, pivotIndex + 1, high);
        }
    }
    return arr;
}

function insertionSortRange(arr, low, high) {
    for (let i = low + 1; i <= high; i++) {
        const key = arr[i];
        let j = i - 1;
        
        while (j >= low && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
}

Quick Sort Analysis

Time Complexity:

  • Best/Average Case: O(n log n) - balanced partitions
  • Worst Case: O(n²) - already sorted or reverse sorted with poor pivot
  • Space: O(log n) - recursion stack space

Characteristics:

  • In-place: Sorts within original array
  • Unstable: May change relative order of equal elements
  • Cache-friendly: Good locality of reference
  • Practical: Often fastest in practice despite O(n²) worst case

3. Heap Sort - The Priority-Based Organizer

The Concept: Heap Data Structure Sorting

Real-World Analogy: Hospital Emergency Room

Imagine managing patient prioritization in a busy emergency room:

🏥 Heap Sort Hospital Strategy:

1. Build a "priority heap" of all patients by severity
2. Always treat the most critical patient first (extract max)
3. Reorganize remaining patients to maintain priority order
4. Repeat until all patients are treated in proper order

Why Heap Sort Works:

  • Heap Property: Parent always more/less than children
  • Complete Binary Tree: Efficient array representation
  • Guaranteed Performance: Always O(n log n)
  • In-Place: No additional arrays needed

Heap Sort Implementation

/**
 * Heap Sort Implementation
 * Time: O(n log n), Space: O(1)
 */

function heapSort(arr) {
    const n = arr.length;
    
    // Build max heap from array
    buildMaxHeap(arr);
    console.log("Built max heap:", [...arr]);
    
    // Extract elements from heap one by one
    for (let i = n - 1; i > 0; i--) {
        // Move current root (max element) to end
        [arr[0], arr[i]] = [arr[i], arr[0]];
        console.log(`Extracted max ${arr[i]}, heap size now ${i}`);
        
        // Restore heap property for reduced heap
        heapify(arr, 0, i);
        console.log("After heapify:", arr.slice(0, i), "| Sorted:", arr.slice(i));
    }
    
    return arr;
}

/**
 * Build max heap from unsorted array
 */
function buildMaxHeap(arr) {
    const n = arr.length;
    
    // Start from last non-leaf node and heapify each node
    for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
        heapify(arr, i, n);
    }
}

/**
 * Maintain heap property for subtree rooted at index i
 * Assumes left and right subtrees are already heaps
 */
function heapify(arr, i, heapSize) {
    let largest = i;      // Initialize largest as root
    let left = 2 * i + 1; // Left child index
    let right = 2 * i + 2; // Right child index
    
    // Check if left child exists and is greater than root
    if (left < heapSize && arr[left] > arr[largest]) {
        largest = left;
    }
    
    // Check if right child exists and is greater than current largest
    if (right < heapSize && arr[right] > arr[largest]) {
        largest = right;
    }
    
    // If largest is not root, swap and continue heapifying
    if (largest !== i) {
        [arr[i], arr[largest]] = [arr[largest], arr[i]];
        
        // Recursively heapify the affected subtree
        heapify(arr, largest, heapSize);
    }
}

// Helper function to visualize heap structure
function printHeapTree(arr, index = 0, depth = 0) {
    if (index >= arr.length) return;
    
    const indent = "  ".repeat(depth);
    console.log(`${indent}${arr[index]}`);
    
    const leftChild = 2 * index + 1;
    const rightChild = 2 * index + 2;
    
    if (leftChild < arr.length || rightChild < arr.length) {
        if (leftChild < arr.length) {
            console.log(`${indent}├─ Left:`);
            printHeapTree(arr, leftChild, depth + 1);
        }
        if (rightChild < arr.length) {
            console.log(`${indent}└─ Right:`);
            printHeapTree(arr, rightChild, depth + 1);
        }
    }
}

// Example usage
console.log("Heap Sort Example:");
const heapSortArray = [64, 34, 25, 12, 22, 11, 90];
console.log("Original:", heapSortArray);

console.log("\nBuilding heap visualization:");
const heapCopy = [...heapSortArray];
buildMaxHeap(heapCopy);
printHeapTree(heapCopy);

console.log("\nSorting process:");
const heapSorted = heapSort([...heapSortArray]);
console.log("Final sorted:", heapSorted);

Heap Sort Analysis

Time Complexity: O(n log n)

  • Build Heap: O(n) - can be proven mathematically
  • Extract Max: O(log n) × n elements = O(n log n)
  • Total: O(n) + O(n log n) = O(n log n)

Space Complexity: O(1)

  • In-place sorting algorithm
  • Only uses constant extra space

Characteristics:

  • Not stable: May change relative order of equal elements
  • Predictable: Always O(n log n) regardless of input
  • In-place: No additional arrays needed
  • Not adaptive: Doesn't benefit from partially sorted input

Algorithm Comparison and Selection Guide

Performance Comparison Table

AlgorithmBest CaseAverage CaseWorst CaseSpaceStableIn-Place
Merge SortO(n log n)O(n log n)O(n log n)O(n)YesNo
Quick SortO(n log n)O(n log n)O(n²)O(log n)NoYes
Heap SortO(n log n)O(n log n)O(n log n)O(1)NoYes

When to Use Each Algorithm

Choose Merge Sort When:

  • Stability required: Need to preserve relative order of equal elements
  • Predictable performance: Cannot tolerate O(n²) worst case
  • External sorting: Dealing with data larger than memory
  • Linked lists: Merge sort works well with linked list structures
  • Parallel processing: Algorithm is easily parallelizable

Choose Quick Sort When:

  • General purpose: Best average-case performance for most data
  • Memory constrained: Need in-place sorting with minimal extra space
  • Cache performance: Want good locality of reference
  • Practical speed: Fastest algorithm in practice for most inputs
  • Random data: Input is not pathological (sorted/reverse sorted)

Choose Heap Sort When:

  • Guaranteed performance: Need O(n log n) worst-case guarantee
  • Memory critical: Need in-place sorting with O(1) space
  • Real-time systems: Predictable performance is crucial
  • Priority queue operations: Already using heap data structure
  • Security critical: Avoid worst-case scenarios of quick sort

Hybrid Strategies

Introsort (Introspective Sort):

// Used by many standard libraries (C++ std::sort)
function introsort(arr, low = 0, high = arr.length - 1, maxDepth = null) {
    if (maxDepth === null) {
        maxDepth = 2 * Math.floor(Math.log2(arr.length));
    }
    
    if (low < high) {
        if (maxDepth === 0) {
            // Switch to heap sort when recursion too deep
            heapSortRange(arr, low, high);
        } else if (high - low + 1 <= 16) {
            // Use insertion sort for small arrays
            insertionSortRange(arr, low, high);
        } else {
            // Use quick sort for medium arrays
            const pivotIndex = partition(arr, low, high);
            introsort(arr, low, pivotIndex - 1, maxDepth - 1);
            introsort(arr, pivotIndex + 1, high, maxDepth - 1);
        }
    }
    return arr;
}

Timsort (Used in Python and Java):

  • Combines merge sort and insertion sort
  • Adaptive to existing order in data
  • Stable and optimized for real-world data patterns
  • Identifies and merges existing sorted runs

Practical Applications

1. Database Query Optimization

// External merge sort for large database result sets
class ExternalMergeSort {
    constructor(maxMemory) {
        this.maxMemory = maxMemory;
        this.tempFiles = [];
    }
    
    sortLargeDataset(data) {
        // Phase 1: Create sorted runs
        const runs = this.createSortedRuns(data);
        
        // Phase 2: Merge runs
        return this.mergeRuns(runs);
    }
    
    createSortedRuns(data) {
        const runs = [];
        const chunkSize = Math.floor(this.maxMemory / 8); // Assuming 8 bytes per element
        
        for (let i = 0; i < data.length; i += chunkSize) {
            const chunk = data.slice(i, i + chunkSize);
            chunk.sort((a, b) => a - b); // Sort in memory
            runs.push(chunk);
        }
        
        return runs;
    }
    
    mergeRuns(runs) {
        while (runs.length > 1) {
            const mergedRuns = [];
            
            for (let i = 0; i < runs.length; i += 2) {
                if (i + 1 < runs.length) {
                    mergedRuns.push(this.mergeTwoRuns(runs[i], runs[i + 1]));
                } else {
                    mergedRuns.push(runs[i]);
                }
            }
            
            runs = mergedRuns;
        }
        
        return runs[0];
    }
    
    mergeTwoRuns(run1, run2) {
        // Standard merge operation
        return merge(run1, run2);
    }
}

2. Real-Time System Scheduling

// Heap sort for priority-based task scheduling
class TaskScheduler {
    constructor() {
        this.tasks = [];
    }
    
    addTask(task, priority) {
        this.tasks.push({ task, priority, timestamp: Date.now() });
    }
    
    scheduleTasks() {
        // Sort by priority (descending) then by timestamp (ascending)
        return heapSort(this.tasks.map((t, i) => ({ ...t, index: i })))
            .sort((a, b) => {
                if (a.priority !== b.priority) {
                    return b.priority - a.priority; // Higher priority first
                }
                return a.timestamp - b.timestamp; // Earlier timestamp first
            });
    }
}

3. Graphics and Game Development

// Quick sort for z-buffer sorting in 3D graphics
class ZBufferSort {
    sortPolygons(polygons) {
        // Sort by z-depth for proper rendering order
        return quickSort(polygons, (a, b) => b.depth - a.depth);
    }
    
    // Optimized for GPU-friendly memory access patterns
    quickSortGPUFriendly(arr) {
        // Implementation that minimizes memory bandwidth
        // Uses cache-friendly partitioning strategies
        return quickSort(arr);
    }
}

Summary

Advanced Sorting Mastery Achieved

Divide-and-Conquer Principles:

  • Problem Decomposition: Break complex problems into manageable subproblems
  • Recursive Solutions: Solve subproblems using same algorithm approach
  • Efficient Combination: Merge solutions optimally to solve original problem
  • Scalability: Achieve optimal O(n log n) performance for large datasets

Algorithm-Specific Strengths:

Merge Sort Excellence:

  • Stability: Preserves relative order of equal elements
  • Predictability: Consistent O(n log n) performance
  • External Sorting: Handles data larger than available memory
  • Parallelization: Naturally suitable for parallel processing

Quick Sort Superiority:

  • Practical Speed: Fastest average-case performance for random data
  • Memory Efficiency: In-place sorting with minimal extra space
  • Cache Performance: Excellent locality of reference for modern processors
  • Adaptability: Various optimizations available for different scenarios

Heap Sort Reliability:

  • Guaranteed Performance: Always O(n log n) regardless of input
  • Space Efficiency: True in-place sorting with O(1) extra space
  • Predictability: No worst-case scenarios like quick sort
  • Priority Applications: Natural fit for priority queue operations

Real-World Applications Mastered

Industrial-Scale Sorting:

  • Database Systems: External sorting for large query results
  • Distributed Systems: Merge sort for combining sorted data streams
  • Real-Time Systems: Heap sort for predictable performance requirements
  • Graphics Processing: Quick sort for depth-buffer management

Optimization Strategies:

  • Hybrid Algorithms: Combine strengths of multiple approaches
  • Adaptive Techniques: Leverage existing order in input data
  • Memory Optimization: Balance between time and space requirements
  • Platform-Specific: Optimize for specific hardware characteristics

Strategic Algorithm Selection

Decision Matrix:

Requirement          | Best Choice     | Reason
==================== | =============== | ================================
Stability needed     | Merge Sort      | Preserves equal element order
Memory constrained   | Heap Sort       | O(1) space complexity
General purpose      | Quick Sort      | Best average performance
Worst-case guarantee | Heap/Merge Sort | No O(n²) scenarios
External data        | Merge Sort      | Designed for disk-based sorting
Parallel processing  | Merge Sort      | Naturally parallelizable
Real-time systems    | Heap Sort       | Predictable performance

You now possess the algorithmic thinking, implementation expertise, and optimization strategies necessary to choose and implement the most appropriate sorting solution for any scenario, from small embedded systems to massive distributed databases.

The journey from O(n²) basic sorts to O(n log n) advanced algorithms represents a fundamental transformation in computational thinking - the ability to see problems through the lens of divide-and-conquer strategies that scale elegantly from hundreds to millions of elements.

Photo of Rahul Aher

Written by Rahul Aher

I'm Rahul, Sr. Software Engineer (SDE II) and passionate content creator. Sharing my expertise in software development to assist learners.

More about me