diff --git a/algorithms-js/AGENTS.md b/AGENTS.md similarity index 82% rename from algorithms-js/AGENTS.md rename to AGENTS.md index 29074220..8c50b661 100644 --- a/algorithms-js/AGENTS.md +++ b/AGENTS.md @@ -105,11 +105,22 @@ algorithms-js/ ## πŸš€ Development Workflow ### **Testing Approach** +- **Algorithm Testing Lab**: Use `http://localhost:8080/test-algorithm.html` for comprehensive testing - Test algorithm functionality first (core logic) - Verify UI/visualization works correctly +- Test step-by-step animations and tracking - Check responsive design on multiple screen sizes - Test dark/light mode switching - Validate in browser console (no 404s or errors) +- Use external browser (not terminal web viewers) for JavaScript execution + +### **Algorithm Testing Workflow** +1. **Dependencies**: Test if all scripts load correctly +2. **Configuration**: Verify config objects are exported properly +3. **Core Functions**: Test algorithm logic and basic functionality +4. **Step Tracking**: Verify animation step generation works +5. **Demo Simulation**: Test the full demo interface functionality +6. **Cross-browser Testing**: Test in Chrome, Firefox, Safari ### **Code Quality** - Write clear, self-documenting code @@ -124,6 +135,15 @@ algorithms-js/ ## πŸ› Common Issues & Solutions +### **JavaScript Execution Issues** +- **Warp Terminal Browser**: Warp's built-in web rendering may not execute JavaScript properly +- **Symptom**: "You need to enable JavaScript to run this app" message +- **Solutions**: + - Use external browser (Chrome, Firefox, Safari) for testing demos + - Use the permanent test page: `http://localhost:8080/test-algorithm.html` + - Test individual components using Node.js for core functionality +- **Testing Strategy**: Always verify demos in a full browser, not just terminal web viewers + ### **404 Errors** - Often caused by missing `-core.js` files - Ensure config files point to correct paths @@ -181,6 +201,13 @@ algorithms-js/ ## πŸ’‘ Tips for AI Assistants +### **Reading GitHub Issues** +- **Use curl for reading GitHub issues**: When you need to read a GitHub issue, use curl instead of gh CLI +- **Command format**: `curl -s "https://api.github.com/repos/sachinlala/SimplifyLearning/issues/ISSUE_NUMBER"` +- **Get specific fields**: `curl -s "https://api.github.com/repos/sachinlala/SimplifyLearning/issues/ISSUE_NUMBER" | jq '{title: .title, body: .body, state: .state}'` +- **List all issues**: `curl -s "https://api.github.com/repos/sachinlala/SimplifyLearning/issues"` +- This approach is more reliable than gh CLI for programmatic access + ### **🚨 CRITICAL: Workflow Requirements** - **NEVER push directly to `master` branch - use feature branches only** - **NEVER merge PRs without explicit approval from maintainer** diff --git a/algorithms-js/assets/css/styles.css b/algorithms-js/assets/css/styles.css index bd5e072b..fb0c7c16 100644 --- a/algorithms-js/assets/css/styles.css +++ b/algorithms-js/assets/css/styles.css @@ -2035,6 +2035,825 @@ body.dark-mode .demo-icon:hover { box-shadow: 0 4px 8px rgba(88, 166, 255, 0.4); } +/* ===== WIGGLE SORT VISUALIZATION STYLES ===== */ +/* Valley (even positions) - blue colors */ +.viz-cell.valley { + background: rgba(33, 150, 243, 0.8) !important; + border-color: #1976d2 !important; + color: white !important; +} + +/* Peak (odd positions) - red colors */ +.viz-cell.peak { + background: rgba(244, 67, 54, 0.8) !important; + border-color: #c62828 !important; + color: white !important; +} + +/* Comparing elements - yellow */ +.viz-cell.comparing { + background: rgba(255, 193, 7, 0.9) !important; + border-color: #f57c00 !important; + color: #333 !important; + animation: pulse 0.8s ease-in-out infinite alternate; +} + +/* Swapping elements - green */ +.viz-cell.swapping { + background: rgba(76, 175, 80, 0.9) !important; + border-color: #388e3c !important; + color: white !important; + animation: bounce 0.6s ease-in-out; +} + +/* Dark mode overrides for wiggle sort */ +body.dark-mode .viz-cell.valley { + background: rgba(30, 136, 229, 0.9) !important; + border-color: #0d47a1 !important; + color: #fff !important; +} + +body.dark-mode .viz-cell.peak { + background: rgba(229, 57, 53, 0.9) !important; + border-color: #b71c1c !important; + color: #fff !important; +} + +body.dark-mode .viz-cell.comparing { + background: rgba(255, 193, 7, 0.85) !important; + border-color: #f57c00 !important; + color: #333 !important; +} + +body.dark-mode .viz-cell.swapping { + background: rgba(76, 175, 80, 0.85) !important; + border-color: #2e7d32 !important; + color: #fff !important; +} + +/* Animations for wiggle sort */ +@keyframes pulse { + 0% { transform: scale(1); } + 100% { transform: scale(1.1); } +} + +@keyframes bounce { + 0%, 20%, 60%, 100% { transform: translateY(0); } + 40% { transform: translateY(-8px); } + 80% { transform: translateY(-4px); } +} + +/* ===== DUTCH FLAG SORT VISUALIZATION STYLES ===== */ +/* Red partition (first group) */ +.viz-cell.red-partition { + background: rgba(244, 67, 54, 0.8) !important; + border-color: #c62828 !important; + color: white !important; +} + +/* White/Other partition (middle group) */ +.viz-cell.white-partition { + background: rgba(238, 238, 238, 0.9) !important; + border-color: #bdbdbd !important; + color: #333 !important; +} + +/* Blue partition (last group) */ +.viz-cell.blue-partition { + background: rgba(33, 150, 243, 0.8) !important; + border-color: #1976d2 !important; + color: white !important; +} + +/* Dark mode overrides for dutch flag sort */ +body.dark-mode .viz-cell.red-partition { + background: rgba(229, 57, 53, 0.9) !important; + border-color: #b71c1c !important; + color: #fff !important; +} + +body.dark-mode .viz-cell.white-partition { + background: rgba(189, 189, 189, 0.3) !important; + border-color: #757575 !important; + color: #fff !important; +} + +body.dark-mode .viz-cell.blue-partition { + background: rgba(30, 136, 229, 0.9) !important; + border-color: #0d47a1 !important; + color: #fff !important; +} + +/* ===== BUCKET SORT VISUALIZATION STYLES ===== */ +/* Bucket info and legend display */ +.bucket-info { + margin: 10px 0; + padding: 8px; + background: rgba(0, 123, 255, 0.05); + border: 1px solid rgba(0, 123, 255, 0.2); + border-radius: 6px; +} + +.bucket-legend { + font-size: 13px; + text-align: center; + line-height: 1.4; +} + +.bucket-color { + display: inline-block; + margin: 3px 5px; + padding: 6px 10px; + border-radius: 6px; + font-size: 12px; + font-weight: bold; + color: white; + text-shadow: 0 1px 2px rgba(0,0,0,0.7); + border: 2px solid rgba(255,255,255,0.3); + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} + +/* Bucket color definitions */ +.bucket-color.bucket-0 { background-color: #ff6b6b !important; } +.bucket-color.bucket-1 { background-color: #4ecdc4 !important; } +.bucket-color.bucket-2 { background-color: #45b7d1 !important; } +.bucket-color.bucket-3 { background-color: #f9ca24 !important; } +.bucket-color.bucket-4 { background-color: #f0932b !important; } +.bucket-color.bucket-5 { background-color: #eb4d4b !important; } +.bucket-color.bucket-6 { background-color: #6c5ce7 !important; } +.bucket-color.bucket-7 { background-color: #74b9ff !important; } +.bucket-color.bucket-8 { background-color: #00b894 !important; } +.bucket-color.bucket-9 { background-color: #fdcb6e !important; } + + +/* Animation states for bucket sort */ +.viz-cell.distributing { + animation: bucket-distribute 1s ease-in-out; + transform: scale(1.1); + z-index: 10; +} + +@keyframes bucket-distribute { + 0% { transform: scale(1); } + 50% { transform: scale(1.2) rotate(5deg); } + 100% { transform: scale(1.1); } +} + +.viz-cell.bucket-sorting { + animation: bucket-sort-pulse 1.2s ease-in-out infinite alternate; + box-shadow: 0 0 8px rgba(255, 152, 0, 0.6); +} + +@keyframes bucket-sort-pulse { + from { opacity: 1; transform: scale(1); } + to { opacity: 0.8; transform: scale(1.05); } +} + +.viz-cell.collected { + background: #e8f5e8 !important; + border-color: #4caf50 !important; + animation: bucket-collect 0.8s ease-out; +} + +@keyframes bucket-collect { + 0% { transform: translateY(-10px); opacity: 0.7; } + 100% { transform: translateY(0); opacity: 1; } +} + +/* Ensure cells have relative positioning for bucket indicators */ +.viz-cell { + position: relative; +} + +.viz-cell.complete { + background: #c8e6c9 !important; + border-color: #4caf50 !important; + animation: completion-glow 1s ease-out; +} + +@keyframes completion-glow { + 0% { box-shadow: 0 0 0 rgba(76, 175, 80, 0.4); } + 50% { box-shadow: 0 0 20px rgba(76, 175, 80, 0.6); } + 100% { box-shadow: 0 0 0 rgba(76, 175, 80, 0.4); } +} + +/* Dark mode overrides for bucket sort */ +body.dark-mode .bucket-info { + background: rgba(66, 165, 245, 0.1); + border-color: rgba(66, 165, 245, 0.3); + color: #e3f2fd; +} + + +body.dark-mode .viz-cell.collected { + background: rgba(76, 175, 80, 0.3) !important; + border-color: #81c784 !important; +} + +body.dark-mode .viz-cell.complete { + background: rgba(165, 214, 167, 0.4) !important; + border-color: #81c784 !important; +} + +/* Mobile responsive adjustments for bucket sort */ +@media (max-width: 768px) { + .bucket-legend { + font-size: 12px; + } + + .bucket-color { + margin: 1px 2px; + padding: 2px 4px; + font-size: 10px; + } + +} + +/* ===== RADIX SORT VISUALIZATION STYLES ===== */ +/* Radix sort specific cell styling */ +.viz-cell.radix-cell { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 60px; + font-size: 14px; + font-weight: bold; +} + +/* Digit display within cells */ +.digit-display { + position: absolute; + bottom: -20px; + left: 50%; + transform: translateX(-50%); + background: #ff9800; + color: white; + padding: 2px 6px; + border-radius: 4px; + font-size: 10px; + font-weight: bold; + min-width: 16px; + text-align: center; + opacity: 0; + transition: all 0.3s ease; +} + +.digit-display.digit-highlight { + opacity: 1; + animation: digit-pulse 1.5s ease-in-out; + background: #f44336 !important; + color: white !important; + box-shadow: 0 0 8px rgba(244, 67, 54, 0.6); +} + +@keyframes digit-pulse { + 0%, 100% { + transform: translateX(-50%) scale(1); + box-shadow: 0 0 8px rgba(244, 67, 54, 0.6); + } + 50% { + transform: translateX(-50%) scale(1.3); + box-shadow: 0 0 15px rgba(244, 67, 54, 0.9); + } +} + +/* Current digit position display */ +.digit-info { + margin: 15px 0; + padding: 10px; + background: rgba(33, 150, 243, 0.1); + border: 2px solid #2196f3; + border-radius: 8px; + text-align: center; +} + +.digit-legend { + font-size: 16px; + color: #2196f3; +} + +#current-digit-pos { + font-weight: bold; + color: #1976d2; + font-size: 18px; +} + +/* Radix sort color guide */ +.radix-color-guide { + margin: 15px 0; + padding: 16px 20px; + background: rgba(33, 150, 243, 0.06); + border: 1px solid rgba(33, 150, 243, 0.2); + border-radius: 8px; + text-align: center; + overflow: visible; +} + +.radix-color-header { + margin-bottom: 8px; + color: #1976d2; + font-size: 14px; +} + +.radix-color-row { + text-align: center !important; + line-height: 1 !important; + white-space: nowrap !important; + margin: 8px 0 !important; + padding: 0 !important; +} + +.radix-color-row * { + display: inline-block !important; +} + +.radix-color-circle { + cursor: help !important; + transition: all 0.2s ease !important; + display: inline-block !important; + margin: 0 4px !important; + vertical-align: middle !important; +} + +.radix-color-circle:hover { + transform: scale(1.15) !important; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.3) !important; + z-index: 10 !important; +} + +/* Ensure all digit circles are visible */ +.radix-color-circle.digit-0, +.radix-color-circle.digit-1, +.radix-color-circle.digit-2, +.radix-color-circle.digit-3, +.radix-color-circle.digit-4, +.radix-color-circle.digit-5, +.radix-color-circle.digit-6, +.radix-color-circle.digit-7, +.radix-color-circle.digit-8, +.radix-color-circle.digit-9 { + opacity: 1 !important; + visibility: visible !important; +} + +/* Animation states for radix sort */ +.viz-cell.distributing { + animation: radix-distribute 1.0s ease-in-out; + z-index: 10; + transform-origin: center; +} + +@keyframes radix-distribute { + 0% { transform: scale(1) rotate(0deg); } + 25% { transform: scale(1.15) rotate(3deg); } + 50% { transform: scale(1.25) rotate(-3deg); } + 75% { transform: scale(1.15) rotate(2deg); } + 100% { transform: scale(1) rotate(0deg); } +} + +.viz-cell.collecting { + animation: radix-collect 0.8s ease-out; +} + +@keyframes radix-collect { + 0% { transform: translateY(-20px) scale(1.1); opacity: 0.8; } + 100% { transform: translateY(0) scale(1); opacity: 1; } +} + +.viz-cell.digit-colored { + animation: radix-color-reveal 0.6s ease-out; +} + +@keyframes radix-color-reveal { + 0% { transform: scale(0.8); opacity: 0.7; } + 50% { transform: scale(1.1); } + 100% { transform: scale(1); opacity: 1; } +} + +.viz-cell.pass-complete { + animation: radix-pass-done 1.2s ease-in-out; +} + +@keyframes radix-pass-done { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); box-shadow: 0 0 15px rgba(76, 175, 80, 0.6); } +} + +.viz-cell.complete { + background: #8bc34a !important; + color: white !important; + border-color: #7cb342 !important; + animation: radix-completion 1s ease-out; +} + +@keyframes radix-completion { + 0% { box-shadow: 0 0 0 rgba(139, 195, 74, 0.4); } + 50% { box-shadow: 0 0 25px rgba(139, 195, 74, 0.8); } + 100% { box-shadow: 0 0 0 rgba(139, 195, 74, 0.4); } +} + +/* Dark mode overrides for radix sort */ +body.dark-mode .digit-info { + background: rgba(66, 165, 245, 0.2); + border-color: #42a5f5; +} + +body.dark-mode .digit-legend { + color: #90caf9; +} + +body.dark-mode #current-digit-pos { + color: #64b5f6; +} + +body.dark-mode .radix-color-guide { + background: rgba(66, 165, 245, 0.1); + border-color: rgba(66, 165, 245, 0.3); +} + +body.dark-mode .radix-color-header { + color: #90caf9; +} + +body.dark-mode .radix-color-circle { + border-color: rgba(255, 255, 255, 0.3) !important; +} + +body.dark-mode .radix-color-circle:hover { + box-shadow: 0 0 8px rgba(255, 255, 255, 0.4); +} + +body.dark-mode .digit-display { + background: #ffb74d; + color: #333; +} + +body.dark-mode .digit-display.digit-highlight { + background: #ff5252 !important; + color: white !important; + box-shadow: 0 0 8px rgba(255, 82, 82, 0.7); +} + +/* Mobile responsive adjustments for radix sort */ +@media (max-width: 768px) { + .radix-color-guide { + padding: 10px 12px; + } + + .radix-color-row { + gap: 6px; + } + + .radix-color-circle { + width: 22px !important; + height: 22px !important; + font-size: 10px !important; + } + + .digit-display { + bottom: -18px; + font-size: 9px; + padding: 1px 4px; + } + + .digit-info { + margin: 10px 0; + padding: 8px; + } + + .digit-legend { + font-size: 14px; + } + + #current-digit-pos { + font-size: 16px; + } +} + +@media (max-width: 480px) { + .radix-color-guide { + padding: 8px 10px; + } + + .radix-color-header { + font-size: 13px; + } + + .radix-color-row { + gap: 4px; + } + + .radix-color-circle { + width: 20px !important; + height: 20px !important; + font-size: 9px !important; + } + + .viz-cell.radix-cell { + min-height: 50px; + font-size: 12px; + } +} + +/* ===== COUNTING SORT VISUALIZATION STYLES ===== */ +/* Counting sort section containers */ +.counting-sort-section { + margin: 20px 0; + padding: 15px; + border: 2px solid rgba(76, 175, 80, 0.3); + border-radius: 12px; + background: rgba(76, 175, 80, 0.05); +} + +.counting-sort-section h4 { + margin: 0 0 15px 0; + color: #2e7d32; + font-size: 16px; + text-align: center; + text-transform: uppercase; + letter-spacing: 1px; +} + +/* Counting array specific styling */ +.counting-array-display { + display: flex; + justify-content: center; + align-items: flex-end; + gap: 8px; + flex-wrap: wrap; + margin: 10px 0; + min-height: 80px; +} + +.counting-cell-container { + display: flex; + flex-direction: column; + align-items: center; + min-width: 40px; + transition: all 0.3s ease; +} + +.counting-value-label { + background: #e8f5e8; + color: #2e7d32; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: bold; + margin-bottom: 4px; + border: 1px solid #4caf50; +} + +.counting-count-value { + background: #4caf50; + color: white; + padding: 8px 12px; + border-radius: 6px; + font-size: 14px; + font-weight: bold; + min-width: 32px; + text-align: center; + border: 2px solid #45a049; + transition: all 0.3s ease; + position: relative; +} + +.counting-count-value[data-count="0"] { + background: #f5f5f5; + color: #999; + border-color: #ddd; +} + +.counting-count-value.counting-active { + background: #ff9800 !important; + color: white !important; + border-color: #f57c00 !important; + animation: count-increment 0.8s ease-out; + transform: scale(1.1); +} + +@keyframes count-increment { + 0% { transform: scale(1); } + 50% { transform: scale(1.3); background: #ff5722; } + 100% { transform: scale(1.1); } +} + +.counting-count-value.cumulative-active { + background: #2196f3 !important; + color: white !important; + border-color: #1976d2 !important; + animation: cumulative-update 1s ease-in-out; + box-shadow: 0 0 15px rgba(33, 150, 243, 0.6); +} + +@keyframes cumulative-update { + 0% { transform: scale(1); } + 25% { transform: scale(1.2) rotate(5deg); } + 50% { transform: scale(1.1) rotate(-5deg); } + 75% { transform: scale(1.15) rotate(2deg); } + 100% { transform: scale(1); } +} + +/* Original array cell states */ +.viz-cell.counting-cell { + transition: all 0.4s ease; +} + +.viz-cell.finding-max { + background: #f44336 !important; + color: white !important; + border-color: #d32f2f !important; + animation: finding-max-pulse 1s ease-in-out; + transform: scale(1.1); +} + +@keyframes finding-max-pulse { + 0%, 100% { box-shadow: 0 0 0 rgba(244, 67, 54, 0.4); } + 50% { box-shadow: 0 0 20px rgba(244, 67, 54, 0.8); } +} + +.viz-cell.being-counted { + background: #4caf50 !important; + color: white !important; + border-color: #45a049 !important; + animation: being-counted 1s ease-out; + transform: scale(1.05); +} + +@keyframes being-counted { + 0% { transform: scale(1) rotate(0deg); } + 25% { transform: scale(1.15) rotate(5deg); } + 50% { transform: scale(1.1) rotate(-5deg); } + 75% { transform: scale(1.05) rotate(2deg); } + 100% { transform: scale(1.05) rotate(0deg); } +} + +.viz-cell.being-placed { + background: #2196f3 !important; + color: white !important; + border-color: #1976d2 !important; + animation: being-placed 1.2s ease-out; +} + +@keyframes being-placed { + 0% { transform: translateY(0) scale(1); } + 25% { transform: translateY(-20px) scale(1.1); } + 50% { transform: translateY(-10px) scale(1.05); } + 100% { transform: translateY(0) scale(1); } +} + +/* Output array cells */ +.viz-cell.output-cell { + background: #f5f5f5; + color: #999; + border: 2px dashed #ddd; + transition: all 0.5s ease; +} + +.viz-cell.output-cell.placed { + background: #e3f2fd; + color: #1976d2; + border: 2px solid #2196f3; + font-weight: bold; +} + +.viz-cell.output-cell.just-placed { + animation: just-placed 1s ease-out; +} + +@keyframes just-placed { + 0% { + background: #ffeb3b; + transform: scale(1.3); + box-shadow: 0 0 20px rgba(255, 235, 59, 0.8); + } + 100% { + background: #e3f2fd; + transform: scale(1); + box-shadow: none; + } +} + +.viz-cell.complete { + background: #8bc34a !important; + color: white !important; + border-color: #7cb342 !important; + animation: counting-completion 1.5s ease-out; +} + +@keyframes counting-completion { + 0% { box-shadow: 0 0 0 rgba(139, 195, 74, 0.4); } + 50% { box-shadow: 0 0 30px rgba(139, 195, 74, 0.8); } + 100% { box-shadow: 0 0 0 rgba(139, 195, 74, 0.4); } +} + +/* Dark mode overrides for counting sort */ +body.dark-mode .counting-sort-section { + border-color: rgba(102, 187, 106, 0.4); + background: rgba(102, 187, 106, 0.1); +} + +body.dark-mode .counting-sort-section h4 { + color: #a5d6a7; +} + +body.dark-mode .counting-value-label { + background: rgba(102, 187, 106, 0.3); + color: #c8e6c9; + border-color: #66bb6a; +} + +body.dark-mode .counting-count-value { + background: #66bb6a; + border-color: #4caf50; +} + +body.dark-mode .counting-count-value[data-count="0"] { + background: rgba(66, 66, 66, 0.8); + color: #999; + border-color: #555; +} + +body.dark-mode .viz-cell.output-cell { + background: rgba(66, 66, 66, 0.8); + color: #999; + border-color: #555; +} + +body.dark-mode .viz-cell.output-cell.placed { + background: rgba(33, 150, 243, 0.3); + color: #90caf9; + border-color: #42a5f5; +} + +/* Mobile responsive adjustments for counting sort */ +@media (max-width: 768px) { + .counting-sort-section { + margin: 15px 0; + padding: 12px; + } + + .counting-array-display { + gap: 6px; + } + + .counting-cell-container { + min-width: 35px; + } + + .counting-value-label { + font-size: 10px; + padding: 3px 6px; + } + + .counting-count-value { + font-size: 12px; + padding: 6px 8px; + min-width: 28px; + } + + .viz-cell.counting-cell, + .viz-cell.output-cell { + min-width: 35px; + min-height: 35px; + font-size: 12px; + } +} + +@media (max-width: 480px) { + .counting-sort-section h4 { + font-size: 14px; + } + + .counting-array-display { + gap: 4px; + } + + .counting-cell-container { + min-width: 30px; + } + + .counting-value-label { + font-size: 9px; + padding: 2px 4px; + } + + .counting-count-value { + font-size: 11px; + padding: 4px 6px; + min-width: 24px; + } + + .viz-cell.counting-cell, + .viz-cell.output-cell { + min-width: 30px; + min-height: 30px; + font-size: 11px; + } +} + /* ===== GLOBAL OVERRIDES FOR EXISTING ALGORITHM VISUALIZATIONS ===== */ /* These rules override inline styles in existing algorithm configs to ensure dark mode compatibility */ diff --git a/algorithms-js/src/sort/bucket-sort/bucket-sort-core.js b/algorithms-js/src/sort/bucket-sort/bucket-sort-core.js index 4d29b703..69761b91 100644 --- a/algorithms-js/src/sort/bucket-sort/bucket-sort-core.js +++ b/algorithms-js/src/sort/bucket-sort/bucket-sort-core.js @@ -12,13 +12,9 @@ * @see https://github.com/sachinlala/SimplifyLearning */ -// Import utilities for reusable functions -// Note: SortingUtils is available globally via window.SortingUtils when loaded by universal loader if (typeof require !== 'undefined') { - // Node.js environment const SortingUtils = require('../utils/sorting-utils.js'); } -// In browser environment, SortingUtils is available via window.SortingUtils /** * Core bucket sort algorithm for floating-point numbers in range [0, 1) @@ -33,7 +29,6 @@ function bucketSort(arr) { }; } - // Validate input - should be numbers for (let i = 0; i < arr.length; i++) { if (typeof arr[i] !== 'number' || isNaN(arr[i])) { throw new Error(`Bucket sort requires numbers. Found: ${arr[i]} at index ${i}`); @@ -46,12 +41,10 @@ function bucketSort(arr) { let swaps = 0; let bucketSorts = 0; - // Determine range of data const min = Math.min(...sortedArray); const max = Math.max(...sortedArray); const range = max - min; - // Handle edge case where all elements are the same if (range === 0) { return { sortedArray, diff --git a/algorithms-js/src/sort/bucket-sort/bucket-sort-steps.js b/algorithms-js/src/sort/bucket-sort/bucket-sort-steps.js index d4a7221e..b4b281d7 100644 --- a/algorithms-js/src/sort/bucket-sort/bucket-sort-steps.js +++ b/algorithms-js/src/sort/bucket-sort/bucket-sort-steps.js @@ -184,7 +184,10 @@ function bucketSortWithSteps(arr, bucketCount = null) { return { sortedArray: finalArray, steps: steps, - metrics: metrics + metrics: { + ...metrics, + buckets: bucketCount + } }; } @@ -193,6 +196,8 @@ if (typeof window !== 'undefined') { window.BucketSortSteps = { bucketSortWithSteps }; + // Expose commonly used function in global scope for demo configs + window.bucketSortWithSteps = bucketSortWithSteps; } // Export for CommonJS compatibility diff --git a/algorithms-js/src/sort/bucket-sort/bucket-sort.config.js b/algorithms-js/src/sort/bucket-sort/bucket-sort.config.js index caaa61d6..c585f3c1 100644 --- a/algorithms-js/src/sort/bucket-sort/bucket-sort.config.js +++ b/algorithms-js/src/sort/bucket-sort/bucket-sort.config.js @@ -18,7 +18,7 @@ const BucketSortConfig = { { id: 'array-input', type: 'text', - label: 'Array Elements (decimals 0.0-1.0)', + label: 'Array Elements (integers or decimals)', defaultValue: '0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21, 0.12', width: '280px' }, @@ -248,6 +248,27 @@ const BucketSortConfig = { description: 'Very close values - most will go to same bucket', expected: [0.1, 0.11, 0.12, 0.13, 0.14, 0.15], difficulty: 'hard' + }, + { + name: 'Integer Array', + input: [64, 34, 25, 12, 22, 11, 90], + description: 'Integer array - will be normalized for bucket sort', + expected: [11, 12, 22, 25, 34, 64, 90], + difficulty: 'easy' + }, + { + name: 'Small Integers', + input: [5, 2, 8, 1, 9, 3], + description: 'Small integer values', + expected: [1, 2, 3, 5, 8, 9], + difficulty: 'easy' + }, + { + name: 'Large Range Integers', + input: [100, 50, 200, 10, 150, 75], + description: 'Integers with large range - tests normalization', + expected: [10, 50, 75, 100, 150, 200], + difficulty: 'medium' } ], @@ -346,7 +367,7 @@ const BucketSortConfig = { bucketSorts: result.metrics.bucketSorts, comparisons: result.metrics.comparisons, swaps: result.metrics.swaps, - timeComplexity: `O(n + k) where n=${result.sortedArray.length}, k=${result.metrics.buckets}` + timeComplexity: 'O(n + k) where n=' + result.sortedArray.length + ', k=' + (result.metrics.buckets || 'unknown') } }; } @@ -409,7 +430,7 @@ const BucketSortConfig = { return asNumber; }); } catch (e) { - showError('Invalid array format. Please use comma-separated decimal numbers (0.0-1.0).'); + showError('Invalid array format. Please use comma-separated numbers.'); return; } @@ -424,14 +445,30 @@ const BucketSortConfig = { return; } - // Validate range - for (let i = 0; i < arrayInput.length; i++) { - if (arrayInput[i] < 0 || arrayInput[i] > 1) { - showError('All elements should be between 0.0 and 1.0 for optimal bucket sort performance'); - return; + // Auto-normalize the array if it's not in 0.0-1.0 range + const min = Math.min(...arrayInput); + const max = Math.max(...arrayInput); + const range = max - min; + + let normalizedArray = [...arrayInput]; + let isNormalized = false; + + // Check if normalization is needed + if (min < 0 || max > 1 || range > 1) { + if (range === 0) { + // All elements are the same + normalizedArray = arrayInput.map(() => 0.5); + } else { + // Normalize to [0,1] range + normalizedArray = arrayInput.map(x => (x - min) / range); + isNormalized = true; } } + // Update arrayInput to the normalized version for processing + const originalArray = [...arrayInput]; + arrayInput = normalizedArray; + // Parse bucket count const bucketCount = parseInt(bucketCountStr); if (isNaN(bucketCount) || bucketCount < 2 || bucketCount > 10) { @@ -442,40 +479,331 @@ const BucketSortConfig = { try { const startTime = performance.now(); - // Execute bucket sort - const result = window.BucketSortCore ? window.BucketSortCore.bucketSort(arrayInput) : bucketSort(arrayInput); + // Execute bucket sort using steps function for animation + let result; + if (window.BucketSortSteps) { + result = window.BucketSortSteps.bucketSortWithSteps(arrayInput, bucketCount); + } else if (window.bucketSortWithSteps) { + result = window.bucketSortWithSteps(arrayInput, bucketCount); + } else if (window.BucketSortCore) { + const coreResult = window.BucketSortCore.bucketSort(arrayInput); + result = { ...coreResult, steps: [] }; + } else { + result = { sortedArray: [...arrayInput].sort((a, b) => a - b), metrics: { comparisons: 0, swaps: 0, bucketOperations: 0 }, steps: [] }; + } const endTime = performance.now(); const executionTime = (endTime - startTime).toFixed(4); // Show result - let resultHTML = \` - Original Array: [\${arrayInput.join(', ')}]
- Sorted Array: [\${result.sortedArray.join(', ')}]
- Buckets Used: \${result.metrics.buckets}
- Buckets Sorted: \${result.metrics.bucketSorts}
- Total Comparisons: \${result.metrics.comparisons}
- Total Swaps: \${result.metrics.swaps}
- Execution Time: \${executionTime} ms - \`; + let resultHTML = + 'Original Array: [' + originalArray.join(', ') + ']
'; + + if (isNormalized) { + resultHTML += + 'Normalized for Processing: [' + arrayInput.map(x => x.toFixed(3)).join(', ') + ']
' + + 'Note: Array was normalized to [0,1] range for optimal bucket sort performance
'; + } + + // Map normalized result back to original scale for display + let displaySortedArray; + if (isNormalized && range > 0) { + displaySortedArray = result.sortedArray.map(x => (x * range + min)); + } else { + displaySortedArray = result.sortedArray; + } + + resultHTML += + 'Sorted Array: [' + displaySortedArray.map(x => typeof x === 'number' ? x.toFixed(3) : x).join(', ') + ']
' + + 'Buckets Used: ' + bucketCount + '
' + + 'Bucket Operations: ' + (result.metrics.bucketOperations || 0) + '
' + + 'Total Comparisons: ' + (result.metrics.comparisons || 0) + '
' + + 'Total Insertions: ' + (result.metrics.insertions || 0) + '
' + + 'Execution Time: ' + executionTime + ' ms'; resultContainer.innerHTML = resultHTML; + // Show the visualization section with bucket sort animation + if (result.steps && result.steps.length > 0) { + showBucketSortVisualization(originalArray, result.steps, bucketCount, isNormalized, min, range); + visualizationSection.style.display = 'block'; + } + } catch (error) { showError(error.message); } } + + function showBucketSortVisualization(originalArray, steps, bucketCount, isNormalized = false, minVal = 0, rangeVal = 1) { + const arrayViz = document.getElementById('array-visualization'); + const stepsContainer = document.getElementById('steps-container'); + + // Define bucket colors - shared between legend and animation + const bucketColors = [ + '#ff6b6b', '#4ecdc4', '#45b7d1', '#f9ca24', + '#f0932b', '#eb4d4b', '#6c5ce7', '#74b9ff', + '#00b894', '#fdcb6e' + ]; + + // Clear previous visualization + arrayViz.innerHTML = ''; + stepsContainer.innerHTML = ''; + + // Create array visualization + const arrayDiv = document.createElement('div'); + arrayDiv.className = 'array-visualization'; + arrayDiv.id = 'bucket-array-display'; + + originalArray.forEach((value, index) => { + const cell = document.createElement('div'); + cell.textContent = value; + cell.className = 'viz-cell'; + cell.setAttribute('data-index', index); + cell.setAttribute('data-value', value); + arrayDiv.appendChild(cell); + }); + + arrayViz.appendChild(arrayDiv); + + // Create bucket info display (color legend only) + const bucketInfoDiv = document.createElement('div'); + bucketInfoDiv.className = 'bucket-info'; + bucketInfoDiv.id = 'bucket-info'; + + let bucketColorsHTML = ''; + for (let i = 0; i < bucketCount; i++) { + const color = bucketColors[i % bucketColors.length]; + bucketColorsHTML += 'B' + i + ' '; + } + bucketInfoDiv.innerHTML = '
Bucket Color Guide:
' + bucketColorsHTML + '
'; + arrayViz.appendChild(bucketInfoDiv); + + // Add controls with legend + const controlsDiv = document.createElement('div'); + controlsDiv.className = 'viz-controls'; + controlsDiv.innerHTML = + '

Bucket Sort Visualization (' + bucketCount + ' buckets)

' + + '' + + '' + + '' + + '
' + + 'Color by Bucket | Sort Each Color Group | Collect Results | Complete' + + '' + + '
'; + arrayViz.appendChild(controlsDiv); + + // Status display + const statusDiv = document.createElement('div'); + statusDiv.id = 'bucket-status'; + statusDiv.className = 'viz-status'; + statusDiv.textContent = 'Ready to start bucket sort animation...'; + arrayViz.appendChild(statusDiv); + + // Animation variables + let currentStepIndex = 0; + let animationRunning = false; + let animationInterval; + + function updateBucketVisualization(step) { + const cells = arrayDiv.querySelectorAll('.viz-cell'); + const statusDiv = document.getElementById('bucket-status'); + + // Reset all cell classes + cells.forEach(cell => { + cell.className = 'viz-cell'; + }); + + // Update array values if changed + if (step.array) { + step.array.forEach((value, index) => { + if (cells[index] && value !== null && value !== undefined) { + cells[index].textContent = value; + } + }); + } + + // Apply bucket color coding based on step type and phase + if (step.type === 'distribute' && step.targetBucket !== undefined) { + // Show current element being distributed with preview of target bucket color + if (step.currentElementIndex !== undefined && cells[step.currentElementIndex]) { + cells[step.currentElementIndex].classList.add('distributing'); + const targetColor = bucketColors[step.targetBucket % bucketColors.length]; + cells[step.currentElementIndex].style.backgroundColor = targetColor; // Exact color match with legend + cells[step.currentElementIndex].style.borderColor = targetColor; + cells[step.currentElementIndex].style.color = '#ffffff'; + cells[step.currentElementIndex].style.fontWeight = 'bold'; + cells[step.currentElementIndex].setAttribute('data-bucket', step.targetBucket); + } + } else if (step.phase === 'distribution-complete' || step.phase === 'bucket-sorting' || step.phase === 'collection') { + // Color all elements by their bucket assignment + if (step.buckets) { + step.buckets.forEach((bucket, bucketIndex) => { + bucket.forEach(value => { + // Find the cell with this value and color it + const matchingCells = Array.from(cells).filter(cell => + parseFloat(cell.textContent) === value || cell.textContent === value.toString() + ); + matchingCells.forEach(cell => { + cell.style.backgroundColor = bucketColors[bucketIndex % bucketColors.length]; // Exact color match with legend + cell.style.borderColor = bucketColors[bucketIndex % bucketColors.length]; + cell.style.color = '#ffffff'; // White text for better contrast + cell.style.fontWeight = 'bold'; + cell.setAttribute('data-bucket', bucketIndex); + }); + }); + }); + } + } + + // Highlight current bucket being sorted + if (step.currentBucket !== undefined && step.phase === 'bucket-sorting') { + cells.forEach(cell => { + if (cell.getAttribute('data-bucket') === step.currentBucket.toString()) { + cell.classList.add('bucket-sorting'); + } + }); + } + + // Show collection phase + if (step.type === 'collect') { + cells.forEach((cell, index) => { + if (index <= step.collectedTo) { + cell.classList.add('collected'); + } + }); + } + + // Final completion state + if (step.type === 'complete') { + cells.forEach(cell => { + cell.classList.add('complete'); + // Reset to default text styling + cell.style.color = ''; + cell.style.fontWeight = ''; + }); + } + + // Update status + statusDiv.textContent = step.message; + + // Show step info in container + const stepInfo = document.createElement('div'); + stepInfo.className = step.type === 'complete' ? 'viz-step-info complete' : 'viz-step-info'; + + let stepTypeColor = '#007acc'; + if (step.type === 'complete') stepTypeColor = '#28a745'; + else if (step.type === 'distribute') stepTypeColor = '#2196f3'; + else if (step.phase === 'bucket-sorting') stepTypeColor = '#ff9800'; + else if (step.type === 'collect') stepTypeColor = '#4caf50'; + + stepInfo.style.borderLeftColor = stepTypeColor; + + let phaseEmoji = '*'; + if (step.type === 'distribute') phaseEmoji = '+'; + else if (step.phase === 'bucket-sorting') phaseEmoji = '~'; + else if (step.type === 'collect') phaseEmoji = '-'; + else if (step.type === 'complete') phaseEmoji = '!'; + else if (step.type === 'initialize') phaseEmoji = '?'; + + stepInfo.innerHTML = + '' + phaseEmoji + ' Step ' + (currentStepIndex + 1) + ': ' + step.message + '
' + + '' + + 'Phase: ' + (step.phase || 'processing') + ' | ' + + 'Comparisons: ' + (step.metrics.comparisons || 0) + ' | ' + + 'Operations: ' + (step.metrics.bucketOperations || 0) + + ''; + + if (stepsContainer.children.length > 8) { + stepsContainer.removeChild(stepsContainer.firstChild); + } + stepsContainer.appendChild(stepInfo); + } + + function startBucketAnimation() { + if (animationRunning || currentStepIndex >= steps.length) return; + + animationRunning = true; + document.getElementById('start-bucket-animation').disabled = true; + document.getElementById('pause-bucket-animation').disabled = false; + + animationInterval = setInterval(() => { + if (currentStepIndex >= steps.length) { + clearInterval(animationInterval); + animationRunning = false; + document.getElementById('start-bucket-animation').disabled = false; + document.getElementById('pause-bucket-animation').disabled = true; + return; + } + + updateBucketVisualization(steps[currentStepIndex]); + currentStepIndex++; + }, 1500); // 1.5 second delay between steps + } + + function pauseBucketAnimation() { + clearInterval(animationInterval); + animationRunning = false; + document.getElementById('start-bucket-animation').disabled = false; + document.getElementById('pause-bucket-animation').disabled = true; + } + + function resetBucketAnimation() { + clearInterval(animationInterval); + animationRunning = false; + currentStepIndex = 0; + document.getElementById('start-bucket-animation').disabled = false; + document.getElementById('pause-bucket-animation').disabled = true; + stepsContainer.innerHTML = ''; + + // Reset all cells to default state + const cells = arrayDiv.querySelectorAll('.viz-cell'); + cells.forEach(cell => { + cell.className = 'viz-cell'; + cell.style.backgroundColor = ''; + cell.style.borderColor = ''; + cell.style.color = ''; + cell.style.fontWeight = ''; + cell.removeAttribute('data-bucket'); + }); + + // Reset visualization to initial state + if (steps.length > 0) { + updateBucketVisualization(steps[0]); + } + document.getElementById('bucket-status').textContent = 'Ready to start bucket sort animation...'; + } + + // Bind control events + document.getElementById('start-bucket-animation').addEventListener('click', startBucketAnimation); + document.getElementById('pause-bucket-animation').addEventListener('click', pauseBucketAnimation); + document.getElementById('reset-bucket-animation').addEventListener('click', resetBucketAnimation); + + // Show initial state + if (steps.length > 0) { + updateBucketVisualization(steps[0]); + } + } ` }; // Export for both Node.js and browser environments if (typeof module !== 'undefined' && module.exports) { - module.exports = BucketSortConfig; + module.exports = { + BUCKET_SORT_CONFIG: BucketSortConfig, + BucketSortConfig + }; } else if (typeof window !== 'undefined') { + // Primary exports + window.BUCKET_SORT_CONFIG = BucketSortConfig; window.BucketSortConfig = BucketSortConfig; - // Additional exports for universal loader compatibility - window.BUCKET_SORT_CONFIG = BucketSortConfig; - window.bucketsortConfig = BucketSortConfig; - window.bucketsortconfig = BucketSortConfig; + // Universal loader compatibility - these are the names the loader looks for + window.bucketsortConfig = BucketSortConfig; // algorithmName.replace(/-/g, '') + 'Config' + window.bucketsortconfig = BucketSortConfig; // algorithmName.replace(/-/g, '').toLowerCase() + 'Config' + window.bucketSortConfig = BucketSortConfig; // toCamelCase(algorithmName) + 'Config' } diff --git a/algorithms-js/src/sort/counting-sort/counting-sort-steps.js b/algorithms-js/src/sort/counting-sort/counting-sort-steps.js index 9c1679a7..277fcec8 100644 --- a/algorithms-js/src/sort/counting-sort/counting-sort-steps.js +++ b/algorithms-js/src/sort/counting-sort/counting-sort-steps.js @@ -228,6 +228,12 @@ if (typeof module !== 'undefined' && module.exports) { window.CountingSortSteps = { countingSortWithSteps }; - // Expose commonly used functions in global scope for demo configs + + // Additional exports for universal loader compatibility window.countingSortWithSteps = countingSortWithSteps; -} \ No newline at end of file + + // Alternative naming variants + window.COUNTING_SORT_STEPS = { + countingSortWithSteps + }; +} diff --git a/algorithms-js/src/sort/counting-sort/counting-sort.config.js b/algorithms-js/src/sort/counting-sort/counting-sort.config.js index 8538bf74..9710ab96 100644 --- a/algorithms-js/src/sort/counting-sort/counting-sort.config.js +++ b/algorithms-js/src/sort/counting-sort/counting-sort.config.js @@ -355,27 +355,332 @@ const CountingSortConfig = { try { const startTime = performance.now(); - // Execute counting sort - const result = window.CountingSortCore ? window.CountingSortCore.countingSort(arrayInput) : countingSort(arrayInput); + // Execute counting sort using steps function for animation + let result; + if (window.CountingSortSteps) { + result = window.CountingSortSteps.countingSortWithSteps(arrayInput); + } else if (window.countingSortWithSteps) { + result = window.countingSortWithSteps(arrayInput); + } else if (window.CountingSortCore) { + const coreResult = window.CountingSortCore.countingSort(arrayInput); + result = { ...coreResult, steps: [] }; + } else { + result = { sortedArray: [...arrayInput].sort((a, b) => a - b), metrics: { comparisons: 0, counts: 0, range: 0 }, steps: [] }; + } const endTime = performance.now(); const executionTime = (endTime - startTime).toFixed(4); // Show result - let resultHTML = \` - Original Array: [\${arrayInput.join(', ')}]
- Sorted Array: [\${result.sortedArray.join(', ')}]
- Range (k): \${result.metrics.range} (0 to \${result.metrics.maxValue})
- Space Used: O(n + k) = O(\${arrayInput.length} + \${result.metrics.range})
- Execution Time: \${executionTime} ms - \`; + let resultHTML = + 'Original Array: [' + arrayInput.join(', ') + ']
' + + 'Sorted Array: [' + result.sortedArray.join(', ') + ']
' + + 'Range (k): ' + (result.metrics.range || 'N/A') + '
' + + 'Comparisons: ' + (result.metrics.comparisons || 0) + '
' + + 'Count Operations: ' + (result.metrics.counts || 0) + '
' + + 'Execution Time: ' + executionTime + ' ms'; resultContainer.innerHTML = resultHTML; + // Show the visualization section with counting sort animation + if (result.steps && result.steps.length > 0) { + showCountingSortVisualization(arrayInput, result.steps); + visualizationSection.style.display = 'block'; + } + } catch (error) { showError(error.message); } } + + function showCountingSortVisualization(originalArray, steps) { + const arrayViz = document.getElementById('array-visualization'); + const stepsContainer = document.getElementById('steps-container'); + + // Clear previous visualization + arrayViz.innerHTML = ''; + stepsContainer.innerHTML = ''; + + // Create main containers + const originalArrayDiv = document.createElement('div'); + originalArrayDiv.className = 'counting-sort-section'; + originalArrayDiv.innerHTML = '

Original Array

'; + + const arrayDisplay = document.createElement('div'); + arrayDisplay.className = 'array-visualization'; + arrayDisplay.id = 'counting-original-array'; + + originalArray.forEach((value, index) => { + const cell = document.createElement('div'); + cell.textContent = value; + cell.className = 'viz-cell counting-cell'; + cell.setAttribute('data-index', index); + cell.setAttribute('data-value', value); + arrayDisplay.appendChild(cell); + }); + + originalArrayDiv.appendChild(arrayDisplay); + arrayViz.appendChild(originalArrayDiv); + + // Create counting array section + const countingSection = document.createElement('div'); + countingSection.className = 'counting-sort-section'; + countingSection.innerHTML = '

Counting Array

'; + countingSection.id = 'counting-array-section'; + countingSection.style.display = 'none'; + + const countingArrayDiv = document.createElement('div'); + countingArrayDiv.className = 'counting-array-display'; + countingArrayDiv.id = 'counting-array-display'; + countingSection.appendChild(countingArrayDiv); + + arrayViz.appendChild(countingSection); + + // Create output array section + const outputSection = document.createElement('div'); + outputSection.className = 'counting-sort-section'; + outputSection.innerHTML = '

Output Array (Building Result)

'; + outputSection.id = 'output-array-section'; + outputSection.style.display = 'none'; + + const outputArrayDiv = document.createElement('div'); + outputArrayDiv.className = 'array-visualization'; + outputArrayDiv.id = 'counting-output-array'; + outputSection.appendChild(outputArrayDiv); + + arrayViz.appendChild(outputSection); + + // Add controls with legend + const controlsDiv = document.createElement('div'); + controlsDiv.className = 'viz-controls'; + controlsDiv.innerHTML = + '

Counting Sort Visualization

' + + '' + + '' + + '' + + '
' + + 'πŸ” Find Max | πŸ“Š Count Values | πŸ”’ Cumulative Sum | πŸ“ Place Elements | βœ… Complete' + + '' + + '
'; + arrayViz.appendChild(controlsDiv); + + // Status display + const statusDiv = document.createElement('div'); + statusDiv.id = 'counting-status'; + statusDiv.className = 'viz-status'; + statusDiv.textContent = 'Ready to start counting sort animation...'; + arrayViz.appendChild(statusDiv); + + // Animation variables + let currentStepIndex = 0; + let animationRunning = false; + let animationInterval; + + function updateCountingVisualization(step) { + const originalCells = arrayDisplay.querySelectorAll('.viz-cell'); + const statusDiv = document.getElementById('counting-status'); + const countingSection = document.getElementById('counting-array-section'); + const outputSection = document.getElementById('output-array-section'); + const countingDisplay = document.getElementById('counting-array-display'); + const outputDisplay = document.getElementById('counting-output-array'); + + // Reset all original array cell classes + originalCells.forEach(cell => { + cell.className = 'viz-cell counting-cell'; + }); + + // Handle different phases + if (step.phase === 'find-max') { + step.highlightIndices.forEach(index => { + if (originalCells[index]) { + originalCells[index].classList.add('finding-max'); + } + }); + } else if (step.phase === 'count') { + countingSection.style.display = 'block'; + + // Create/update counting array display + if (step.countArray) { + countingDisplay.innerHTML = ''; + step.countArray.forEach((count, value) => { + const countCell = document.createElement('div'); + countCell.className = 'counting-cell-container'; + + const valueLabel = document.createElement('div'); + valueLabel.className = 'counting-value-label'; + valueLabel.textContent = value; + + const countValue = document.createElement('div'); + countValue.className = 'counting-count-value'; + countValue.textContent = count; + countValue.setAttribute('data-count', count); + + if (step.countingValue === value) { + countValue.classList.add('counting-active'); + } + + countCell.appendChild(valueLabel); + countCell.appendChild(countValue); + countingDisplay.appendChild(countCell); + }); + } + + step.highlightIndices.forEach(index => { + if (originalCells[index]) { + originalCells[index].classList.add('being-counted'); + } + }); + } else if (step.phase === 'cumulative') { + // Update counting array to show cumulative sums + if (step.countArray) { + const countCells = countingDisplay.querySelectorAll('.counting-count-value'); + step.countArray.forEach((count, value) => { + if (countCells[value]) { + countCells[value].textContent = count; + countCells[value].setAttribute('data-count', count); + + if (step.cumulativeIndex === value) { + countCells[value].classList.add('cumulative-active'); + } + } + }); + } + } else if (step.phase === 'place') { + outputSection.style.display = 'block'; + + // Create/update output array display + if (step.output) { + outputDisplay.innerHTML = ''; + step.output.forEach((value, index) => { + const cell = document.createElement('div'); + cell.className = 'viz-cell output-cell'; + if (value !== undefined) { + cell.textContent = value; + cell.classList.add('placed'); + + if (step.placingPosition === index) { + cell.classList.add('just-placed'); + } + } + outputDisplay.appendChild(cell); + }); + } + + step.highlightIndices.forEach(index => { + if (originalCells[index]) { + originalCells[index].classList.add('being-placed'); + } + }); + } else if (step.phase === 'complete') { + originalCells.forEach(cell => { + cell.classList.add('complete'); + }); + + const outputCells = outputDisplay.querySelectorAll('.viz-cell'); + outputCells.forEach(cell => { + cell.classList.add('complete'); + }); + } + + // Update status + statusDiv.textContent = step.message; + + // Show step info in container + const stepInfo = document.createElement('div'); + stepInfo.className = step.type === 'complete' ? 'viz-step-info complete' : 'viz-step-info'; + + let stepTypeColor = '#007acc'; + if (step.phase === 'find-max') stepTypeColor = '#f44336'; + else if (step.phase === 'count') stepTypeColor = '#4caf50'; + else if (step.phase === 'cumulative') stepTypeColor = '#ff9800'; + else if (step.phase === 'place') stepTypeColor = '#2196f3'; + else if (step.phase === 'complete') stepTypeColor = '#8bc34a'; + + stepInfo.style.borderLeftColor = stepTypeColor; + + let phaseEmoji = 'πŸ”„'; + if (step.phase === 'find-max') phaseEmoji = 'πŸ”'; + else if (step.phase === 'count') phaseEmoji = 'πŸ“Š'; + else if (step.phase === 'cumulative') phaseEmoji = 'πŸ”’'; + else if (step.phase === 'place') phaseEmoji = 'πŸ“'; + else if (step.phase === 'complete') phaseEmoji = 'βœ…'; + + stepInfo.innerHTML = + '' + phaseEmoji + ' Step ' + (currentStepIndex + 1) + ': ' + step.message + '
' + + '' + + 'Phase: ' + (step.phase || 'processing') + ' | ' + + 'Comparisons: ' + (step.comparisons || 0) + ' | ' + + 'Count Ops: ' + (step.counts || 0) + + ''; + + if (stepsContainer.children.length > 8) { + stepsContainer.removeChild(stepsContainer.firstChild); + } + stepsContainer.appendChild(stepInfo); + } + + function startCountingAnimation() { + if (animationRunning || currentStepIndex >= steps.length) return; + + animationRunning = true; + document.getElementById('start-counting-animation').disabled = true; + document.getElementById('pause-counting-animation').disabled = false; + + animationInterval = setInterval(() => { + if (currentStepIndex >= steps.length) { + clearInterval(animationInterval); + animationRunning = false; + document.getElementById('start-counting-animation').disabled = false; + document.getElementById('pause-counting-animation').disabled = true; + return; + } + + updateCountingVisualization(steps[currentStepIndex]); + currentStepIndex++; + }, 1000); // 1 second delay between steps + } + + function pauseCountingAnimation() { + clearInterval(animationInterval); + animationRunning = false; + document.getElementById('start-counting-animation').disabled = false; + document.getElementById('pause-counting-animation').disabled = true; + } + + function resetCountingAnimation() { + clearInterval(animationInterval); + animationRunning = false; + currentStepIndex = 0; + document.getElementById('start-counting-animation').disabled = false; + document.getElementById('pause-counting-animation').disabled = true; + stepsContainer.innerHTML = ''; + + // Reset visualization + document.getElementById('counting-array-section').style.display = 'none'; + document.getElementById('output-array-section').style.display = 'none'; + + if (steps.length > 0) { + updateCountingVisualization(steps[0]); + } + document.getElementById('counting-status').textContent = 'Ready to start counting sort animation...'; + } + + // Bind control events + document.getElementById('start-counting-animation').addEventListener('click', startCountingAnimation); + document.getElementById('pause-counting-animation').addEventListener('click', pauseCountingAnimation); + document.getElementById('reset-counting-animation').addEventListener('click', resetCountingAnimation); + + // Show initial state + if (steps.length > 0) { + updateCountingVisualization(steps[0]); + } + } ` }; diff --git a/algorithms-js/src/sort/dutch-flag-sort/dutch-flag-sort-core.js b/algorithms-js/src/sort/dutch-flag-sort/dutch-flag-sort-core.js index 69f94800..4125d490 100644 --- a/algorithms-js/src/sort/dutch-flag-sort/dutch-flag-sort-core.js +++ b/algorithms-js/src/sort/dutch-flag-sort/dutch-flag-sort-core.js @@ -13,14 +13,10 @@ * @see https://github.com/sachinlala/SimplifyLearning */ -// Import utilities for reusable functions -// Import utilities for reusable functions -// Note: SortingUtils is available globally via window.SortingUtils when loaded by universal loader +// SortingUtils is available globally via window.SortingUtils when loaded by universal loader if (typeof require !== 'undefined') { - // Node.js environment const SortingUtils = require('../utils/sorting-utils.js'); } -// In browser environment, SortingUtils is available via window.SortingUtils /** * Core Dutch National Flag sort algorithm diff --git a/algorithms-js/src/sort/dutch-flag-sort/dutch-flag-sort-steps.js b/algorithms-js/src/sort/dutch-flag-sort/dutch-flag-sort-steps.js index 2f194b9e..ce72e100 100644 --- a/algorithms-js/src/sort/dutch-flag-sort/dutch-flag-sort-steps.js +++ b/algorithms-js/src/sort/dutch-flag-sort/dutch-flag-sort-steps.js @@ -2,36 +2,424 @@ * Dutch Flag Sort - Step Tracking for Visualization * * This file contains step-by-step tracking logic for Dutch Flag Sort visualization. - * Currently placeholder - to be implemented in future animation updates. + * Implements detailed step tracking for both 2-way and 3-way partitioning. * * @see https://github.com/sachinlala/SimplifyLearning */ /** * Dutch Flag Sort with step-by-step tracking for visualization - * @param {number[]} arr - Array to be sorted + * @param {any[]} arr - Array to be sorted + * @param {any} redValue - Red group value + * @param {any} whiteValue - White group value (null for 2-way partitioning) + * @param {any} blueValue - Blue group value * @returns {Object} Result with sorted array, steps, and metrics */ -function dutchFlagSortWithSteps(arr) { - // TODO: Implement step-by-step tracking for Dutch Flag Sort visualization - // This is a placeholder that will be enhanced in future iterations +function dutchFlagSortWithSteps(arr, redValue, whiteValue, blueValue) { + if (!arr || arr.length <= 1) { + return { + sortedArray: arr || [], + steps: [], + metrics: { comparisons: 0, swaps: 0, partitions: { red: 0, white: 0, blue: 0 } } + }; + } + + const sortedArray = [...arr]; + const n = sortedArray.length; + const steps = []; + let comparisons = 0; + let swaps = 0; + + // Handle two-value case (Polish flag) - whiteValue is implicit + const isTwoValueSort = (whiteValue === undefined || whiteValue === null); + const partitionType = isTwoValueSort ? '2-way' : '3-way'; + let redCount = 0; + let whiteCount = 0; + let blueCount = 0; + + // Initial state + steps.push({ + type: 'start', + array: [...sortedArray], + message: `Starting Dutch Flag Sort (${partitionType} partitioning)`, + phase: 'initialization', + partitionType, + redValue, + whiteValue: isTwoValueSort ? 'implicit' : whiteValue, + blueValue, + comparisons, + swaps, + partitions: { red: redCount, white: whiteCount, blue: blueCount }, + highlightIndices: [] + }); + + // Explain the algorithm + if (isTwoValueSort) { + steps.push({ + type: 'explanation', + array: [...sortedArray], + message: `Two-way partitioning: ${JSON.stringify(redValue)} | everything else | ${JSON.stringify(blueValue)}`, + phase: 'explanation', + partitionType, + redValue, + whiteValue: 'implicit', + blueValue, + comparisons, + swaps, + partitions: { red: redCount, white: whiteCount, blue: blueCount }, + highlightIndices: [] + }); + } else { + steps.push({ + type: 'explanation', + array: [...sortedArray], + message: `Three-way partitioning: ${JSON.stringify(redValue)} | ${JSON.stringify(whiteValue)} | ${JSON.stringify(blueValue)}`, + phase: 'explanation', + partitionType, + redValue, + whiteValue, + blueValue, + comparisons, + swaps, + partitions: { red: redCount, white: whiteCount, blue: blueCount }, + highlightIndices: [] + }); + } + + // Initialize pointers + let red = 0; // boundary for red section (left) + let white = n - 1; // current element being processed (from right) + let blue = n - 1; // boundary for blue section (right) + + steps.push({ + type: 'initialize-pointers', + array: [...sortedArray], + message: 'Initialize pointers: red=0, white=n-1, blue=n-1. Processing from right to left.', + phase: 'initialization', + partitionType, + pointers: { red, white, blue }, + redValue, + whiteValue: isTwoValueSort ? 'implicit' : whiteValue, + blueValue, + comparisons, + swaps, + partitions: { red: redCount, white: whiteCount, blue: blueCount }, + highlightIndices: [red, white, blue] + }); + + // Main partitioning loop + while (white >= red) { + const current = sortedArray[white]; + comparisons++; + + steps.push({ + type: 'examine-element', + array: [...sortedArray], + message: `Examining element ${JSON.stringify(current)} at position ${white}`, + phase: 'partitioning', + partitionType, + currentElement: current, + currentIndex: white, + pointers: { red, white, blue }, + redValue, + whiteValue: isTwoValueSort ? 'implicit' : whiteValue, + blueValue, + comparisons, + swaps, + partitions: { red: redCount, white: whiteCount, blue: blueCount }, + highlightIndices: [white] + }); + + if (current === redValue) { + // Element belongs in red section + steps.push({ + type: 'identify-red', + array: [...sortedArray], + message: `${JSON.stringify(current)} belongs in red section`, + phase: 'partitioning', + partitionType, + currentElement: current, + targetSection: 'red', + pointers: { red, white, blue }, + redValue, + whiteValue: isTwoValueSort ? 'implicit' : whiteValue, + blueValue, + comparisons, + swaps, + partitions: { red: redCount, white: whiteCount, blue: blueCount }, + highlightIndices: [red, white] + }); + + // Swap with red boundary if needed + if (red !== white) { + const redElement = sortedArray[red]; + [sortedArray[red], sortedArray[white]] = [sortedArray[white], sortedArray[red]]; + swaps++; + + steps.push({ + type: 'swap-to-red', + array: [...sortedArray], + message: `Swapped ${JSON.stringify(current)} with ${JSON.stringify(redElement)} to expand red section`, + phase: 'partitioning', + partitionType, + swappedElements: [current, redElement], + swappedIndices: [red, white], + targetSection: 'red', + pointers: { red, white, blue }, + redValue, + whiteValue: isTwoValueSort ? 'implicit' : whiteValue, + blueValue, + comparisons, + swaps, + partitions: { red: redCount, white: whiteCount, blue: blueCount }, + highlightIndices: [red, white] + }); + } else { + steps.push({ + type: 'no-swap-red', + array: [...sortedArray], + message: `${JSON.stringify(current)} is already in position for red section`, + phase: 'partitioning', + partitionType, + currentElement: current, + targetSection: 'red', + pointers: { red, white, blue }, + redValue, + whiteValue: isTwoValueSort ? 'implicit' : whiteValue, + blueValue, + comparisons, + swaps, + partitions: { red: redCount, white: whiteCount, blue: blueCount }, + highlightIndices: [red] + }); + } + + redCount++; + red++; // Expand red section + + } else if (current === blueValue) { + // Element belongs in blue section + steps.push({ + type: 'identify-blue', + array: [...sortedArray], + message: `${JSON.stringify(current)} belongs in blue section`, + phase: 'partitioning', + partitionType, + currentElement: current, + targetSection: 'blue', + pointers: { red, white, blue }, + redValue, + whiteValue: isTwoValueSort ? 'implicit' : whiteValue, + blueValue, + comparisons, + swaps, + partitions: { red: redCount, white: whiteCount, blue: blueCount }, + highlightIndices: [white, blue] + }); + + // Swap with blue boundary if needed + if (white !== blue) { + const blueElement = sortedArray[blue]; + [sortedArray[white], sortedArray[blue]] = [sortedArray[blue], sortedArray[white]]; + swaps++; + + steps.push({ + type: 'swap-to-blue', + array: [...sortedArray], + message: `Swapped ${JSON.stringify(current)} with ${JSON.stringify(blueElement)} to expand blue section`, + phase: 'partitioning', + partitionType, + swappedElements: [current, blueElement], + swappedIndices: [white, blue], + targetSection: 'blue', + pointers: { red, white, blue }, + redValue, + whiteValue: isTwoValueSort ? 'implicit' : whiteValue, + blueValue, + comparisons, + swaps, + partitions: { red: redCount, white: whiteCount, blue: blueCount }, + highlightIndices: [white, blue] + }); + } else { + steps.push({ + type: 'no-swap-blue', + array: [...sortedArray], + message: `${JSON.stringify(current)} is already in position for blue section`, + phase: 'partitioning', + partitionType, + currentElement: current, + targetSection: 'blue', + pointers: { red, white, blue }, + redValue, + whiteValue: isTwoValueSort ? 'implicit' : whiteValue, + blueValue, + comparisons, + swaps, + partitions: { red: redCount, white: whiteCount, blue: blueCount }, + highlightIndices: [blue] + }); + } + + blueCount++; + blue--; // Shrink blue boundary + white--; // Move back to check the swapped element + + } else { + // Element belongs in white section (middle) or is unknown + const isWhiteElement = (isTwoValueSort || current === whiteValue); + + steps.push({ + type: 'identify-white', + array: [...sortedArray], + message: isWhiteElement ? + `${JSON.stringify(current)} belongs in ${isTwoValueSort ? 'middle' : 'white'} section` : + `${JSON.stringify(current)} is unknown, treating as ${isTwoValueSort ? 'middle' : 'white'} element`, + phase: 'partitioning', + partitionType, + currentElement: current, + targetSection: 'white', + isUnknownElement: !isWhiteElement, + pointers: { red, white, blue }, + redValue, + whiteValue: isTwoValueSort ? 'implicit' : whiteValue, + blueValue, + comparisons, + swaps, + partitions: { red: redCount, white: whiteCount, blue: blueCount }, + highlightIndices: [white] + }); + + whiteCount++; + white--; // Element stays in place, move to next + } + + // Show updated boundaries after each operation + if (steps.length > 0) { + const lastStep = steps[steps.length - 1]; + if (lastStep.type !== 'update-boundaries') { + steps.push({ + type: 'update-boundaries', + array: [...sortedArray], + message: `Updated boundaries: red=[0,${red-1}], white=[${red},${blue}], blue=[${blue+1},${n-1}]`, + phase: 'partitioning', + partitionType, + pointers: { red, white, blue }, + sections: { + red: { start: 0, end: red - 1, count: redCount }, + white: { start: red, end: blue, count: whiteCount }, + blue: { start: blue + 1, end: n - 1, count: blueCount } + }, + redValue, + whiteValue: isTwoValueSort ? 'implicit' : whiteValue, + blueValue, + comparisons, + swaps, + partitions: { red: redCount, white: whiteCount, blue: blueCount }, + highlightIndices: [] + }); + } + } + } + + // Final validation and summary + steps.push({ + type: 'complete', + array: [...sortedArray], + message: `Dutch Flag Sort complete! Partitioned into ${redCount} red, ${whiteCount} ${isTwoValueSort ? 'middle' : 'white'}, and ${blueCount} blue elements in ${swaps} swaps.`, + phase: 'complete', + partitionType, + finalSections: { + red: { count: redCount, values: sortedArray.slice(0, red) }, + white: { count: whiteCount, values: sortedArray.slice(red, blue + 1) }, + blue: { count: blueCount, values: sortedArray.slice(blue + 1) } + }, + redValue, + whiteValue: isTwoValueSort ? 'implicit' : whiteValue, + blueValue, + comparisons, + swaps, + partitions: { red: redCount, white: whiteCount, blue: blueCount }, + highlightIndices: [] + }); + return { - sortedArray: [...arr], - steps: [], - metrics: { comparisons: 0, swaps: 0, partitions: 0 } + sortedArray, + steps, + metrics: { + comparisons, + swaps, + partitions: { red: redCount, white: whiteCount, blue: blueCount }, + redValue, + whiteValue: isTwoValueSort ? 'implicit' : whiteValue, + blueValue + } }; } +/** + * Three-way Dutch Flag Sort with step tracking + * @param {any[]} arr - Array to partition + * @param {any} redValue - First group value + * @param {any} whiteValue - Second group value + * @param {any} blueValue - Third group value + * @returns {Object} Result with sorted array, steps, and metrics + */ +function dutchFlagSort3WayWithSteps(arr, redValue, whiteValue, blueValue) { + return dutchFlagSortWithSteps(arr, redValue, whiteValue, blueValue); +} + +/** + * Two-way Dutch Flag Sort with step tracking + * @param {any[]} arr - Array to partition + * @param {any} firstValue - First group value + * @param {any} secondValue - Second group value + * @returns {Object} Result with sorted array, steps, and metrics + */ +function dutchFlagSort2WayWithSteps(arr, firstValue, secondValue) { + return dutchFlagSortWithSteps(arr, firstValue, null, secondValue); +} + +/** + * Sort colors with step tracking (classic Dutch flag problem) + * @param {string[]} arr - Array of color strings + * @returns {Object} Result with sorted array, steps, and metrics + */ +function sortColorsWithSteps(arr) { + return dutchFlagSortWithSteps(arr, 'red', 'white', 'blue'); +} + +/** + * Sort 0s, 1s, and 2s with step tracking + * @param {number[]} arr - Array of 0s, 1s, and 2s + * @returns {Object} Result with sorted array, steps, and metrics + */ +function sort012WithSteps(arr) { + return dutchFlagSortWithSteps(arr, 0, 1, 2); +} + // Export for both Node.js and browser environments if (typeof module !== 'undefined' && module.exports) { module.exports = { - dutchFlagSortWithSteps + dutchFlagSortWithSteps, + dutchFlagSort3WayWithSteps, + dutchFlagSort2WayWithSteps, + sortColorsWithSteps, + sort012WithSteps }; } else if (typeof window !== 'undefined') { window.DutchFlagSortSteps = { - dutchFlagSortWithSteps + dutchFlagSortWithSteps, + dutchFlagSort3WayWithSteps, + dutchFlagSort2WayWithSteps, + sortColorsWithSteps, + sort012WithSteps }; // Expose commonly used functions in global scope for demo configs window.dutchFlagSortWithSteps = dutchFlagSortWithSteps; -} \ No newline at end of file + window.dutchFlagSort3WayWithSteps = dutchFlagSort3WayWithSteps; + window.dutchFlagSort2WayWithSteps = dutchFlagSort2WayWithSteps; + window.sortColorsWithSteps = sortColorsWithSteps; + window.sort012WithSteps = sort012WithSteps; +} diff --git a/algorithms-js/src/sort/dutch-flag-sort/dutch-flag-sort.config.js b/algorithms-js/src/sort/dutch-flag-sort/dutch-flag-sort.config.js index 1b820f2b..7c711a8e 100644 --- a/algorithms-js/src/sort/dutch-flag-sort/dutch-flag-sort.config.js +++ b/algorithms-js/src/sort/dutch-flag-sort/dutch-flag-sort.config.js @@ -444,31 +444,229 @@ DUTCH_FLAG_SORT_CONFIG.customDemoFunction = ` try { const startTime = performance.now(); - // Execute dutch flag sort - const result = window.DutchFlagSortCore ? - window.DutchFlagSortCore.dutchFlagSort(arrayInput, redValue, blueValue) : - dutchFlagSort(arrayInput, redValue, blueValue); + // Execute dutch flag sort using steps function for animation + let result; + if (window.DutchFlagSortSteps) { + result = window.DutchFlagSortSteps.dutchFlagSortWithSteps(arrayInput, redValue, null, blueValue); + } else if (window.dutchFlagSortWithSteps) { + result = window.dutchFlagSortWithSteps(arrayInput, redValue, null, blueValue); + } else if (window.DutchFlagSortCore) { + const coreResult = window.DutchFlagSortCore.dutchFlagSort(arrayInput, redValue, blueValue); + result = { ...coreResult, steps: [] }; + } else { + result = { sortedArray: [...arrayInput].sort(), metrics: { comparisons: 0, swaps: 0 }, steps: [] }; + } const endTime = performance.now(); const executionTime = (endTime - startTime).toFixed(4); // Show result - let resultHTML = \` - Original Array: [\${arrayInput.join(', ')}]
- Partitioned Array: [\${result.sortedArray.join(', ')}]
- Red Group (\${redValue}): positions 0 to \${result.metrics.redEnd || 'N/A'}
- White/Other Group: middle section
- Blue Group (\${blueValue}): end section
- Total Swaps: \${result.metrics.swaps || 0}
- Execution Time: \${executionTime} ms - \`; + let resultHTML = + 'Original Array: [' + arrayInput.join(', ') + ']
' + + 'Partitioned Array: [' + result.sortedArray.join(', ') + ']
' + + 'Red Group (' + redValue + '): first section
' + + 'White/Other Group: middle section
' + + 'Blue Group (' + blueValue + '): end section
' + + 'Total Swaps: ' + (result.metrics.swaps || 0) + '
' + + 'Execution Time: ' + executionTime + ' ms'; resultContainer.innerHTML = resultHTML; + // Show the visualization section with dutch flag animation + if (result.steps && result.steps.length > 0) { + showDutchFlagVisualization(arrayInput, result.steps, redValue, blueValue); + visualizationSection.style.display = 'block'; + } + } catch (error) { showError(error.message); } } + + function showDutchFlagVisualization(originalArray, steps, redValue, blueValue) { + const arrayViz = document.getElementById('array-visualization'); + const stepsContainer = document.getElementById('steps-container'); + + // Clear previous visualization + arrayViz.innerHTML = ''; + stepsContainer.innerHTML = ''; + + // Create array visualization + const arrayDiv = document.createElement('div'); + arrayDiv.className = 'array-visualization'; + arrayDiv.id = 'dutch-flag-array-display'; + + originalArray.forEach((value, index) => { + const cell = document.createElement('div'); + cell.textContent = value; + cell.className = 'viz-cell'; + cell.setAttribute('data-index', index); + cell.setAttribute('data-value', value); + arrayDiv.appendChild(cell); + }); + + arrayViz.appendChild(arrayDiv); + + // Add controls with legend + const controlsDiv = document.createElement('div'); + controlsDiv.className = 'viz-controls'; + controlsDiv.innerHTML = + '

Dutch Flag Sort Visualization

' + + '' + + '' + + '' + + '
' + + 'πŸ”΄ Red (' + redValue + ') | βšͺ White/Other | πŸ”΅ Blue (' + blueValue + ') | 🟑 Comparing | 🟒 Swapping' + + '' + + '
'; + arrayViz.appendChild(controlsDiv); + + // Status display + const statusDiv = document.createElement('div'); + statusDiv.id = 'dutch-flag-status'; + statusDiv.className = 'viz-status'; + statusDiv.textContent = 'Ready to start dutch flag sort animation...'; + arrayViz.appendChild(statusDiv); + + // Animation variables + let currentStepIndex = 0; + let animationRunning = false; + let animationInterval; + + function updateDutchFlagVisualization(step) { + const cells = arrayDiv.querySelectorAll('.viz-cell'); + const statusDiv = document.getElementById('dutch-flag-status'); + + // Reset all cell classes + cells.forEach(cell => { + cell.className = 'viz-cell'; + }); + + // Update array values + step.array.forEach((value, index) => { + if (cells[index]) { + cells[index].textContent = value; + } + }); + + // Color cells based on their partition group + step.array.forEach((value, index) => { + if (cells[index]) { + if (value === redValue) { + cells[index].classList.add('red-partition'); + } else if (value === blueValue) { + cells[index].classList.add('blue-partition'); + } else { + cells[index].classList.add('white-partition'); + } + } + }); + + // Highlight current indices being processed + if (step.highlightIndices) { + step.highlightIndices.forEach(index => { + if (cells[index]) { + cells[index].classList.add('comparing'); + } + }); + } + + // Highlight swapped indices + if (step.swappedIndices) { + step.swappedIndices.forEach(index => { + if (cells[index]) { + cells[index].classList.add('swapping'); + } + }); + } + + // Update status + statusDiv.textContent = step.message; + + // Show step info in container + const stepInfo = document.createElement('div'); + stepInfo.className = step.type === 'complete' ? 'viz-step-info complete' : 'viz-step-info'; + + let stepTypeColor = '#007acc'; + if (step.type === 'complete') stepTypeColor = '#28a745'; + else if (step.type === 'swap') stepTypeColor = '#dc3545'; + else if (step.type === 'compare') stepTypeColor = '#ffc107'; + + stepInfo.style.borderLeftColor = stepTypeColor; + + stepInfo.innerHTML = + 'Step ' + (currentStepIndex + 1) + ': ' + step.message + '
' + + '' + + 'Phase: ' + (step.phase || 'processing') + ' | ' + + 'Comparisons: ' + (step.comparisons || 0) + ' | ' + + 'Swaps: ' + (step.swaps || 0) + + ''; + + if (stepsContainer.children.length > 8) { + stepsContainer.removeChild(stepsContainer.firstChild); + } + stepsContainer.appendChild(stepInfo); + } + + function startDutchFlagAnimation() { + if (animationRunning || currentStepIndex >= steps.length) return; + + animationRunning = true; + document.getElementById('start-dutch-animation').disabled = true; + document.getElementById('pause-dutch-animation').disabled = false; + + animationInterval = setInterval(() => { + if (currentStepIndex >= steps.length) { + clearInterval(animationInterval); + animationRunning = false; + document.getElementById('start-dutch-animation').disabled = false; + document.getElementById('pause-dutch-animation').disabled = true; + return; + } + + updateDutchFlagVisualization(steps[currentStepIndex]); + currentStepIndex++; + }, 1200); // 1.2 second delay between steps + } + + function pauseDutchFlagAnimation() { + clearInterval(animationInterval); + animationRunning = false; + document.getElementById('start-dutch-animation').disabled = false; + document.getElementById('pause-dutch-animation').disabled = true; + } + + function resetDutchFlagAnimation() { + clearInterval(animationInterval); + animationRunning = false; + currentStepIndex = 0; + document.getElementById('start-dutch-animation').disabled = false; + document.getElementById('pause-dutch-animation').disabled = true; + stepsContainer.innerHTML = ''; + + // Reset visualization + if (steps.length > 0) { + updateDutchFlagVisualization(steps[0]); + } + document.getElementById('dutch-flag-status').textContent = 'Ready to start dutch flag sort animation...'; + } + + // Bind control events + document.getElementById('start-dutch-animation').addEventListener('click', startDutchFlagAnimation); + document.getElementById('pause-dutch-animation').addEventListener('click', pauseDutchFlagAnimation); + document.getElementById('reset-dutch-animation').addEventListener('click', resetDutchFlagAnimation); + + // Show initial state + if (steps.length > 0) { + updateDutchFlagVisualization(steps[0]); + } + } `; // Export for both Node.js and browser environments diff --git a/algorithms-js/src/sort/radix-sort/radix-sort-steps.js b/algorithms-js/src/sort/radix-sort/radix-sort-steps.js index 15bcb5dd..1acd2ef4 100644 --- a/algorithms-js/src/sort/radix-sort/radix-sort-steps.js +++ b/algorithms-js/src/sort/radix-sort/radix-sort-steps.js @@ -189,6 +189,18 @@ if (typeof window !== 'undefined') { getDigit, getMaxDigits }; + + // Additional exports for universal loader compatibility + window.radixSortWithSteps = radixSortWithSteps; + window.getDigit = getDigit; + window.getMaxDigits = getMaxDigits; + + // Alternative naming variants + window.RADIX_SORT_STEPS = { + radixSortWithSteps, + getDigit, + getMaxDigits + }; } // Export for CommonJS compatibility diff --git a/algorithms-js/src/sort/radix-sort/radix-sort.config.js b/algorithms-js/src/sort/radix-sort/radix-sort.config.js index 338335ac..0af16ec1 100644 --- a/algorithms-js/src/sort/radix-sort/radix-sort.config.js +++ b/algorithms-js/src/sort/radix-sort/radix-sort.config.js @@ -370,28 +370,362 @@ const RadixSortConfig = { try { const startTime = performance.now(); - // Execute radix sort - const result = window.RadixSortCore ? window.RadixSortCore.radixSort(arrayInput) : radixSort(arrayInput); + // Execute radix sort using steps function for animation + let result; + if (window.RadixSortSteps) { + result = window.RadixSortSteps.radixSortWithSteps(arrayInput); + } else if (window.radixSortWithSteps) { + result = window.radixSortWithSteps(arrayInput); + } else if (window.RadixSortCore) { + const coreResult = window.RadixSortCore.radixSort(arrayInput); + result = { ...coreResult, steps: [] }; + } else { + result = { sortedArray: [...arrayInput].sort((a, b) => a - b), metrics: { passes: 0, bucketOperations: 0, maxDigits: 0 }, steps: [] }; + } const endTime = performance.now(); const executionTime = (endTime - startTime).toFixed(4); // Show result - let resultHTML = \` - Original Array: [\${arrayInput.join(', ')}]
- Sorted Array: [\${result.sortedArray.join(', ')}]
- Max Digits (d): \${result.metrics.maxDigits}
- Passes: \${result.metrics.passes}
- Total Operations: \${result.metrics.totalOperations}
- Execution Time: \${executionTime} ms - \`; + let resultHTML = + 'Original Array: [' + arrayInput.join(', ') + ']
' + + 'Sorted Array: [' + result.sortedArray.join(', ') + ']
' + + 'Max Digits (d): ' + (result.metrics.maxDigits || 0) + '
' + + 'Passes: ' + (result.metrics.passes || 0) + '
' + + 'Bucket Operations: ' + (result.metrics.bucketOperations || 0) + '
' + + 'Execution Time: ' + executionTime + ' ms'; resultContainer.innerHTML = resultHTML; + // Show the visualization section with radix sort animation + if (result.steps && result.steps.length > 0) { + showRadixSortVisualization(arrayInput, result.steps); + visualizationSection.style.display = 'block'; + } + } catch (error) { showError(error.message); } } + + function showRadixSortVisualization(originalArray, steps) { + const arrayViz = document.getElementById('array-visualization'); + const stepsContainer = document.getElementById('steps-container'); + + // Clear previous visualization + arrayViz.innerHTML = ''; + stepsContainer.innerHTML = ''; + + // Define digit bucket colors (10 distinct colors for digits 0-9) + const digitColors = [ + '#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#feca57', + '#ff9ff3', '#54a0ff', '#5f27cd', '#00d2d3', '#ff9f43' + ]; + + // Create array visualization + const arrayDiv = document.createElement('div'); + arrayDiv.className = 'array-visualization'; + arrayDiv.id = 'radix-array-display'; + + originalArray.forEach((value, index) => { + const cell = document.createElement('div'); + cell.textContent = value; + cell.className = 'viz-cell radix-cell'; + cell.setAttribute('data-index', index); + cell.setAttribute('data-value', value); + + // Add digit display container + const digitDisplay = document.createElement('div'); + digitDisplay.className = 'digit-display'; + digitDisplay.textContent = ''; + cell.appendChild(digitDisplay); + + arrayDiv.appendChild(cell); + }); + + arrayViz.appendChild(arrayDiv); + + // Create digit position display + const digitInfoDiv = document.createElement('div'); + digitInfoDiv.className = 'digit-info'; + digitInfoDiv.id = 'digit-info'; + digitInfoDiv.innerHTML = '
Current Digit Position: Ready to start...
'; + arrayViz.appendChild(digitInfoDiv); + + // Create compact color legend for digit assignments + const colorGuide = document.createElement('div'); + colorGuide.className = 'radix-color-guide'; + + const colorHeader = document.createElement('div'); + colorHeader.className = 'radix-color-header'; + colorHeader.innerHTML = 'Digit Colors:'; + colorGuide.appendChild(colorHeader); + + const colorRow = document.createElement('div'); + colorRow.className = 'radix-color-row'; + colorRow.style.textAlign = 'center'; + colorRow.style.lineHeight = '1'; + colorRow.style.whiteSpace = 'nowrap'; + colorRow.style.margin = '8px 0'; + + for (let digit = 0; digit <= 9; digit++) { + // Create color circle with digit inside - directly append to row + const colorCircle = document.createElement('span'); + colorCircle.className = 'radix-color-circle digit-' + digit; + colorCircle.title = 'Digit ' + digit + ' - ' + digitColors[digit]; + colorCircle.style.backgroundColor = digitColors[digit]; + colorCircle.style.color = 'white'; + colorCircle.style.fontWeight = 'bold'; + colorCircle.style.fontSize = '11px'; + colorCircle.style.display = 'inline-block'; + colorCircle.style.textAlign = 'center'; + colorCircle.style.lineHeight = '24px'; + colorCircle.style.width = '24px'; + colorCircle.style.height = '24px'; + colorCircle.style.borderRadius = '50%'; + colorCircle.style.border = '2px solid rgba(0,0,0,0.2)'; + colorCircle.style.textShadow = '0 1px 1px rgba(0,0,0,0.5)'; + colorCircle.style.margin = '0 4px'; + colorCircle.style.verticalAlign = 'middle'; + colorCircle.textContent = digit; + + colorRow.appendChild(colorCircle); + } + + colorGuide.appendChild(colorRow); + arrayViz.appendChild(colorGuide); + + // Add controls with legend + const controlsDiv = document.createElement('div'); + controlsDiv.className = 'viz-controls'; + controlsDiv.innerHTML = + '

Radix Sort Visualization

' + + '' + + '' + + '' + + '
' + + 'βœ‹ Extract | 🎨 Color by Digit | πŸ“¦ Distribute | πŸ”„ Collect | βœ… Complete' + + '' + + '
'; + arrayViz.appendChild(controlsDiv); + + // Status display + const statusDiv = document.createElement('div'); + statusDiv.id = 'radix-status'; + statusDiv.className = 'viz-status'; + statusDiv.textContent = 'Ready to start radix sort animation...'; + arrayViz.appendChild(statusDiv); + + // Animation variables + let currentStepIndex = 0; + let animationRunning = false; + let animationInterval; + + function updateRadixVisualization(step) { + const cells = arrayDiv.querySelectorAll('.viz-cell'); + const statusDiv = document.getElementById('radix-status'); + const digitPosSpan = document.getElementById('current-digit-pos'); + + // Reset all cell classes and styles + cells.forEach(cell => { + cell.className = 'viz-cell radix-cell'; + cell.style.backgroundColor = ''; + cell.style.color = ''; + cell.style.borderColor = ''; + const digitDisplay = cell.querySelector('.digit-display'); + if (digitDisplay) { + digitDisplay.textContent = ''; + digitDisplay.classList.remove('digit-highlight'); + } + }); + + // Update array values if changed + if (step.array) { + step.array.forEach((value, index) => { + if (cells[index]) { + cells[index].textContent = value; + cells[index].setAttribute('data-value', value); + } + }); + } + + // Update digit position display + if (step.digitPosition !== undefined) { + const positionName = ['Ones', 'Tens', 'Hundreds', 'Thousands'][step.digitPosition] || 'Position ' + (step.digitPosition + 1); + digitPosSpan.textContent = positionName + ' (' + (step.digitPosition + 1) + ')'; + } + + // Handle different step types with color coding + if (step.type === 'distribute') { + // Highlight current element being distributed + if (step.currentElementIndex !== undefined && cells[step.currentElementIndex]) { + cells[step.currentElementIndex].classList.add('distributing'); + + // Show extracted digit + const digitDisplay = cells[step.currentElementIndex].querySelector('.digit-display'); + if (digitDisplay && step.extractedDigit !== undefined) { + digitDisplay.textContent = step.extractedDigit; + digitDisplay.classList.add('digit-highlight'); + } + + // Apply color based on extracted digit + if (step.extractedDigit !== undefined) { + const digitColor = digitColors[step.extractedDigit]; + cells[step.currentElementIndex].style.backgroundColor = digitColor; + cells[step.currentElementIndex].style.color = 'white'; + cells[step.currentElementIndex].style.borderColor = digitColor; + } + } + } else if (step.type === 'collect') { + // Highlight element being collected + if (step.collectedTo !== undefined && cells[step.collectedTo]) { + cells[step.collectedTo].classList.add('collecting'); + } + } else if (step.type === 'distribution-complete') { + // Color all elements by their current digit + step.array.forEach((value, index) => { + if (cells[index] && step.digitPosition !== undefined) { + const digit = Math.floor(value / Math.pow(10, step.digitPosition)) % 10; + const digitColor = digitColors[digit]; + cells[index].style.backgroundColor = digitColor; + cells[index].style.color = 'white'; + cells[index].style.borderColor = digitColor; + cells[index].classList.add('digit-colored'); + } + }); + } else if (step.type === 'pass-complete') { + // Show completion of pass with gentle pulse + cells.forEach(cell => { + cell.classList.add('pass-complete'); + }); + } + + // Final completion state + if (step.type === 'complete') { + cells.forEach(cell => { + cell.className = 'viz-cell radix-cell complete'; + cell.style.backgroundColor = ''; + cell.style.color = ''; + cell.style.borderColor = ''; + const digitDisplay = cell.querySelector('.digit-display'); + if (digitDisplay) { + digitDisplay.textContent = ''; + digitDisplay.classList.remove('digit-highlight'); + } + }); + digitPosSpan.textContent = 'Sorting Complete!'; + } + + // Update status + statusDiv.textContent = step.message; + + // Show step info in container + const stepInfo = document.createElement('div'); + stepInfo.className = step.type === 'complete' ? 'viz-step-info complete' : 'viz-step-info'; + + let stepTypeColor = '#007acc'; + if (step.type === 'complete') stepTypeColor = '#28a745'; + else if (step.type === 'distribute') stepTypeColor = '#ff9800'; + else if (step.type === 'collect') stepTypeColor = '#4caf50'; + else if (step.phase === 'digit-processing') stepTypeColor = '#2196f3'; + + stepInfo.style.borderLeftColor = stepTypeColor; + + let phaseEmoji = '*'; + if (step.type === 'distribute') phaseEmoji = '+'; + else if (step.type === 'collect') phaseEmoji = '-'; + else if (step.type === 'complete') phaseEmoji = '!'; + else if (step.type === 'pass-start') phaseEmoji = '>'; + + stepInfo.innerHTML = + '' + phaseEmoji + ' Step ' + (currentStepIndex + 1) + ': ' + step.message + '
' + + '' + + 'Phase: ' + (step.phase || 'processing') + ' | ' + + 'Pass: ' + (step.passNumber || 'N/A') + ' | ' + + 'Operations: ' + (step.metrics.bucketOperations || 0) + + ''; + + if (stepsContainer.children.length > 8) { + stepsContainer.removeChild(stepsContainer.firstChild); + } + stepsContainer.appendChild(stepInfo); + } + + function startRadixAnimation() { + if (animationRunning || currentStepIndex >= steps.length) return; + + animationRunning = true; + document.getElementById('start-radix-animation').disabled = true; + document.getElementById('pause-radix-animation').disabled = false; + + animationInterval = setInterval(() => { + if (currentStepIndex >= steps.length) { + clearInterval(animationInterval); + animationRunning = false; + document.getElementById('start-radix-animation').disabled = false; + document.getElementById('pause-radix-animation').disabled = true; + return; + } + + updateRadixVisualization(steps[currentStepIndex]); + currentStepIndex++; + }, 1200); // 1.2 second delay between steps + } + + function pauseRadixAnimation() { + clearInterval(animationInterval); + animationRunning = false; + document.getElementById('start-radix-animation').disabled = false; + document.getElementById('pause-radix-animation').disabled = true; + } + + function resetRadixAnimation() { + clearInterval(animationInterval); + animationRunning = false; + currentStepIndex = 0; + document.getElementById('start-radix-animation').disabled = false; + document.getElementById('pause-radix-animation').disabled = true; + stepsContainer.innerHTML = ''; + + // Reset visualization + document.getElementById('current-digit-pos').textContent = 'Ready to start...'; + + // Reset all cells to default state + const cells = arrayDiv.querySelectorAll('.viz-cell'); + cells.forEach(cell => { + cell.className = 'viz-cell radix-cell'; + cell.style.backgroundColor = ''; + cell.style.color = ''; + cell.style.borderColor = ''; + const digitDisplay = cell.querySelector('.digit-display'); + if (digitDisplay) { + digitDisplay.textContent = ''; + digitDisplay.classList.remove('digit-highlight'); + } + }); + + if (steps.length > 0) { + updateRadixVisualization(steps[0]); + } + document.getElementById('radix-status').textContent = 'Ready to start radix sort animation...'; + } + + // Bind control events + document.getElementById('start-radix-animation').addEventListener('click', startRadixAnimation); + document.getElementById('pause-radix-animation').addEventListener('click', pauseRadixAnimation); + document.getElementById('reset-radix-animation').addEventListener('click', resetRadixAnimation); + + // Show initial state + if (steps.length > 0) { + updateRadixVisualization(steps[0]); + } + } ` }; diff --git a/algorithms-js/src/sort/wiggle-sort/wiggle-sort-core.js b/algorithms-js/src/sort/wiggle-sort/wiggle-sort-core.js index cdea0406..4b900e02 100644 --- a/algorithms-js/src/sort/wiggle-sort/wiggle-sort-core.js +++ b/algorithms-js/src/sort/wiggle-sort/wiggle-sort-core.js @@ -14,14 +14,10 @@ * @see https://github.com/sachinlala/SimplifyLearning */ -// Import utilities for reusable functions -// Import utilities for reusable functions -// Note: SortingUtils is available globally via window.SortingUtils when loaded by universal loader +// SortingUtils is available globally via window.SortingUtils when loaded by universal loader if (typeof require !== 'undefined') { - // Node.js environment const SortingUtils = require('../utils/sorting-utils.js'); } -// In browser environment, SortingUtils is available via window.SortingUtils /** * Wiggle Sort I - In-place O(n) algorithm @@ -42,18 +38,15 @@ function wiggleSortI(arr) { let comparisons = 0; let swaps = 0; - // In-place wiggle sort algorithm for (let i = 0; i < n - 1; i++) { comparisons++; if (i % 2 === 0) { - // Even index: should be less than next (valley) if (sortedArray[i] > sortedArray[i + 1]) { SortingUtils.swap(sortedArray, i, i + 1); swaps++; } } else { - // Odd index: should be greater than next (peak) if (sortedArray[i] < sortedArray[i + 1]) { SortingUtils.swap(sortedArray, i, i + 1); swaps++; diff --git a/algorithms-js/src/sort/wiggle-sort/wiggle-sort-steps.js b/algorithms-js/src/sort/wiggle-sort/wiggle-sort-steps.js index 0e6c1a4e..0ccfa6e8 100644 --- a/algorithms-js/src/sort/wiggle-sort/wiggle-sort-steps.js +++ b/algorithms-js/src/sort/wiggle-sort/wiggle-sort-steps.js @@ -2,36 +2,464 @@ * Wiggle Sort - Step Tracking for Visualization * * This file contains step-by-step tracking logic for Wiggle Sort visualization. - * Currently placeholder - to be implemented in future animation updates. + * Implements detailed step tracking for both Wiggle Sort I and II variants. * * @see https://github.com/sachinlala/SimplifyLearning */ /** - * Wiggle Sort with step-by-step tracking for visualization + * Wiggle Sort I with step-by-step tracking for visualization + * In-place O(n) algorithm that creates wiggling pattern * @param {number[]} arr - Array to be sorted * @returns {Object} Result with sorted array, steps, and metrics */ -function wiggleSortWithSteps(arr) { - // TODO: Implement step-by-step tracking for Wiggle Sort visualization - // This is a placeholder that will be enhanced in future iterations +function wiggleSortIWithSteps(arr) { + if (!arr || arr.length <= 1) { + return { + sortedArray: arr || [], + steps: [], + metrics: { comparisons: 0, swaps: 0, wiggleOperations: 0 } + }; + } + + const sortedArray = [...arr]; + const n = sortedArray.length; + const steps = []; + let comparisons = 0; + let swaps = 0; + let wiggleOperations = 0; + + // Initial state + steps.push({ + type: 'start', + array: [...sortedArray], + message: 'Starting Wiggle Sort I - creating wiggling pattern in-place', + phase: 'initialization', + variant: 'I', + comparisons, + swaps, + wiggleOperations, + highlightIndices: [] + }); + + // Pattern explanation + steps.push({ + type: 'pattern-explanation', + array: [...sortedArray], + message: 'Target pattern: arr[0] < arr[1] > arr[2] < arr[3] > arr[4]... (valley-peak-valley-peak)', + phase: 'explanation', + variant: 'I', + comparisons, + swaps, + wiggleOperations, + highlightIndices: [] + }); + + // Process each adjacent pair + for (let i = 0; i < n - 1; i++) { + comparisons++; + wiggleOperations++; + + const isEvenIndex = i % 2 === 0; + const expectedRelation = isEvenIndex ? 'valley (<=)' : 'peak (>=)'; + const currentValue = sortedArray[i]; + const nextValue = sortedArray[i + 1]; + + // Show what we're checking + steps.push({ + type: 'check-position', + array: [...sortedArray], + message: `Position ${i} should be ${expectedRelation} relative to position ${i + 1}`, + phase: 'checking', + variant: 'I', + currentIndex: i, + nextIndex: i + 1, + expectedPattern: isEvenIndex ? 'valley' : 'peak', + comparisons, + swaps, + wiggleOperations, + highlightIndices: [i, i + 1] + }); + + // Compare values + steps.push({ + type: 'compare', + array: [...sortedArray], + message: `Comparing: ${currentValue} and ${nextValue}`, + phase: 'comparison', + variant: 'I', + currentIndex: i, + nextIndex: i + 1, + currentValue, + nextValue, + expectedPattern: isEvenIndex ? 'valley' : 'peak', + comparisons, + swaps, + wiggleOperations, + highlightIndices: [i, i + 1] + }); + + let needsSwap = false; + if (isEvenIndex) { + // Even index: should be valley (less than or equal to next) + needsSwap = currentValue > nextValue; + } else { + // Odd index: should be peak (greater than or equal to next) + needsSwap = currentValue < nextValue; + } + + if (needsSwap) { + // Swap needed + [sortedArray[i], sortedArray[i + 1]] = [sortedArray[i + 1], sortedArray[i]]; + swaps++; + + steps.push({ + type: 'swap', + array: [...sortedArray], + message: `Swapped ${currentValue} and ${nextValue} to fix ${isEvenIndex ? 'valley' : 'peak'} pattern`, + phase: 'swapping', + variant: 'I', + swappedIndices: [i, i + 1], + swappedValues: [currentValue, nextValue], + patternFixed: isEvenIndex ? 'valley' : 'peak', + comparisons, + swaps, + wiggleOperations, + highlightIndices: [i, i + 1] + }); + } else { + // No swap needed + steps.push({ + type: 'no-swap', + array: [...sortedArray], + message: `Pattern already correct: ${currentValue} ${isEvenIndex ? '≀' : 'β‰₯'} ${nextValue}`, + phase: 'validation', + variant: 'I', + currentIndex: i, + nextIndex: i + 1, + patternCorrect: isEvenIndex ? 'valley' : 'peak', + comparisons, + swaps, + wiggleOperations, + highlightIndices: [i, i + 1] + }); + } + } + + // Final validation + steps.push({ + type: 'validation', + array: [...sortedArray], + message: 'Validating final wiggle pattern...', + phase: 'validation', + variant: 'I', + comparisons, + swaps, + wiggleOperations, + highlightIndices: [] + }); + + // Check final pattern + const patternAnalysis = analyzeWigglePattern(sortedArray); + + steps.push({ + type: 'complete', + array: [...sortedArray], + message: `Wiggle Sort I complete! Made ${comparisons} comparisons and ${swaps} swaps. Pattern is ${patternAnalysis.isValid ? 'valid' : 'invalid'}.`, + phase: 'complete', + variant: 'I', + patternAnalysis, + comparisons, + swaps, + wiggleOperations, + highlightIndices: [] + }); + + return { + sortedArray, + steps, + metrics: { comparisons, swaps, wiggleOperations, variant: 'I' } + }; +} + +/** + * Wiggle Sort II with step-by-step tracking for visualization + * Sort-based algorithm that avoids adjacent duplicates + * @param {number[]} arr - Array to be sorted + * @returns {Object} Result with sorted array, steps, and metrics + */ +function wiggleSortIIWithSteps(arr) { + if (!arr || arr.length <= 1) { + return { + sortedArray: arr || [], + steps: [], + metrics: { comparisons: 0, swaps: 0, wiggleOperations: 0 } + }; + } + + const n = arr.length; + const steps = []; + let comparisons = 0; + let swaps = 0; + let wiggleOperations = 0; + + // Initial state + steps.push({ + type: 'start', + array: [...arr], + message: 'Starting Wiggle Sort II - sort first, then rearrange to avoid adjacent duplicates', + phase: 'initialization', + variant: 'II', + comparisons, + swaps, + wiggleOperations, + highlightIndices: [] + }); + + // Step 1: Sort the array + const sortedArray = [...arr]; + steps.push({ + type: 'sort-phase', + array: [...sortedArray], + message: 'Step 1: Sorting array to prepare for rearrangement', + phase: 'sorting', + variant: 'II', + comparisons, + swaps, + wiggleOperations, + highlightIndices: [] + }); + + sortedArray.sort((a, b) => { + comparisons++; + return a - b; + }); + + steps.push({ + type: 'sort-complete', + array: [...sortedArray], + message: `Array sorted: [${sortedArray.join(', ')}]`, + phase: 'sorting', + variant: 'II', + comparisons, + swaps, + wiggleOperations, + highlightIndices: [] + }); + + // Step 2: Split into two halves + const mid = Math.floor((n + 1) / 2); + const smallerHalf = sortedArray.slice(0, mid); + const largerHalf = sortedArray.slice(mid); + + steps.push({ + type: 'split-halves', + array: [...sortedArray], + message: `Split into smaller half [${smallerHalf.join(', ')}] and larger half [${largerHalf.join(', ')}]`, + phase: 'splitting', + variant: 'II', + smallerHalf: [...smallerHalf], + largerHalf: [...largerHalf], + mid, + comparisons, + swaps, + wiggleOperations, + highlightIndices: [] + }); + + // Step 3: Rearrange in wiggle pattern + const result = new Array(n); + let left = mid - 1; // End of smaller half + let right = n - 1; // End of larger half + + steps.push({ + type: 'rearrange-start', + array: [...sortedArray], + message: 'Step 2: Rearranging - valleys (even positions) get smaller elements, peaks (odd positions) get larger elements', + phase: 'rearranging', + variant: 'II', + result: [...result], + leftPointer: left, + rightPointer: right, + comparisons, + swaps, + wiggleOperations, + highlightIndices: [] + }); + + // Fill result array alternately + for (let i = 0; i < n; i++) { + wiggleOperations++; + + if (i % 2 === 0) { + // Even positions get smaller elements (valleys) + result[i] = sortedArray[left]; + steps.push({ + type: 'place-valley', + array: [...sortedArray], + message: `Position ${i} (valley): placing ${sortedArray[left]} from smaller half`, + phase: 'rearranging', + variant: 'II', + result: [...result], + currentPosition: i, + placedValue: sortedArray[left], + sourceIndex: left, + patternType: 'valley', + leftPointer: left, + rightPointer: right, + comparisons, + swaps: swaps + 1, // Count placements as swaps + wiggleOperations, + highlightIndices: [i] + }); + left--; + } else { + // Odd positions get larger elements (peaks) + result[i] = sortedArray[right]; + steps.push({ + type: 'place-peak', + array: [...sortedArray], + message: `Position ${i} (peak): placing ${sortedArray[right]} from larger half`, + phase: 'rearranging', + variant: 'II', + result: [...result], + currentPosition: i, + placedValue: sortedArray[right], + sourceIndex: right, + patternType: 'peak', + leftPointer: left, + rightPointer: right, + comparisons, + swaps: swaps + 1, // Count placements as swaps + wiggleOperations, + highlightIndices: [i] + }); + right--; + } + swaps++; // Count placement operations + } + + // Final validation + steps.push({ + type: 'validation', + array: [...result], + message: 'Validating final wiggle pattern and checking for adjacent duplicates...', + phase: 'validation', + variant: 'II', + comparisons, + swaps, + wiggleOperations, + highlightIndices: [] + }); + + const patternAnalysis = analyzeWigglePattern(result); + const hasAdjacentDuplicates = checkAdjacentDuplicates(result); + steps.push({ + type: 'complete', + array: [...result], + message: `Wiggle Sort II complete! Made ${comparisons} comparisons and ${swaps} placements. Pattern is ${patternAnalysis.isValid ? 'valid' : 'invalid'}, adjacent duplicates: ${hasAdjacentDuplicates ? 'present' : 'avoided'}.`, + phase: 'complete', + variant: 'II', + patternAnalysis, + hasAdjacentDuplicates, + comparisons, + swaps, + wiggleOperations, + highlightIndices: [] + }); + return { - sortedArray: [...arr], - steps: [], - metrics: { comparisons: 0, swaps: 0, wiggleOperations: 0 } + sortedArray: result, + steps, + metrics: { comparisons, swaps, wiggleOperations, variant: 'II' } }; } +/** + * Main wiggle sort with steps function - delegates to appropriate variant + * @param {number[]} arr - Array to be sorted + * @param {string} variant - 'I' or 'II' + * @returns {Object} Result with sorted array, steps, and metrics + */ +function wiggleSortWithSteps(arr, variant = 'I') { + if (variant === 'II') { + return wiggleSortIIWithSteps(arr); + } else { + return wiggleSortIWithSteps(arr); + } +} + +/** + * Analyze wiggle pattern of an array + * @param {number[]} arr - Array to analyze + * @returns {Object} Pattern analysis + */ +function analyzeWigglePattern(arr) { + if (!arr || arr.length <= 1) { + return { pattern: '', isValid: true, violations: 0 }; + } + + let pattern = ''; + let violations = 0; + + for (let i = 0; i < arr.length - 1; i++) { + if (arr[i] < arr[i + 1]) { + pattern += '<'; + // Check if this matches expected pattern + if (i % 2 !== 0) violations++; // Odd indices should be >= + } else if (arr[i] > arr[i + 1]) { + pattern += '>'; + // Check if this matches expected pattern + if (i % 2 === 0) violations++; // Even indices should be <= + } else { + pattern += '='; + // Equal is okay for both patterns in wiggle sort I + } + } + + return { + pattern, + isValid: violations === 0, + violations + }; +} + +/** + * Check for adjacent duplicates in array + * @param {number[]} arr - Array to check + * @returns {boolean} True if adjacent duplicates exist + */ +function checkAdjacentDuplicates(arr) { + if (!arr || arr.length <= 1) return false; + + for (let i = 0; i < arr.length - 1; i++) { + if (arr[i] === arr[i + 1]) { + return true; + } + } + return false; +} + // Export for both Node.js and browser environments if (typeof module !== 'undefined' && module.exports) { module.exports = { - wiggleSortWithSteps + wiggleSortWithSteps, + wiggleSortIWithSteps, + wiggleSortIIWithSteps, + analyzeWigglePattern, + checkAdjacentDuplicates }; } else if (typeof window !== 'undefined') { window.WiggleSortSteps = { - wiggleSortWithSteps + wiggleSortWithSteps, + wiggleSortIWithSteps, + wiggleSortIIWithSteps, + analyzeWigglePattern, + checkAdjacentDuplicates }; // Expose commonly used functions in global scope for demo configs window.wiggleSortWithSteps = wiggleSortWithSteps; -} \ No newline at end of file + window.wiggleSortIWithSteps = wiggleSortIWithSteps; + window.wiggleSortIIWithSteps = wiggleSortIIWithSteps; +} diff --git a/algorithms-js/src/sort/wiggle-sort/wiggle-sort.config.js b/algorithms-js/src/sort/wiggle-sort/wiggle-sort.config.js index 5205bbf4..3fd52a02 100644 --- a/algorithms-js/src/sort/wiggle-sort/wiggle-sort.config.js +++ b/algorithms-js/src/sort/wiggle-sort/wiggle-sort.config.js @@ -12,6 +12,7 @@ const WIGGLE_SORT_CONFIG = { name: "Wiggle Sort", category: "sort", hasStepsFile: true, + hasVisualization: true, problem: "Arrange array elements in a wiggling pattern where arr[0] < arr[1] > arr[2] < arr[3] > arr[4]... Elements alternate between peaks and valleys.", // Inputs for the demo interface @@ -36,6 +37,14 @@ const WIGGLE_SORT_CONFIG = { } ], + // Multi-language source code paths + sourceCode: { + javascript: "https://github.com/sachinlala/SimplifyLearning/blob/master/algorithms-js/src/sort/wiggle-sort/wiggle-sort-core.js", + java: "https://github.com/sachinlala/SimplifyLearning/tree/master/algorithms-java/src/main/java/com/sl/algorithms/sort/wiggle", + python: "", // Coming soon + go: "" // Coming soon + }, + explanation: { description: 'Wiggle Sort rearranges an array so that elements alternate between being smaller and larger than their neighbors, creating a "wiggling" pattern. Wiggle Sort I achieves this in O(n) time by making local adjustments, while Wiggle Sort II handles duplicates more carefully to avoid adjacent identical elements.' }, @@ -279,7 +288,294 @@ const WIGGLE_SORT_CONFIG = { disadvantages: ["O(n log n) time", "Extra space needed"] } } - } + }, + + customDemoFunction: ` + function runDemo() { + const arrayInputStr = document.getElementById('array-input').value; + const variant = document.getElementById('variant').value; + const resultContainer = document.getElementById('result'); + const errorContainer = document.getElementById('error-message'); + const visualizationSection = document.getElementById('visualization-section'); + + // Clear previous error and result + errorContainer.innerHTML = ''; + errorContainer.style.display = 'none'; + resultContainer.innerHTML = ''; + visualizationSection.style.display = 'none'; + + // Parse input array + let arrayInput; + try { + arrayInput = arrayInputStr.split(',').map(item => { + const trimmed = item.trim(); + const asNumber = parseInt(trimmed); + if (isNaN(asNumber)) { + throw new Error('All elements must be integers'); + } + return asNumber; + }); + } catch (e) { + showError('Invalid array format. Please use comma-separated integers.'); + return; + } + + // Validate input + if (arrayInput.length === 0) { + showError('Array cannot be empty'); + return; + } + + if (arrayInput.length > 15) { + showError('Array size limited to 15 elements for demo purposes'); + return; + } + + try { + const startTime = performance.now(); + + // Execute wiggle sort using steps function for animation + let result; + if (window.WiggleSortSteps) { + if (variant === 'II') { + result = window.WiggleSortSteps.wiggleSortIIWithSteps(arrayInput); + } else { + result = window.WiggleSortSteps.wiggleSortIWithSteps(arrayInput); + } + } else if (window.wiggleSortWithSteps) { + result = window.wiggleSortWithSteps(arrayInput, variant); + } else if (window.WiggleSortCore) { + // Fallback to core functions without steps + if (variant === 'II') { + const coreResult = window.WiggleSortCore.wiggleSortII(arrayInput); + result = { ...coreResult, steps: [] }; + } else { + const coreResult = window.WiggleSortCore.wiggleSortI(arrayInput); + result = { ...coreResult, steps: [] }; + } + } else { + // Fallback if nothing loaded + result = { sortedArray: [...arrayInput].sort((a, b) => a - b), metrics: { comparisons: 0, swaps: 0 }, steps: [] }; + } + + const endTime = performance.now(); + const executionTime = (endTime - startTime).toFixed(4); + + // Analyze the pattern of the result + const patternAnalysis = window.WiggleSortConfigUtils ? + window.WiggleSortConfigUtils.analyzeWigglePattern(result.sortedArray) : + { pattern: 'N/A', isValid: 'Unknown' }; + + // Show result + let resultHTML = + 'Original Array: [' + arrayInput.join(', ') + ']
' + + 'Wiggle Sorted Array: [' + result.sortedArray.join(', ') + ']
' + + 'Variant: Wiggle Sort ' + variant + '
' + + 'Pattern: ' + patternAnalysis.pattern + '
' + + 'Valid Wiggle: ' + (patternAnalysis.isValid ? 'Yes' : 'No') + '
' + + 'Comparisons: ' + (result.metrics.comparisons || 0) + '
' + + 'Swaps: ' + (result.metrics.swaps || 0) + '
' + + 'Time Complexity: ' + (variant === 'II' ? 'O(n log n)' : 'O(n)') + '
' + + 'Space Complexity: ' + (variant === 'II' ? 'O(n)' : 'O(1)') + '
' + + 'Execution Time: ' + executionTime + ' ms'; + + resultContainer.innerHTML = resultHTML; + + // Show the visualization section with wiggle sort animation + if (result.steps && result.steps.length > 0) { + showWiggleSortVisualization(arrayInput, result.steps, variant); + visualizationSection.style.display = 'block'; + } + + } catch (error) { + showError(error.message); + } + } + + function showWiggleSortVisualization(originalArray, steps, variant) { + const arrayViz = document.getElementById('array-visualization'); + const stepsContainer = document.getElementById('steps-container'); + + // Clear previous visualization + arrayViz.innerHTML = ''; + stepsContainer.innerHTML = ''; + + // Create array visualization + const arrayDiv = document.createElement('div'); + arrayDiv.className = 'array-visualization'; + arrayDiv.id = 'wiggle-array-display'; + + originalArray.forEach((value, index) => { + const cell = document.createElement('div'); + cell.textContent = value; + cell.className = 'viz-cell'; + cell.setAttribute('data-index', index); + cell.setAttribute('data-value', value); + arrayDiv.appendChild(cell); + }); + + arrayViz.appendChild(arrayDiv); + + // Add controls with legend + const controlsDiv = document.createElement('div'); + controlsDiv.className = 'viz-controls'; + controlsDiv.innerHTML = + '

Wiggle Sort ' + variant + ' Visualization

' + + '' + + '' + + '' + + '
' + + 'πŸ”΅ Valley (Even) | πŸ”΄ Peak (Odd) | 🟑 Comparing | 🟒 Swapping | βœ… Complete' + + '' + + '
'; + arrayViz.appendChild(controlsDiv); + + // Status display + const statusDiv = document.createElement('div'); + statusDiv.id = 'wiggle-status'; + statusDiv.className = 'viz-status'; + statusDiv.textContent = 'Ready to start wiggle sort animation...'; + arrayViz.appendChild(statusDiv); + + // Animation variables + let currentStepIndex = 0; + let animationRunning = false; + let animationInterval; + + function updateWiggleVisualization(step) { + const cells = arrayDiv.querySelectorAll('.viz-cell'); + const statusDiv = document.getElementById('wiggle-status'); + + // Reset all cell classes + cells.forEach(cell => { + cell.className = 'viz-cell'; + }); + + // Update array values + step.array.forEach((value, index) => { + if (cells[index]) { + cells[index].textContent = value; + } + }); + + // Color cells based on their position (valley or peak pattern) + step.array.forEach((value, index) => { + if (cells[index]) { + if (index % 2 === 0) { + cells[index].classList.add('valley'); // Even positions = valleys + } else { + cells[index].classList.add('peak'); // Odd positions = peaks + } + } + }); + + // Highlight current indices being processed + if (step.highlightIndices) { + step.highlightIndices.forEach(index => { + if (cells[index]) { + cells[index].classList.add('comparing'); + } + }); + } + + // Highlight swapped indices + if (step.swappedIndices) { + step.swappedIndices.forEach(index => { + if (cells[index]) { + cells[index].classList.add('swapping'); + } + }); + } + + // Update status + statusDiv.textContent = step.message; + + // Show step info in container + const stepInfo = document.createElement('div'); + stepInfo.className = step.type === 'complete' ? 'viz-step-info complete' : 'viz-step-info'; + + let stepTypeColor = '#007acc'; + if (step.type === 'complete') stepTypeColor = '#28a745'; + else if (step.type === 'swap') stepTypeColor = '#dc3545'; + else if (step.type === 'compare') stepTypeColor = '#ffc107'; + + stepInfo.style.borderLeftColor = stepTypeColor; + + stepInfo.innerHTML = + 'Step ' + (currentStepIndex + 1) + ': ' + step.message + '
' + + '' + + 'Phase: ' + (step.phase) + ' | ' + + 'Comparisons: ' + (step.comparisons || 0) + ' | ' + + 'Swaps: ' + (step.swaps || 0) + ' | ' + + 'Pattern: ' + (step.patternFixed || step.expectedPattern || 'N/A') + + ''; + + if (stepsContainer.children.length > 8) { + stepsContainer.removeChild(stepsContainer.firstChild); + } + stepsContainer.appendChild(stepInfo); + } + + function startWiggleAnimation() { + if (animationRunning || currentStepIndex >= steps.length) return; + + animationRunning = true; + document.getElementById('start-wiggle-animation').disabled = true; + document.getElementById('pause-wiggle-animation').disabled = false; + + animationInterval = setInterval(() => { + if (currentStepIndex >= steps.length) { + clearInterval(animationInterval); + animationRunning = false; + document.getElementById('start-wiggle-animation').disabled = false; + document.getElementById('pause-wiggle-animation').disabled = true; + return; + } + + updateWiggleVisualization(steps[currentStepIndex]); + currentStepIndex++; + }, 1500); // 1.5 second delay between steps + } + + function pauseWiggleAnimation() { + clearInterval(animationInterval); + animationRunning = false; + document.getElementById('start-wiggle-animation').disabled = false; + document.getElementById('pause-wiggle-animation').disabled = true; + } + + function resetWiggleAnimation() { + clearInterval(animationInterval); + animationRunning = false; + currentStepIndex = 0; + document.getElementById('start-wiggle-animation').disabled = false; + document.getElementById('pause-wiggle-animation').disabled = true; + stepsContainer.innerHTML = ''; + + // Reset visualization + if (steps.length > 0) { + updateWiggleVisualization(steps[0]); + } + document.getElementById('wiggle-status').textContent = 'Ready to start wiggle sort animation...'; + } + + // Bind control events + document.getElementById('start-wiggle-animation').addEventListener('click', startWiggleAnimation); + document.getElementById('pause-wiggle-animation').addEventListener('click', pauseWiggleAnimation); + document.getElementById('reset-wiggle-animation').addEventListener('click', resetWiggleAnimation); + + // Show initial state + if (steps.length > 0) { + updateWiggleVisualization(steps[0]); + } + } + ` }; // Utility functions for config @@ -388,77 +684,7 @@ const WiggleSortConfigUtils = { violations, isValid: violations === 0 }; - }, - - customDemoFunction: ` - function runDemo() { - const arrayInputStr = document.getElementById('array-input').value; - const variant = document.getElementById('variant').value; - const resultContainer = document.getElementById('result'); - const errorContainer = document.getElementById('error-message'); - const visualizationSection = document.getElementById('visualization-section'); - - // Clear previous error and result - errorContainer.innerHTML = ''; - errorContainer.style.display = 'none'; - resultContainer.innerHTML = ''; - visualizationSection.style.display = 'none'; - - // Parse input array - let arrayInput; - try { - arrayInput = arrayInputStr.split(',').map(item => { - const trimmed = item.trim(); - const asNumber = parseInt(trimmed); - if (isNaN(asNumber)) { - throw new Error('All elements must be integers'); - } - return asNumber; - }); - } catch (e) { - showError('Invalid array format. Please use comma-separated integers.'); - return; - } - - // Validate input - if (arrayInput.length === 0) { - showError('Array cannot be empty'); - return; - } - - if (arrayInput.length > 15) { - showError('Array size limited to 15 elements for demo purposes'); - return; - } - - try { - const startTime = performance.now(); - - // Execute wiggle sort - const result = window.WiggleSortCore ? - window.WiggleSortCore.wiggleSort(arrayInput, variant) : - wiggleSort(arrayInput, variant); - - const endTime = performance.now(); - const executionTime = (endTime - startTime).toFixed(4); - - // Show result - let resultHTML = \` - Original Array: [\${arrayInput.join(', ')}]
- Wiggle Sorted Array: [\${result.sortedArray.join(', ')}]
- Variant: \${variant}
- Pattern: \${result.pattern || 'a0 < a1 > a2 < a3 > ...'}
- Total Swaps: \${result.metrics.swaps || 0}
- Execution Time: \${executionTime} ms - \`; - - resultContainer.innerHTML = resultHTML; - - } catch (error) { - showError(error.message); - } - } - ` + } }; // Export for both Node.js and browser environments @@ -468,11 +694,15 @@ if (typeof module !== 'undefined' && module.exports) { WiggleSortConfigUtils }; } else if (typeof window !== 'undefined') { + // Primary exports window.WIGGLE_SORT_CONFIG = WIGGLE_SORT_CONFIG; window.WiggleSortConfigUtils = WiggleSortConfigUtils; - // Additional exports for universal loader compatibility + // Universal loader compatibility - these are the names the loader looks for + window.wigglesortConfig = WIGGLE_SORT_CONFIG; // algorithmName.replace(/-/g, '') + 'Config' + window.wigglesortconfig = WIGGLE_SORT_CONFIG; // algorithmName.replace(/-/g, '').toLowerCase() + 'Config' + window.wiggleSortConfig = WIGGLE_SORT_CONFIG; // toCamelCase(algorithmName) + 'Config' + + // Legacy compatibility window.WiggleSortConfig = WIGGLE_SORT_CONFIG; - window.wigglesortConfig = WIGGLE_SORT_CONFIG; - window.wigglesortconfig = WIGGLE_SORT_CONFIG; } diff --git a/algorithms-js/test-algorithm.html b/algorithms-js/test-algorithm.html new file mode 100644 index 00000000..66663518 --- /dev/null +++ b/algorithms-js/test-algorithm.html @@ -0,0 +1,869 @@ + + + + + + Testing Lab + + + +
+

πŸ§ͺ Testing Lab

+ +
+ +
+

🎯 Algorithm Selection

+

Select an algorithm to test its implementation, step tracking, and demo functionality.

+ +
+ + +
+ +
+ + +
+ +
+ + + +
+ +
+ + + +
+ +
+
πŸ“¦ Dependencies
+
βš™οΈ Configuration
+
πŸ”§ Core Functions
+
🎬 Step Tracking
+
🎭 Demo Simulation
+
+ +
+
+

Dependency Loading Test

+

Testing if all required scripts and dependencies load correctly...

+ +
+
+
+ +
+
+

Configuration Loading Test

+

Testing if algorithm configuration loads and exports correctly...

+ +
+
+
+ +
+
+

Core Functions Test

+

Testing if algorithm core functions are available and working...

+ +
+
+
+ +
+
+

Step Tracking Test

+

Testing if step-by-step tracking works for animations...

+ +
+
+
+ +
+
+

Demo Simulation

+

Simulating the actual demo interface with full functionality...

+ +
+
+
+ +
+

πŸ”— Quick Links

+ + + +
+ + + + \ No newline at end of file